DEV Community

Max Moreno
Max Moreno

Posted on

5 1

Deploying a Rails API-Only App with Postgres using Kamal 2

In this article, we'll walk through the steps to deploy a Rails API-only application using Kamal. Whether you're a seasoned developer or someone who's just discovered that "bundle install" isn't a jewelry store promotion, this guide will help you get your app up and running in no time.

Prerequisites

A locally working Ruby on Rails app (rails 7 in this example).
This app, for this example, we won't use redis so if you use action cable, you need to have solid_cable configured. And for jobs, solid_queue (I use both here)
Docker Installed: Kamal uses Docker, so you'll need Docker installed. No, you can't skip this step—Docker is the star of this show!
A Server with SSH Access (DigitalOcean here). Think of it as your app's new home.
A container registry in DigitalOcean.

Configuring Kamal

Add this to your gemfile:

gem 'kamal', require: false
gem 'thruster', require: false
Enter fullscreen mode Exit fullscreen mode

Then

bundle install
kamal init
Enter fullscreen mode Exit fullscreen mode

This will create a couple of files:

  • config/deploy.yml
  • .kamal/secrets

config/deploy.yml should look like this (make sure to update the IPs and the env vars accordingly):

service: your_app_name_api

image: your_user/your_app_name_api

servers:
  web:
    - 199.xxx.xxx.xx
proxy:
  ssl: false
registry:
  server: registry.digitalocean.com
  username: your_user
  password:
    - KAMAL_REGISTRY_PASSWORD

builder:
  arch: amd64

# Inject ENV variables into containers (secrets come from .kamal/secrets).
env:
  secret:
    - RAILS_MASTER_KEY
    - POSTGRES_PASSWORD
  clear:
    SOLID_QUEUE_IN_PUMA: true
    DB_HOST: your_app_name_api-db

aliases:
  console: app exec --interactive --reuse "bin/rails console"
  shell: app exec --interactive --reuse "bash"
  logs: app logs -f
  dbc: app exec --interactive --reuse "bin/rails dbconsole"

volumes:
  - "your_app_name_api_storage:/app/storage"

accessories:
  db:
    image: postgres:17
    host: 199.xxx.xxx.xx
    port: 127.0.0.1:5432:5432
    env:
      clear:
        POSTGRES_USER: your_app_name_api
        POSTGRES_DB: your_app_name_api_production
        POSTGRES_HOST_AUTH_METHOD: trust
      secret:
        - YOUR_APP_NAME_API_DATABASE_PASSWORD
    files:
      - config/init.sql:/docker-entrypoint-initdb.d/setup.sql
    directories:
      - data:/var/lib/postgresql/data

Enter fullscreen mode Exit fullscreen mode

And in .kamal/secrets add:

KAMAL_REGISTRY_PASSWORD=dop_v1_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
RAILS_MASTER_KEY=$(cat config/master.key)
YOUR_APP_NAME_API_DATABASE_PASSWORD=anythingyouwant
POSTGRES_PASSWORD=anythingyouwant
Enter fullscreen mode Exit fullscreen mode

You will need a couple of additional files. Run this in your terminal:

touch bin/thrust
touch bin/docker-entrypoint
touch Dockerfile
touch config/init.sql
Enter fullscreen mode Exit fullscreen mode

In bin/thrust add:

#!/usr/bin/env ruby
require "rubygems"
require "bundler/setup"

load Gem.bin_path("thruster", "thrust")
Enter fullscreen mode Exit fullscreen mode

In bin/docker-entrypoint add:

#!/bin/bash -e

# Enable jemalloc for reduced memory usage and latency.
if [ -z "${LD_PRELOAD+x}" ]; then
    LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit)
    export LD_PRELOAD
fi

# If running the rails server then create or migrate existing db
if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then
  ./bin/rails db:prepare
fi

exec "${@}"
Enter fullscreen mode Exit fullscreen mode

The Dockerfile, since it's an api only app, this will be enough:

# syntax=docker/dockerfile:1
# check=error=true

# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t your_app_name_api .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name your_app_name_api your_app_name_api

# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.3.1
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

# Rails app lives here
WORKDIR /rails

# Install base packages
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev postgresql-client && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Set production environment
ENV RAILS_ENV="production" \
    BUNDLE_DEPLOYMENT="1" \
    BUNDLE_PATH="/usr/local/bundle" \
    BUNDLE_WITHOUT="development"

# Throw-away build stage to reduce size of final image
FROM base AS build

# Install packages needed to build gems
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential git libpq-dev pkg-config && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
    bundle exec bootsnap precompile --gemfile

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/


# Final stage for app image
FROM base

# Copy built artifacts: gems, application
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --from=build /rails /rails

# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
    chown -R rails:rails db log storage tmp
USER 1000:1000

# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]
Enter fullscreen mode Exit fullscreen mode

Finally, in config/init.sql add:

CREATE DATABASE your_app_name_api_production;
CREATE DATABASE your_app_name_api_production_cable;
Enter fullscreen mode Exit fullscreen mode

Next step is to make sure your database.yml matches the deploy configuration:

production:
  primary: &primary_production
    <<: *default
    database: your_app_name_api_production
    username: your_app_name_api
    password: your_app_name_api_password
    host: your_app_name_api-db
  cable:
    <<: *primary_production
    database: your_app_name_api_production_cable
    migrations_paths: db/cable_migrate
Enter fullscreen mode Exit fullscreen mode

You will also need to make sure that config/environments/production.rb has:

  config.assume_ssl = true
  config.force_ssl = true
  config.solid_queue.connects_to = { database: { writing: :production } } # in case you are using solid_queue
Enter fullscreen mode Exit fullscreen mode

If you don't have a commited file in storage/ you will need to add one:

touch storage/.keep
Enter fullscreen mode Exit fullscreen mode

Deploy

Afte commiting all changes with git, we are ready to go:

kamal setup
Enter fullscreen mode Exit fullscreen mode

After a few minutes, you should see your app deployed =)

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (1)

Collapse
 
feleval_app profile image
Feleval App

It worked right first time! thanks!

Having said that, I think secrets are not handled in a 'production grade' manner

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay