No per-service fees - one plan, unlimited appsFree tier available - start building today15% off your workspace - subscribe to our blogNo per-service fees - one plan, unlimited appsFree tier available - start building today15% off your workspace - subscribe to our blog
Miget x AIPlansEnterpriseCompareBlogDashboard
Start for Free
Blog/Engineering/kamal/
·

Deploy Rails With Kamal to a Managed Cluster

Kamal is the default deploy tool in new Rails apps, and its pitch is simple: SSH into a server, run Docker, get zero-downtime deploys. The part nobody loves is the server. You own the kernel, the security updates, the disk that fills up at 3am.

This is a hands-on walkthrough of deploying a plain Rails app with Kamal to Miget. You write the same deploy.yml, you run the same kamal deploy, but the host Kamal SSHes into is a managed cluster. Kamal thinks it is talking to Docker; on our side that Docker API is translated to Kubernetes. If you want the story of how that translation works, it is in Kamal Deploy: VPS vs PaaS. This post is just the steps, and everything below is from a real deploy of the stock Rails Docker template.

What you need

  • A Miget account.
  • A Rails app with a Dockerfile. Rails 7.1+ generates one for you (rails new already did it).
  • Kamal 2 installed locally: gem install kamal.
  • Docker with buildx. Docker Desktop has it; so does a plain Docker Engine.

If you are on an Apple Silicon Mac, read the arch note near the end before your first deploy. It is the one thing that will bite you.

Step 1: Create the app on Miget

In the dashboard, create a new App and choose Kamal as the deployment method. Paste your SSH public key (~/.ssh/id_ed25519.pub or whichever you use). Miget provisions the Kamal infrastructure for you and hands back four things:

  • An SSH endpoint: a host and a port. This is what Kamal connects to.
  • A registry URL, registry.<region>.miget.io, and a username (your namespace).
  • A registry password.
  • Your app's full name. Miget appends a short suffix, so my-rails-app becomes something like my-rails-app-b4mrd. Use that exact name from here on.

The user Kamal logs in as is always deploy.

Step 2: Write deploy.yml

Kamal keeps its config in config/deploy.yml. Here is the whole file for a Rails app on Miget. Replace the placeholders with the values from step 1.

service: my-rails-app-b4mrd
image: your-namespace/my-rails-app-b4mrd

servers:
  web:
    - your-ssh-host

proxy:
  host: my-rails-app-b4mrd.eu-east-1.migetapp.com
  ssl: true
  app_port: 5000

registry:
  server: registry.eu-east-1.miget.io
  username: your-namespace
  password:
    - KAMAL_REGISTRY_PASSWORD

builder:
  arch: amd64

ssh:
  user: deploy
  port: your-ssh-port

env:
  clear:
    PORT: "5000"
    RAILS_LOG_TO_STDOUT: "1"
    RAILS_SERVE_STATIC_FILES: "1"
  secret:
    - SECRET_KEY_BASE

A few of these lines carry the whole deploy, so they are worth understanding rather than copying blind.

proxy.app_port and PORT are both 5000 on purpose. Miget serves your app's public URL from port 5000, always. Rails reads the PORT variable and binds to it, so setting PORT=5000 is what makes the two agree. Get this wrong and the app builds, boots, and then answers nothing, because the proxy is knocking on a port Rails never opened.

proxy.host is your app's default domain. ssl: true asks for HTTPS. You do not manage certificates here; the platform terminates TLS for *.migetapp.com and forwards the request in. Rails' own config.force_ssl = true (on by default in the generated production.rb) works fine behind that proxy and does not cause a redirect loop, because the proxy passes the X-Forwarded-Proto header Rails trusts.

registry.password names an environment variable, it does not hold the password. Same for SECRET_KEY_BASE under env.secret. Both come from the secrets file next.

Step 3: Put the secrets in .kamal/secrets

Kamal reads secret values from .kamal/secrets. Create it with your registry password from step 1 and a freshly generated Rails secret:

KAMAL_REGISTRY_PASSWORD=paste-the-registry-password-here
SECRET_KEY_BASE=paste-the-output-of-bin-rails-secret

Generate the second value with bin/rails secret. Add .kamal/secrets to .gitignore if it is not already there; it holds real credentials and must never be committed.

Step 4: Deploy

From the app directory:

kamal deploy

The first run does everything at once: it builds your image, pushes it to the Miget registry, then over SSH it pulls the image on the cluster, starts the container, and points the proxy at it. You will see Kamal wait for the container to pass its health check, then release the deploy lock. On the real run this took a few minutes, most of it the image build.

When it finishes, your app is live at the proxy.host domain.

curl -sI https://my-rails-app-b4mrd.eu-east-1.migetapp.com/up

Rails apps expose /up as a health endpoint that returns 200 only if the app booted without exceptions. A 200 there means the whole chain worked: build, push, translation to Kubernetes, boot, routing, TLS.

Every subsequent deploy is the same one command, and Kamal does a zero-downtime rolling swap.

The arch gotcha

If you build on an Apple Silicon Mac, your default image is arm64. The Miget cluster runs on amd64. Ship an arm64 image to it and the container will not start, with an error that does not obviously point at architecture.

That is what builder: arch: amd64 in the config is for. It tells Kamal to build for amd64 regardless of your laptop. buildx does the cross-build through emulation, which is slower on the first run but produces an image the cluster can actually run. If you build on an amd64 machine or in CI, you can drop the line.

About the database

The stock Rails template uses SQLite, and that is what this walkthrough deployed. It works, but the SQLite file lives inside the container, so it is recreated on every deploy. That is fine for a demo and wrong for anything real.

For a real app, add a Postgres addon on Miget and point DATABASE_URL at it, or attach a persistent volume for the SQLite file. Either way your data then outlives a deploy. That is a separate step and not shown here.

When this is for you, and when it is not

Kamal exists partly to avoid PaaS markup and own your servers. If owning the box is the point for you, keep doing that; Kamal is great at it and Miget is not what you want.

This setup is for the other Rails developer: the one who likes the Kamal workflow, the single deploy.yml, the kamal deploy muscle memory, but does not want to be the person who patches the kernel and gets paged when the disk fills. You keep the workflow and hand off the box. That is the whole trade.

If that is you, the fastest way to feel it is to run the four steps above against your own app. It is the same Kamal you already know, pointed somewhere quieter.

Deploy your Rails app

Deploy Rails With Kamal, No Server to Babysit