Joshua Pangborn

August 21, 2024

Enabling SSL on Traefik using Kamal

I previously posted on Deploying a Laravel App with Kamal using the ServerSideUP PHP Docker image. At the end of the article,  we were deploying a Laravel application to a single server, but ready for deployment to multiple servers behind a load balancer. But that is often overkill for small applications or websites. If that is the case, some minor changes to the Kamal ‘deploy.yml’ file will enable Traefik to generate an SSL certificate from Let’s Encrypt.

The ‘deploy.yml’ file that we ended with was:

# Name of your application. Used to uniquely configure containers.
service: fold

# Name of the container image.
image: jpangborn/fold

# Deploy to these servers.
servers:
  - xx.xx.xx.xx

# Credentials for your image host.
registry:
  # Always use an access token rather than real password when possible.
  username:
    - KAMAL_REGISTRY_USERNAME
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  clear:

  secret:
    - LARAVEL_ENV_ENCRYPTION_KEY

labels:
  traefik.http.routers.fold-web.rule: Host(`app.domain.tld`)
  traefik.http.services.fold-web.loadbalancer.server.port: 8080

# Use accessory services (secrets come from .env).
accessories:
  db:
    image: mysql:8.0
    host: xx.xx.xx.xx
    port: 3306
    env:
      clear:
        MYSQL_ROOT_HOST: '%'
      secret:
        - MYSQL_ROOT_PASSWORD
    files:
      - database/init.sql:/docker-entrypoint-initdb.d/setup.sql
    directories:
      - data:/var/lib/mysql

# Configure a custom healthcheck (default is /up on port 3000)
healthcheck:
  path: /up
  port: 8080
  max_attempts: 10
  interval: 20s

We’ll need to make some changes to the ‘deploy.yml’ to instruct Traefik to generate an SSL certificate via Let’s Encrypt and to make sure that http requests are redirected to https.

First, we will add to the application container labels to instruct the container to use the https entry point and to use Let’s Encrypt as the certificate resolver. The labels section should look like this after the changes: (Be sure to change ‘fold-web’ to the name of your service with ‘-web’.)

labels:
  traefik.http.routers.fold-web.rule: Host(`app.domain.tld`)
  traefik.http.routers.fold-web.entrypoints: https
  traefik.http.routers.fold-web.tls.certresolver: letsencrypt
  traefik.http.services.fold-web.loadbalancer.server.port: 8080

Next we configure Traefik to have entry points for http and https, publish port 443, configure the redirections, and to setup the certificate resolver. The following section should be added before the ‘healthcheck’ section of the ‘deploy.yml’.

# Configure custom arguments for Traefik
traefik:
  args:
    entryPoints.http.address: ":80"
    entryPoints.https.address: ":443"
    entryPoints.http.http.redirections.entryPoint.to: https
    entryPoints.http.http.redirections.entryPoint.scheme: https
    entryPoints.http.http.redirections.entrypoint.permanent: true
    certificatesResolvers.letsencrypt.acme.email: "email@domain.tld"
    certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
    certificatesResolvers.letsencrypt.acme.httpchallenge: true
    certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: http
  options:
    publish:
      - 443:443
    volume:
      - "/letsencrypt/acme.json:/letsencrypt/acme.json"

Some notes about the above:
  • The first two entryPoints map the ports to the named entryPoints (http and https).
  • The next three entryPoints entries configure the redirections from http to https.
  • The four certificateResolvers entries configure Traefik to use Let’s Encrypt to generate the certificate.
  • The publish option instructs Traefik to publish the 443 port. (Traefik publishes port 80 by default.)
  • The volume option instructs Traefik to map a file on the server to a location in the Traefik container to maintain the certificate between Treafik restarts.

That completes the changes to ‘deploy.yml’ needed to enable SSL. There is one more thing needed. We need to create the acme.json file on the server to use as the volume for Traefik. This can be easily done with a Kamal hook. We will use the ‘docker-setup’ hook to create the acme.json file when the server is initially provision. The contents of the /.kamal/hooks/docker-setup file should be:

#!/bin/sh

echo "Creating Let's Encrypt File"
for host in $(echo $KAMAL_HOSTS | sed "s/,/ /g")
do
    ssh root@$host 'mkdir -p /letsencrypt && touch /letsencrypt/acme.json && chmod 600 /letsencrypt/acme.json'
done

For reference, the complete ‘deploy.yml’ file should look like:

# Name of your application. Used to uniquely configure containers.
service: fold

# Name of the container image.
image: jpangborn/fold

# Deploy to these servers.
servers:
  - xx.xx.xx.xx

# Credentials for your image host.
registry:
  # Always use an access token rather than real password when possible.
  username:
    - KAMAL_REGISTRY_USERNAME
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  clear:

  secret:
    - LARAVEL_ENV_ENCRYPTION_KEY

labels:
  traefik.http.routers.fold-web.rule: Host(`app.domain.tld`)
  traefik.http.routers.fold-web.entrypoints: https
  traefik.http.routers.fold-web.tls.certresolver: letsencrypt
  traefik.http.services.fold-web.loadbalancer.server.port: 8080

# Use accessory services (secrets come from .env).
accessories:
  db:
    image: mysql:8.0
    host: xx.xx.xx.xx
    port: 3306
    env:
      clear:
        MYSQL_ROOT_HOST: '%'
      secret:
        - MYSQL_ROOT_PASSWORD
    files:
      - database/init.sql:/docker-entrypoint-initdb.d/setup.sql
    directories:
      - data:/var/lib/mysql

# Configure custom arguments for Traefik
traefik:
  args:
    entryPoints.http.address: ":80"
    entryPoints.https.address: ":443"
    entryPoints.http.http.redirections.entryPoint.to: https
    entryPoints.http.http.redirections.entryPoint.scheme: https
    entryPoints.http.http.redirections.entrypoint.permanent: true
    certificatesResolvers.letsencrypt.acme.email: "email@domain.tld"
    certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
    certificatesResolvers.letsencrypt.acme.httpchallenge: true
    certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: http
  options:
    publish:
      - 443:443
    volume:
      - "/letsencrypt/acme.json:/letsencrypt/acme.json"

# Configure a custom healthcheck (default is /up on port 3000)
healthcheck:
  path: /up
  port: 8080
  max_attempts: 10
  interval: 20s

This approach will let you run a small Laravel application on a single server with a application container, a database container, and a Traefik container as a reverse proxy directing traffic and providing SSL. You will get the easy provisioning of the server and the zero-downtime deployment that Kamal provides.