Using Kamal to host multiple Apps on a single server

Use the Kamal deployment tool to host multiple Applications on a single server instance

In this post I'll outline a way you can host multiple Ruby on Rails applications on a single server. This is one way to achieve that desired goal, there may be better ways.

I understand that many developers may not carry-over to the dark side of DevOps things. Comprehending all that Kamal is can be a daunting task. You should have some knowledge of Traefik and Docker.

In this example, we'll have 2 Rails Applications, luckily Rails 7.1.0.beta1 was recently released with all the new goodness. Both Applications in this example are the exact same, SQLite3, esbuild, vanilla out of the box configuration. I'm using Kamal version 1.0.0. The end goal of each was a green response at the route /up.

Want to run your own Docker registry instance for Kamal? Check out my post about it.

Here's a bit of a visual of how things are setup on the single instance:

Kamal commands our server instance through the deploy.yml file we create. If this instance is located on the cloud, you'll have the SSH key to login local to your machine, Kamal simply uses that to login as root and run commands for you.

Kamals kamal server command will bootstrap the server with what is necessary, and at the base of it all is: docker and curl, the Traefik container being the reverse proxy we need to route the traffic in to each container.

This setup is a bit odd in that the first project you'll run the kamal init command in will have some of the traefik configuration we need in it for SSL. If you're hosting a bunch of small projects and don't care if there is overlap into another project then you can continue.

After you've setup the server instance with docker you can login to it and create a directory /letsencrypt/acme.json and file with the contents {}, you can then chmod it 600.

Going to use Hetzner Cloud? Get €⁠20 in credits when you use my link to sign up.

Site 1 Configuration

# deploy.yml

service: site1

image: my-registry/site1

# Deploy to these servers.
servers:
  web:
    hosts:
      - yourip
    options:
      "add-host": host.docker.internal:host-gateway
    labels:
      traefik.http.routers.rails_recipes.entrypoints: websecure
      traefik.http.routers.rails_recipes.rule: Host(`rails.recipes`)
      traefik.http.routers.rails_recipes.tls.certresolver: letsencrypt

# Credentials for your image host.
registry:
  # Specify the registry server, if you're not using Docker Hub
  server: registry.digitalocean.com
  username: deploy

  # Always use an access token rather than real password when possible.
  password:
    - KAMAL_REGISTRY_PASSWORD

traefik:
  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
    certificatesResolvers.letsencrypt.acme.email: "you@youremail"
    certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
    certificatesResolvers.letsencrypt.acme.httpchallenge: true
    certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web

After you have this in your deploy.yml, save it. Be sure your env variables are up to date with kamal env push. You'll notice the naming conventions Kamal uses for your services. This helps in the flexibility of being able to deploy multiple applications.

Run kamal deploy to push this application up to the server.

If you haven't configured traefik yet, you'll what to likely run kamal traefik restart or kamal traefik reboot if its not picking up your changes. Give LetsEncrypt a little time to catch up with the certificate.

Site 2 Configuration

# deploy.yml

service: site2

image: my-registry/site2

servers:
  web:
    hosts:
      - yourip
    options:
      "add-host": host.docker.internal:host-gateway
    labels:
      traefik.http.routers.site2-web.entrypoints: websecure
      traefik.http.routers.site2-web.rule: Host(`changelog.lol`)
      traefik.http.routers.site2-web.tls.certresolver: letsencrypt

registry:
  # Specify the registry server, if you're not using Docker Hub
  server: registry.digitalocean.com
  username: deploy

  # Always use an access token rather than real password when possible.
  password:
    - KAMAL_REGISTRY_PASSWORD

The same goes for this site, be sure you have your env variables up to date and push them. They'll end up in .kamal/env/roles directory on your server instance. You can SSH and check them out there, too.

Now all you need to do is run the kamal deploy command from your other application directory and Kamal will deploy this app for you and start the container. Since we already have Traefik running it will get notified of the changes via the labels we're pushing. You can also watch this with the docker logs command from the server.

I hope this helps you with general concepts or debugging getting your application deployed.

This is dedicated to someone who kept asking me how to do this.

Well, you've made it this far. If you happen to be a developer, check out my in-app feedback tool that I'm hacking on.