Skip to content

Instantly share code, notes, and snippets.

@n-studio
Last active May 29, 2024 09:12
Show Gist options
  • Save n-studio/ac38cf85022dfebcc44b1cc8c6790301 to your computer and use it in GitHub Desktop.
Save n-studio/ac38cf85022dfebcc44b1cc8c6790301 to your computer and use it in GitHub Desktop.
Deploy a web app on a dedicated server with Kamal

Warning

This gist is still a draft. At the moment it is not functional: see basecamp/kamal#257

Motivation

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.

Setup your dedicated server

Setup your domain names

  • Add A/AAAA record in your DNS records for server.mydomain.com, registry.mydomain.com and myapp1.mydomain.com to the IP of your dedicated server

Setup a private container registry

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.

Install SSL certificates for the registry

  • 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;
        ...
      }
    }

Open postgresql access to the docker containers

  • 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

Open redis access to the docker containers

  • Edit /etc/redis/redis.conf to set bind 0.0.0.0 and protected-mode no
  • Allow access for docker containers sudo ufw allow proto tcp from 172.17.0.0/24 to any port 6379

Create a database

  • sudo -u postgres createuser myapp1 --createdb --pwprompt --encrypted
  • Update database.yml, add DB_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 } %>

Install Kamal in your web app

  • 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 choose 7001.
  • Add host_port: 7001 to traefik: in deploy.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.

Create SSL certificates

  • Run sudo certbot certonly --nginx -d myapp1.mydomain.com
  • Edit production.rb in your app to set config.assume_ssl = true

Install reverse proxy with nginx

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

Visit website

  • Open https://app1.mydomain.com in your browser

Run console (for Rails)

  • kamal app exec -i 'bin/rails console'
@n-studio
Copy link
Author

@superp I couldn't find a way to make it work with traefik: false. I'm open to suggestions.

@gregschmit
Copy link

gregschmit commented May 22, 2024

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?

@n-studio
Copy link
Author

@gregschmit You're right! I updated the gist!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment