UPDATE: Kamal 2 is out and this is much easier now. See my article on Deploying a Kirby CMS site with Kamal 2. It doesn't cover everything needed for Laravel, but it gets close.
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.
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.