This guide uses Kamal 2.5.3
Kamal was designed with 1 service = 1 droplet/VPS in mind.
But I'm cheap and I want to be able to deploy multiple demo/poc apps apps on my $20/month dedicated server.
What the hell, I'll even host my private container registry on it.
- https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-22-04
- https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-22-04
- https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-22-04
- https://www.digitalocean.com/community/tutorials/how-to-set-up-minio-object-storage-server-in-standalone-mode-on-ubuntu-20-04
- https://min.io/docs/minio/linux/integrations/generate-lets-encrypt-certificate-using-certbot-for-minio.html
- Add A/AAAA record in your DNS records for
server.mydomain.com
,registry.mydomain.com
,myapp1.mydomain.com
andmyapp2.mydomain.com
to the IP of your dedicated server
Note 1: This tutorial says you need a host server and a client server. In our case, we will use only one server to be both client and server.
Note 2: The port 5000
is already used by datadog-agent by default, so I prefer using 7000:5000
instead.
Docker will have full control of port 80
, so we need to choose another port. I'll pick 8080
.
- Run
sudo certbot certonly --nginx -d registry.mydomain.com
- Add to
/etc/nginx/sites-enabled/registry
:server { ... listen 8080 ssl; listen [::]:8080 ssl; ssl_certificate /etc/letsencrypt/live/registry.domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/registry.domain.com/privkey.pem; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; location / { ... proxy_pass http://localhost:7000; ... } }
- Run Docker locally
- Follow: [https://kamal-deploy.org/docs/installation]
- Add registry/database credentials with EDITOR=vi bin/rails credentials:edit
kamal_registry:
username: ubuntu
password: #<REGISTRY_PASSWORD>
production:
db_username: #<POSTGRES_USER>
db_password: #<POSTGRES_PASSWORD>
- Update your
database.yml
file
production:
primary: &primary_production
<<: *default
host: <%= ENV["DB_HOST"] %>
database: <%= ENV.fetch("POSTGRES_DB", "myapp1_production") %>
username: <%= ENV["POSTGRES_USER"] %>
password: <%= ENV["POSTGRES_PASSWORD"] %>
cache:
<<: *primary_production
database: <%= ENV.fetch("POSTGRES_DB", "myapp1_production") %>_cache
migrations_paths: db/cache_migrate
queue:
<<: *primary_production
database: <%= ENV.fetch("POSTGRES_DB", "myapp1_production") %>_queue
migrations_paths: db/queue_migrate
cable:
<<: *primary_production
database: <%= ENV.fetch("POSTGRES_DB", "myapp1_production") %>_cable
migrations_paths: db/cable_migrate
- Update you
.kamal/secrets
RAILS_MASTER_KEY=$(cat config/master.key)
KAMAL_REGISTRY_USERNAME=$(bin/rails runner "print Rails.application.credentials.kamal_registry.username")
KAMAL_REGISTRY_PASSWORD=$(bin/rails runner "print Rails.application.credentials.kamal_registry.password")
POSTGRES_USER=$(bin/rails runner "print Rails.application.credentials.production.db_username")
POSTGRES_PASSWORD=$(bin/rails runner "print Rails.application.credentials.production.db_password")
- Create a file
config/init.sql
CREATE DATABASE myapp1_production;
CREATE DATABASE myapp1_production_cache;
CREATE DATABASE myapp1_production_queue;
CREATE DATABASE myapp1_production_cable;
- Edit
production.rb
in your app to enableassume_ssl
and disableforce_ssl
config.assume_ssl = true
config.force_ssl = false
- Update
deploy.yml
The deploy.yml
file should look like this:
# Name of your application. Used to uniquely configure containers.
service: myapp1
# Name of the container image.
image: mydomain/myapp1
# Deploy to these servers.
servers:
web:
- server.mydomain.com
deploy_timeout: 40
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
proxy:
ssl: true
host: myapp1.mydomain.com
# Credentials for your image host.
registry:
server: registry.mydomain.com:8080
username:
- KAMAL_REGISTRY_USERNAME
password:
- KAMAL_REGISTRY_PASSWORD
env:
secret:
- RAILS_MASTER_KEY
- POSTGRES_USER
- POSTGRES_PASSWORD
clear:
DB_HOST: myapp1-db
POSTGRES_DB: myapp1_production
# Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
# When you start using multiple servers, you should split out job processing to a dedicated machine.
SOLID_QUEUE_IN_PUMA: true
# Serve static files from Rails
RAILS_SERVE_STATIC_FILES: 1
# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
aliases:
console: app exec --interactive --reuse "bin/rails console"
shell: app exec --interactive --reuse "bash"
logs: app logs -f
dbc: app exec --interactive --reuse "bin/rails dbconsole"
# Use a persistent storage volume for sqlite database files and local Active Storage files.
# Recommended to change this to a mounted volume path that is backed up off server.
volumes:
- "myapp1_storage:/rails/storage"
# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
asset_path: /rails/public/assets
# Configure the image builder.
builder:
arch: amd64
# Use a different ssh user than root
ssh:
user: ubuntu
# Use accessory services (secrets come from .kamal/secrets).
accessories:
db:
image: postgres:17
host: server.mydomain.com
port: 127.0.0.1:8001:5432 # This port must be unique for each app. Here I chose 8001
env:
clear:
POSTGRES_DB: myapp1_production
POSTGRES_HOST_AUTH_METHOD: trust
secret:
- POSTGRES_USER
- POSTGRES_PASSWORD
files:
- config/init.sql:/docker-entrypoint-initdb.d/setup.sql
directories:
- data:/var/lib/postgresql/data
- Run
bin/kamal setup
- Repeat the same for your other app with a different port for postgresql (
8002
)
- Open
https://app1.mydomain.com
in your browser
bin/kamal console
There is an issue with Tailwindcss 4. Revert back to Tailwindcss 3 until the issue is fixed.
In order to run min.io at startup, run: sudo systemctl enable minio.service
If you want to use other ports than the default ones (9000,9001), edit the file: /etc/default/minio
and write: MINIO_OPTS="--certs-dir /home/sammy/.minio/certs --address :<NEW API PORT> --console-address :<NEW CONSOLE PORT>"
docker ps
# find the container ID of kamal-proxy (eg. ae6df7151e5e)
docker stop ae6df7151e5e
sudo certbot certonly --standalone -d registry.mydomain.com
docker start ae6df7151e5e
sudo systemctl restart nginx
How can we use this gist with the original Kamal and
traefik: false
configuration?