DEV Community

Greg Molnar
Greg Molnar

Posted on • Originally published at greg.molnar.io

Deploying a Rails app with Kamal

What is Kamal?

You probably already heard about kamal, a new tool from DHH to deploy Rails apps with docker containers. It is pretty similar to Capistrano, with the difference of using containers, so preparing the servers is less effort and you don't really need to know much in that area to be able to deploy a Rails app.

In this tutorial, I will show you how to

  • deploy a Rails app to a VPS
  • get automatic SSL certificates with Traefik
  • use a hosted database server
  • run Redis on the same droplet
  • run a worker to process background jobs

You need to point your DNS records to the IP address of the droplet, and they will probably propagate by the time you finish your first deployment.

Setup Kamal

To set up kamal in your app, you need to install the gem. It doesn't have to be in the Gemfile, just needs to be available in your terminal, so you can just run gem install kamal. Once that's done, you need to call kamal init --bundle inside your Rails app:

[~/git/kamal-example] +(main) kamal init --bundle Created configuration file in config/deploy.yml Created .env file Created sample hooks in .kamal/hooks Adding MRSK to Gemfile and bundle... INFO [55b3727e] Running /usr/bin/env bundle add kamal as gregmolnar@localhost INFO [55b3727e] Finished in 3.111 seconds with exit status 0 (successful). INFO [12808c1a] Running /usr/bin/env bundle binstubs kamal as gregmolnar@localhost INFO [12808c1a] Finished in 0.120 seconds with exit status 0 (successful). Created binstub file in bin/kamal 
Enter fullscreen mode Exit fullscreen mode

The first thing I recommend to do is to gitignore the .env file, so you are not committing your secrets accidentally.

If you don't already have access to a container registry, now is the time to sign up for one. Some hosting providers, like Digital Ocean offer one with a free tier.

After that, let's open the .env file and add your container registry credentials and your Rails master key.

If you generated your Rails app with 7.1+, you already have a Dockerfile, but if you are on an older version, you need to create one, and it needs to look like this:

# syntax = docker/dockerfile:1 # Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile ARG RUBY_VERSION=3.2.2 FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base # Rails app lives here WORKDIR /rails # 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 libvips pkg-config curl # 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/ # Precompiling assets for production without requiring secret RAILS_MASTER_KEY RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile # Final stage for app image FROM base # Install packages needed for deployment RUN apt-get update -qq && \ apt-get install --no-install-recommends -y libvips postgresql-client curl && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Copy built artifacts: gems, application COPY --from=build /usr/local/bundle /usr/local/bundle COPY --from=build /rails /rails # Run and own only the runtime files as a non-root user for security RUN useradd rails --home /rails --shell /bin/bash && \ chown -R rails:rails db log storage tmp USER rails:rails # Entrypoint prepares the database. ENTRYPOINT ["/rails/bin/docker-entrypoint"] # Start the server by default, this can be overwritten at runtime EXPOSE 3000 
Enter fullscreen mode Exit fullscreen mode

This Dockerfile configures the base image, the working directory, and the environment variables. It installs the necessary apt packages(make sure curl is added since it was not in the default Dockefile generated by Rails at the beginning), bundles the gems, copies over the application code, precompiles the assets, creates a non-root user to own the files and run the Rails process, sets the entry point and exposes port 3000.
In the default Rails Dockerfile, there is also a command to start the Rails server, but since we will also have a worker, we will run different commands in the containers and set those in the kamal config file.

And that is the next one you need to modify. This is what you will end up with. I will break it down below:

