Multiple apps on one server using kamal
Kamal’s effectiveness is undeniable. Its integration with power-efficient bare-metal servers, such as those available on Hetzner.com, elevates its appeal, allowing for the simultaneous deployment of multiple Rails applications (or applications developed with other frameworks) on a single server. This setup can be further enhanced with the inclusion of additional components like PostgreSQL or Redis.
It’s important to note that while each service in this configuration operates within its own namespace, Traefik does not share this characteristic; only one Traefik service can be included in our setup. In my scenario, I’ve designated the Prograils service, which powers the prograils.com website, as the primary service. Below is the complete configuration for this setup:
service: prograils
image: prograils/prograils
servers:
web:
hosts:
- 46.4.64.80
options:
network: "prograils"
labels:
traefik.http.routers.prograils-web-production.rule: Host(`prograils.com`)
worker:
traefik: false
hosts:
- 46.4.64.80
cmd: bin/sidekiq
options:
network: "prograils"
# Credentials for your image host.
registry:
username: mlitwiniuk
password:
- MRSK_REGISTRY_PASSWORD
env:
clear:
HOST: 'prograils.com'
DB_HOST: 'prograils-db'
DB_PORT: 5432
REDIS_URL: 'redis://prograils-redis:6379/0'
RAILS_LOG_TO_STDOUT: true
RAILS_SERVE_STATIC_FILES: false
secret:
- POSTGRES_PASSWORD
- SECRET_KEY_BASE
- APPSIGNAL_PUSH_API_KEY
- APPSIGNAL_APP_NAME
accessories:
db:
image: postgres:15
port: 5433:5432
host: 46.4.64.80
env:
clear:
POSTGRES_USER: 'prograils'
POSTGRES_DB: 'prograils_production'
secret:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
options:
network: "prograils"
redis:
image: redis:7.0
host: 46.4.64.80
port: 6380:6379
directories:
- data:/data
options:
network: "prograils"
traefik:
options:
network: "prograils"
healthcheck:
max_attempts: 10
There are few non-standard configuration options in here:
- Web server has additional label:
traefik.http.routers.prograils-web-production.rule: Host(`prograils.com`)
- it’s to inform traefik where to direct requests for prograils.com domain. Also note the service name suffix (-production
) - I’m deploying all my apps with destination parameter - Postgres and redis use standard port internally, but custom one externally - it’s for me to be able to connect to them to ie. make a backup. And having non-custom port ensures me, that there won’t be any conflicts between same services:
port: 5433:5432
- All services, accessories and traefik have an additional option with network defined - this is for those containers to be in the same “virtual” docker network and one could access the other via the name, ie.
prograils-db
and not the IP address (which is subject of change upon machine restart):options: network: "prograils"
- Kamal won’t create docker network during the deploy process, but actually it’s pretty easy to automate it through hooks, ie. like the one by @tikal here: Docker networking and accessories · Issue #41 · basecamp/kamal · GitHub - put it in
.kamal/hooks/pre-deploy
and don’t forget to make it executable.#!/usr/bin/env bash REMOTE_HOST="user@prograils.com" NETWORK_NAME="prograils" # SSH into the remote host and execute Docker commands ssh $REMOTE_HOST << EOF # Check if the Docker network already exists if ! docker network inspect "$NETWORK_NAME" &>/dev/null; then # If it doesn't exist, create it docker network create "$NETWORK_NAME" echo "Created Docker network: $NETWORK_NAME" else echo "Docker network $NETWORK_NAME already exists, skipping creation." fi EOF
Config of the second app (it’s staging version of humadroid.io ) is very similar:
service: humadroid
image: mlitwiniuk/humadroid
servers:
web:
hosts:
- 46.4.64.80
options:
network: "humadroid"
labels:
traefik.http.routers.humadroid-web-production.rule: HostRegexp(`humadroid.dev`, `{subdomain:[a-z0-9]+}.humadroid.dev`)
worker:
traefik: false
hosts:
- 46.4.64.80
cmd: bin/sidekiq -C config/sidekiq.yml
options:
network: "humadroid"
registry:
username: mlitwiniuk
password:
- MRSK_REGISTRY_PASSWORD
env:
clear:
HOST: 'humadroid.dev'
RAILS_LOG_TO_STDOUT: true
RAILS_SERVE_STATIC_FILES: true
DB_HOST: 'humadroid-db'
DB_PORT: 5432
REDIS_URL: 'redis://humadroid-redis:6379/0'
secret:
- POSTGRES_PASSWORD
- SECRET_KEY_BASE
accessories:
db:
image: postgres:15
port: 5434:5432
host: 46.4.64.80
env:
clear:
POSTGRES_USER: 'humadroid'
POSTGRES_DB: 'humadroid_production'
secret:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
options:
network: "humadroid"
redis:
image: redis:7.0
host: 46.4.64.80
port: 6381:6379
directories:
- data:/data
options:
network: "humadroid"
healthcheck:
max_attempts: 10
Note that there is no explicit Traefik configuration mentioned here. Traefik automatically identifies how to do routing through the labels assigned to the web server. All servers and supplementary services are incorporated into a newly created, separate Docker network, with accessory services configured to expose distinct ports. Essentially, that’s all there is to it. The only remaining step is to link Traefik to this new network (named “humadroid” in this case). This connection is established through a post-deploy hook, which is similar in nature to a pre-deploy hook. This hook should be placed in .kamal/hooks/post-deploy and must be made executable.
#!/usr/bin/env bash
REMOTE_HOST="-p 2122 prograils@prograils.com"
NETWORK_NAME="humadroid"
# SSH into the remote host and execute Docker commands
ssh $REMOTE_HOST << EOF
# Check if the Docker network already exists
if ! docker network inspect "$NETWORK_NAME" 2>/dev/null | grep traefik; then
# If it doesn't exist, create it
docker network connect "$NETWORK_NAME" traefik
echo "Connected traefik to docker network: $NETWORK_NAME"
else
echo "Traefik already connected to docker network $NETWORK_NAME ."
fi
EOF
And voila, both apps should be working from the same machine, all contenerized and deplyed thanks to kamal.