This gist is still a draft. At the moment it is not functional: see basecamp/kamal#257
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-install-and-use-postgresql-on-ubuntu-22-04
- https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-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
- Add A/AAAA record in your DNS records for
server.mydomain.com
,registry.mydomain.com
andmyapp1.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.
- Run
sudo certbot certonly --nginx -d registry.mydomain.com
- Add to
/etc/nginx/sites-enabled/registry
:server { ... listen 443 ssl; listen [::]:443 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; ... } }
- Add
host all all 172.17.0.0/24 scram-sha-256
to/etc/postgresql/16/main/pg_hba.conf
- Add
listen_addresses = '*'
to/etc/postgresql/16/main/postgresql.conf
- Edit
/etc/redis/redis.conf
to setbind 0.0.0.0
andprotected-mode no
- Allow access for docker containers
sudo ufw allow proto tcp from 172.17.0.0/24 to any port 6379
sudo -u postgres createuser myapp1 --createdb --pwprompt --encrypted
- Update
database.yml
, addDB_HOST
,DB_USERNAME
,DB_PASSWORD
,DB_PORT
to your.env.erb
file.production: <<: *default host: <%= ENV["DB_HOST"] %> database: myapp1_production username: <%= ENV["DB_USERNAME"] %> password: <%= ENV["DB_PASSWORD"] %> port: <%= ENV.fetch("DB_PORT") { 5432 } %>
- Run Docker locally
- Follow: [https://kamal-deploy.org/docs/installation]
- Add database/redis credentials
- Run
bin/kamal env push
- Run
bin/kamal deploy
- Run
bin/kamal traefik reboot
after any change in the traefik configuration - Choose a unique port for your service, use
sudo lsof -i -P -n | grep LISTEN
to check which ports are already in use. For this guide I'll choose7001
. - Add
host_port: 7001
totraefik:
indeploy.yml
The deploy.yml
file should look like this:
# Name of your application. Used to uniquely configure containers.
service: my-app-1
# Name of the container image.
image: n-studio/my-app-1
# Deploy to these servers.
servers:
- server.mydomain.com
# Credentials for your image host.
registry:
# Specify the registry server, if you're not using Docker Hub
server: registry.mydomain.com
# Always use an access token rather than real password when possible.
username:
- KAMAL_REGISTRY_USERNAME
password:
- KAMAL_REGISTRY_PASSWORD
# Inject ENV variables into containers (secrets come from .env).
# Remember to run `kamal env push` after making changes!
env:
clear:
DB_HOST: 172.17.0.1
DB_PORT: 5432
REDIS_URL: redis://172.17.0.1:6379/1 # give a unique db number for each app
secret:
- RAILS_MASTER_KEY
- DB_USERNAME
- DB_PASSWORD
# Use a different ssh user than root
ssh:
user: ubuntu
# Configure custom arguments for Traefik
traefik:
name: traefik-my-app-1
host_port: 7001
args:
entrypoints.my-app-1-web.address: ':7001'
Note: This uses the branch n-studio/kamal:custom-traefik-name to allow a custom name for the traefik container.
- Run
sudo certbot certonly --nginx -d myapp1.mydomain.com
- Edit
production.rb
in your app to setconfig.assume_ssl = true
server {
server_name myapp1.mydomain.com;
listen 80;
listen [::]:80;
## redirect http to https ##
rewrite ^ https://$server_name$request_uri? permanent;
}
server {
server_name myapp1.mydomain.com;
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/letsencrypt/live/myapp1.mydomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp1.mydomain.com/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://localhost:7001;
proxy_set_header Host $http_host; # required for docker client's sake
proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Host $host;
proxy_read_timeout 900;
}
}
- Run
sudo service nginx restart
- Open
https://app1.mydomain.com
in your browser
kamal app exec -i 'bin/rails console'
Out of curiosity, if each app gets its own traefik instance which use a different host port, why the need for the routing (
traefik.http.routers.my-app-1-web.entrypoints: my-app-1-web
)? I would imagine that when Kamal deploys, it tells the traefik instance for app1 to bind to the exposed ports on app1, and same for app2.In other words, I've seen the usage of traefik router when you have 1 traefik instance, and it needs to conditionally route to app1 or app2, but if you have 2 traefik instances, wouldn't they just know from how Kamal deploys them to route everything to/from the container they deploy with?