Buy my course: Security for Rails Developers.
This is an outdated article. Click here to read an updated version:
Deploying a Rails app with Kamal
What is MRSK?
You probably already heard about mrsk, 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
- run Caddy in front of the docker container to handle SSL
- use a hosted database server
- run Redis on the same droplet
- run a worker to process background jobs
Setting up Caddy
Caddy is an open-source web server with automatic SSL certificates, so it can be used in front of Puma servers. To set up Caddy on a droplet, follow the process described in the official docs. For instance, on a Debian-based machine, you need to run the following shell commands:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
The above commands will add the package registry to the server with its key, updates the list of available packages, and install Caddy. At the end of the installation, Caddy will be automatically started as a systemd service and will listen on ports 80 and 443.
Caddy is configured via the /etc/caddy/Caddyfile
, so let’s open that file with our favorite text editor(vim) on the server and setup our domain:
# /etc/caddy/Caddyfile
{
debug
log {
output file /var/log/caddy/caddy.log {
roll_size 10MB
}
}
}
www.yourdomain.com {
redir https://{host}{uri}
}
yourdomain.com {
reverse_proxy tcp://0.0.0.0:8080
}
You also 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 MRSK
To set up mrsk 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 mrsk
. Once that’s done, you need to call mrsk init --bundle
inside your Rails app:
[~/git/mrsk-example] +(main) mrsk init --bundle
Created configuration file in config/deploy.yml
Created .env file
Created sample hooks in .mrsk/hooks
Adding MRSK to Gemfile and bundle...
INFO [55b3727e] Running /usr/bin/env bundle add mrsk as gregmolnar@localhost
INFO [55b3727e] Finished in 3.111 seconds with exit status 0 (successful).
INFO [12808c1a] Running /usr/bin/env bundle binstubs mrsk as gregmolnar@localhost
INFO [12808c1a] Finished in 0.120 seconds with exit status 0 (successful).
Created binstub file in bin/mrsk
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 allowance.
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 will already have a Dockerfile, but if you are on an older version, you will need to create one, and it will need 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
What this Dockerfile does, in a nutshell, is it 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 mrsk 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
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
#publish:
# - 8080:8080
# args:
# accesslog: true
# accesslog.format: json
# Configure a custom healthcheck (default is /up on port 3000)
# healthcheck:
# path: /healthz
# port: 4000
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, mrsk will start a Rails server, and on the job server it will start a Sidekiq process.
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.
One important thing to note here is that you need to make sure the Redis port is closed on the firewall. I generally recommend to close all ports and just open the ones you have to.
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 run on port 8080 since Caddy is running in front of it.
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/mrsk setup
and wait patiently until your first build completes and deploys. Afterward, you can just run bin/mrsk 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 mrsk commands:
- If you want to start a Rails console:
mrsk app exec -i "bin/rails c"
- If you want to see your logs:
mrsk app logs -f
- If you have a stuck lock file, ssh to the host and delete the
mrsk_lock
folder mrsk --help
lists all the available commands
I hope this article helps to get your feet wet with mrsk.
Or follow me on Twitter
I run an indie startup providing vulnerability scanning for your Ruby on Rails app.
It is free to use at the moment, and I am grateful for any feedback about it.If you would like to give it a spin, you can do it here: Vulnerability Scanning for your Ruby on Rails app!