# Name of your application. Used to uniquely configure containers. service: my_awesome_app # Name of the container image. image: container_registry/my_awesome_app # Deploy to these servers. servers: web: hosts: - 111.11.111.11 labels: traefik.http.routers.domain.rule: Host(`yourdomain.com`) traefik.http.routers.domain.entrypoints: websecure traefik.http.routers.domain.tls.certresolver: letsencrypt options: "add-host": host.docker.internal:host-gateway cmd: "./bin/rails server" job: hosts: - 111.11.111.11 options: "add-host": host.docker.internal:host-gateway cmd: "bundle exec sidekiq -C config/sidekiq.yml -v" # Credentials for your image host. registry: # Specify the registry server, if you're not using Docker Hub server: registry.digitalocean.com username: - MRSK_REGISTRY_PASSWORD # Always use an access token rather than real password when possible. password: - MRSK_REGISTRY_PASSWORD # Inject ENV variables into containers (secrets come from .env). env: clear: REDIS_URL: "redis://host.docker.internal:36379/0" secret: - RAILS_MASTER_KEY - DATABASE_PASSWORD - SMTP_USER - SMTP_PASSWORD - DO_BUCKET_KEY - DO_BUCKET_SECRET - DO_BUCKET - SIDEKIQ_USERNAME - SIDEKIQ_PASSWORD # Configure builder setup. # builder: # args: # RUBY_VERSION: 3.2.0 # secrets: # - GITHUB_TOKEN # remote: # arch: amd64 # host: ssh://app@192.168.0.1 builder: multiarch: false # Use accessory services (secrets come from .env). accessories: redis: image: redis:latest roles: - web port: "36379:6379" volumes: - /var/lib/redis:/data # Configure custom arguments for Traefik traefik: # host_port: 8080 options: publish: - "443:443" volume: - "/letsencrypt/acme.json:/letsencrypt/acme.json" args: entryPoints.web.address: ":80" entryPoints.websecure.address: ":443" entryPoints.web.http.redirections.entryPoint.to: websecure entryPoints.web.http.redirections.entryPoint.scheme: https entryPoints.web.http.redirections.entrypoint.permanent: true entrypoints.websecure.http.tls: true entrypoints.websecure.http.tls.domains[0].main: "yourdomain.com" certificatesResolvers.letsencrypt.acme.email: "youremail@domain.com" certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json" certificatesResolvers.letsencrypt.acme.httpchallenge: true certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web # args: # accesslog: true # accesslog.format: json # Configure a custom healthcheck (default is /up on port 3000) # healthcheck: # path: /healthz # port: 4000 
Enter fullscreen mode Exit fullscreen mode

In this file, you need to specify the name of your app and the image you want to use. Then you can configure servers. In this example, there is a "web" server and a "job" server. They are both running on the same host, so let's make docker to put them on the internal network so they can access each other.

On the web server, kamal will start a Rails server, and on the job server it will start a Sidekiq process.

For the web server, we configure the traefik router for Let's Encrypt.

Then we configure the registry access details, and then we set the environment variables we need. REDIS_URL is not a secret, so we can just enter it directly. The rest will be pulled from the .env file.

The next step is to configure our builder. If you are on a Linux machine and deploying to Linux servers with the same architecture(like me), you can disable multiarch like in the above example to speed up the build process.

The next section configures the "accessories". An accessory can be a database server, memcached, etc., or Redis in this case. As for the database, let's use a hosted one by your provider(like Digital Ocean). For Redis, you will use the same VM.

You need to configure the image, set the role you want to put it on, forward the ports to the host, and mount a volume.

And the final part of this config file sets Traefik to listen on port 80 and 443 and configures Let's Encrypt. You need to adapt the above config with your domain name and email. You also need to SSH to your server and create the acme.json file with the correct permissions:

mkdir -p /letsencrypt && touch /letsencrypt/acme.json && chmod 600 /letsencrypt/acme.json 
Enter fullscreen mode Exit fullscreen mode

One final step is to turn on a firewall, preferably at your provider, and block everything except port 80 and 443, so Treafik and Redis are not accessible from the internet.

Everything is configured now, and it is time to build the server. For that, you need to call bin/kamal setup and wait patiently until your first build completes and deploys. Afterward, you can just run bin/kamal deploy to deploy a new build.

Once your app is deployed and your DNS records are propagated, you can access your newly deployed application.

Here are a few handy kamal commands:

  • If you want to start a Rails console: kamal app exec -i "bin/rails c"
  • If you want to see your logs: kamal app logs -f
  • If you have a stuck lock file, ssh to the host and delete the kamal_lock folder
  • kamal --help lists all the available commands

I hope this article helps to get your feet wet with kamal.

Top comments (0)