Secure Your Kamal App Deployments With Let's Encrypt

Looking how to easily set up HTTPS on a web application deployed with Kamal? All it takes are a few updates to your Kamal configuration.

Deploying web applications using Kamal is pretty easy once you’ve chosen your preferred infrastructure and packaged your app into a Docker image. All it takes is a single command, and your application will be up and running on your chosen servers with little effort. Unfortunately, Kamal still doesn’t handle everything you need to set up a modern web application.

One of the glaring omissions that you need to deal with on your own is setting up HTTPS for secure communication between the user and your application. When you deploy your application using Kamal, you can only access it through non-secure HTTP connections, which is a no-no in this day and age.

In a previous article, I explained how to set up HTTPS on a load balancer for Kamal-deployed applications. However, for many web applications that don’t handle a lot of traffic and don’t require many resources, you’ll likely use a single server to run your web application. So, how can you get secure connectivity with your applications when deploying them on a single server with Kamal?

Downsides of Using Services Like Cloudflare

While Kamal currently doesn’t have specific configuration settings to get your app working with HTTPS, there are ways you can get your Kamal-deployed applications to use secure HTTPS connections.

A common way to do this is using a service like Cloudflare to proxy your domain through a secure connection. You can use their DNS service to point to your server, first going through Cloudflare via HTTPS and then having Cloudflare pass the connection along to your server.

However, using Cloudflare does have drawbacks:

Cloudflare is a wonderful service that I frequently use, and it works well for most scenarios. But if you’re looking for a more secure way to ensure your browser is encrypting connections between users and your app, you’ll eventually need to get an SSL certificate to make that happen.

Using Kamal to Get Free SSL Certificates With Let’s Encrypt

These days, getting an SSL certificate is dead simple, thanks to Let’s Encrypt. With Let’s Encrypt, you can generate an SSL certificate for your domain without paying a single cent and have the process entirely automated, so you don’t have to worry about renewals.

Kamal doesn’t have a direct setting that let’s you do this, but that doesn’t mean you can’t generate an SSL certificate from Let’s Encrypt for your web apps. With a few updates to an existing Kamal configuration, you can have automated SSL certificate generation and renewals in a snap. All the heavy lifting for this process is handled by Traefik, the reverse proxy service Kamal uses for routing connections to the app running in your Docker container.

One of the cool features that Traefik provides for running apps through Docker containers is using labels to configure its behavior. When running a Docker container, you can specify a label as a key-value pair with metadata. Traefik watches those container-level labels to configure its routing settings, like forwarding requests based on the hostname, specifying custom ports, and more.

Kamal sets up a few default labels on deployed application containers, but we can also modify these labels along with Traefik’s configuration for added functionality. To set up SSL connectivity, Traefik already has built-in support to automatically generate Let’s Encrypt certificates and set up our web application to use HTTPS through labels.

Traefik itself runs as a Docker container when deploying with Kamal, and we can manage how it behaves directly in the Kamal configuration file for our applications. Let’s update an existing Kamal configuration to generate an SSL certificate and automatically set up HTTPS connectivity to see how it works.

If you’re new to Kamal and want to learn more about how to deploy your web applications, read the article “Deploy Your Rails Applications the Easy Way With Kamal” on this website or watch “Rails Deployments Made Easy with Terraform and Kamal” on YouTube for an introduction to Kamal.

Configuring Traefik to Generate Let’s Encrypt SSL Certificates

For this article, I’ll use a web application that I’ve already deployed called Airport Gap as an example of how to set up HTTPS with Kamal. The Kamal configuration used for deploying this app works great, and I can access the application immediately after setting up a server. By default, it only works through non-secure HTTP connections. If I attempt to visit the application using HTTPS, it won’t work.

Let’s fix this by updating how Traefik works in our Kamal deployments. Kamal allows you to modify Traefik’s configuration through the traefik settings placed at the root level of your existing Kamal configuration, typically located in the config/deploy.yml file in your web application directory. Under traefik, you can change which Docker image Traefik uses, set environment variables, or even deactivate it altogether.

For our example, we’ll need to change how Kamal launches the Docker container and pass additional arguments to the Traefik container. We can do this using the options and args sections under the top-level key:

traefik:
  options:
    # Options to pass to `docker run` when starting the Traefik container.
  args:
    # Additional arguments to pass to Traefik.

Updating how Kamal runs the Traefik Docker container

First, we’ll set up the options section to configure additional arguments Kamal passes along when executing docker run during deploys. These settings will allow us to start the Traefik proxy with a few required settings. We’ll need to change two things with how Kamal starts up the service:

  • We need to map port 443 on the server to port 443 on Traefik for secure HTTPS connectivity.
  • We also need to mount a file from the host server to the container to store the SSL certificate data from Let’s Encrypt.

We can achieve these two steps by adding the following configuration settings under options:

traefik:
  options:
    publish:
      - "443:443"
    volume:
      - "/etc/letsencrypt/acme.json:/etc/letsencrypt/acme.json"
  args:
    # Additional arguments to pass to Traefik. We'll handle this later.

The publish setting lets us define which ports to forward from the host to the proxy service in Docker. We can map multiple ports here, but we only need to map port 443 from the server to port 443 on the container, so any time a user makes a secure HTTPS request to our application, our server can send it through Traefik.

The volume setting is needed to mount a file from the host server to the container. Docker containers are ephemeral, so all its data will disappear once a container is shut down. Using a volume avoids that issue by mapping files between the Docker container and the server’s file system so the data remains on the host. We’ll need this to store the data that Let’s Encrypt uses to set up the SSL certificate for our application and let Traefik know about an existing certificate for the domain.

We can also map multiple files or directories under volume, but we’ll only need to mount one file. Let’s Encrypt uses a JSON file to store its configuration when generating a server. The file we’re mounting in this example is located in /etc/letsencrypt/acme.json on the host server, and we’re placing it inside the same location inside the Traefik Docker container. The location and file name don’t matter as long as it’s a valid path to a file that both the host and Docker container can access.

Setting up configuration for Let’s Encrypt on the host server

Before going any further, the acme.json file that Let’s Encrypt uses must exist on the server first at the exact location specified under volume in your Kamal configuration before attempting to mount it in the Traefik container. This step doesn’t happen automatically, so we’ll need to do this manually or using a configuration management tool like Ansible.

Since this is a one-time setup, the easiest way to handle this is logging in to your host server through SSH, creating the directory specified in the volume section for your Kamal configuration, and creating a blank acme.json file.

It’s also essential to set the correct permissions for the acme.json file. Let’s Encrypt needs the file to have read and write permissions for the file owner and no permissions for the group or other users. Otherwise, the SSL certificate generation process will fail if the permissions are too open or restrictive.

You can run the following commands on the server to handle these steps if logged in to the server through SSH:

mkdir -p /etc/letsencrypt
touch /etc/letsencrypt/acme.json
chmod 600 /etc/letsencrypt/acme.json

Passing additional arguments to Traefik

Now that we’ve set up how Kamal runs the Traefik Docker container, we’ll need to configure Traefik itself using the args setting under the root-level traefik key.

In this section, we’ll configure Traefik to accept and request all requests to HTTPS and configure the built-in Let’s Encrypt configuration to generate an SSL certificate automatically. There are quite a few arguments to pass along, so I’ll show the entire traefik configuration and explain each argument below:

traefik:
  options:
    publish:
      - "443:443"
    volume:
      - "/etc/letsencrypt/acme.json:/etc/letsencrypt/acme.json"
  args:
    entryPoints.web.address: ":80"
    entryPoints.websecure.address: ":443"
    entryPoints.web.http.redirections.entryPoint.to: "websecure"
    entryPoints.web.http.redirections.entryPoint.scheme: "https"
    entryPoints.web.http.redirections.entrypoint.permanent: true
    certificatesResolvers.letsencrypt.acme.email: "email@test.com"
    certificatesResolvers.letsencrypt.acme.storage: "/etc/letsencrypt/acme.json"
    certificatesResolvers.letsencrypt.acme.httpchallenge: true
    certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: "web"

Configuring EntryPoints

First, we’ll begin configuring Traefik’s EntryPoints. As the name suggests, an EntryPoint defines where network connections enter the Traefik proxy service. An EntryPoint defines the ports that Traefik opens up to accept requests.

By default, Kamal doesn’t set up any specific EntryPoints, using port 80 as a “catch-all” EntryPoint into the web application. We’ll have to explicitly set up EntryPoints in our Traefik service to handle port 80 and port 443. While we won’t use port 80 for regular requests to our web application, we’ll still need it open for Let’s Encrypt’s SSL certificate generation process.

The first two arguments we’re passing to Traefik are entryPoints.web.address and entryPoints.websecure.address. Arguments beginning with entryPoints (note the capital “P”) are for managing Traefik’s EntryPoints. The web and websecure segments of these arguments are the names of our EntryPoints, and we can use any name for these. The address segment tells Traefik the host and port we want to configure for the EntryPoint.

We’ll use the web EntryPoint as the non-secure one for port 80. We don’t need to specify a host if you don’t need to limit the scope of the EntryPoint, but we do need a port, so we can set the value of this argument to ":80". The colon is important so Traefik knows to accept requests for any host on our server. We’ll do the same for the websecure EntryPoint but use port 443 instead, so the argument’s value is ":443".

These settings will open up Traefik to accept network requests for these ports. However, we eventually want our non-secure HTTP requests redirected to the secure HTTPS EntryPoint. The other three EntryPoint arguments we have handle this redirection for us.

The entryPoints.web.http.redirections.entryPoint.to argument sets up Traefik to redirect from the specified EntryPoint—in this case, web—to another. The argument’s value is the EntryPoint to which we want to redirect. We called our secure EntryPoint websecure, so that’s the value to set here.

The following argument, entryPoints.web.http.redirections.entryPoint.scheme, tells Traefik that the redirect for the web EntryPoint we specified above should automatically transition the URL from “http://” to “https://”.

Finally, the last redirection-related argument we’ll set is entryPoints.web.http.redirections.entryPoint.permanent. Setting the value of this argument to true will tell Traefik to send a “308 Permanent Redirect” status code to the client when attempting to go to the non-secure HTTP address of our web application. This status code helps search engines and other clients know they should use the secure EntryPoint moving forward.

Configuring Let’s Encrypt

Now it’s time to configure Traefik to use Let’s Encrypt to generate an SSL certificate for our application, which we can also do through arguments. These arguments will have the prefix of certificatesResolvers.letsencrypt.acme, indicating we’ll use Let’s Encrypt’s Automated Certificate Management Environment, or ACME, protocol.

The first argument we’ll set up for this is certificatesResolvers.letsencrypt.acme.email to indicate which email address to register for generating the certificate. This argument is required, and it’s important to set a valid email address so you can receive notifications about expiring certificates and other vital information from Let’s Encrypt. They won’t spam you with unimportant messages and only use this to send critical notifications.

Next, we’ll set up the certificatesResolvers.letsencrypt.acme.storage argument. This argument sets the file that Let’s Encrypt will use to store the information about the generated certificate. Remember the volume we set up earlier in the options section of the Traefik configuration? Here is where we’ll put it to use.

The value for this argument is the same file that we’ll mount in the Traefik Docker container. Make sure the file is in the exact location inside of the Traefik container as configured when mounting the volume and that the file is created and set with the proper permissions on the host server.

After setting the email and the storage path, we’ll need to configure the Let’s Encrypt ACME challenge using the certificatesResolvers.letsencrypt.acme.httpchallenge argument. Let’s Encrypt has different types of challenges for verifying how we control this domain name and server, depending on your needs, which we can set with Traefik.

The easiest and most automated way to handle it for a single domain using Traefik is the HTTP challenge. This challenge requires a specific URL for your domain containing a token and thumbprint of your account key. By setting this argument to true, Traefik automatically handles setting up this route and the contents of the web page for you, so you won’t have to do anything for the challenge.

Finally, we’ll set the certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint label to tell Traefik which EntryPoint to use when requesting the unique URL for the challenge. We’ll need to send our request to the web EntryPoint since it’s the only accessible one before the SSL certificate process completes.

Configuring Traefik Routers to Our Web Application

The configuration done so far handles the routing for Traefik, but we still need to configure our web application to generate an SSL certificate for its domain and use the websecure EntryPoint for connections to the host. We need to modify how Traefik routes traffic to our web application container, which we can do by applying labels to our web application container.

We can apply these labels under the servers top-level key in our Kamal configuration for each role we need. In our case, we want to apply the labels to the web application, so we’ll create a new labels section under the web role in the configuration. Again, I’ll show the final configuration for the labels we need and explain them below:

servers:
  web:
    hosts:
      - 123.123.123.123
    labels:
      traefik.http.routers.airportgap.rule: Host(`airportgap.com`)
      traefik.http.routers.airportgap.entrypoints: "websecure"
      traefik.http.routers.airportgap.tls.certresolver: "letsencrypt"

The labels we’ll configure in our application container will create a new router in our Traefik service. In Traefik, routers handle any incoming requests, processes them according to your configuration, and eventually passes the request to a service—in this case, our web application.

Labels related to Traefik routing in Docker are strings prefixed with traefik.http.routers, followed by the router’s name. Since we’re creating a new router, we can use any name as long as it’s unique and we use the same router name where relevant in our configuration. For this example, I’ll name our router airportgap to match the application name.

The first router label we’ll configure is traefik.http.routers.airportgap.rule. This label helps Traefik figure out which configuration to use for a given router according to what it matches. For example, we can have a router with a rule that matches requests to www.domain.com, which passes the request to a web application service, while another router contains a rule matching requests to api.domain.com, which passes the request to a separate API service.

You can specify a router rule by the request’s hostname, path, headers, and many other matchers, along with combining them to create more specific routing rules when needed, like matching a host and path. The Traefik documentation has more details about the rules you can set for your routers.

To keep it simple for this example, I’ll set a rule to match any requests made to the hostname airportgap.com, so the value for this label will be Host(`airportgap.com`). It’s important to note that when setting up a rule definition like this one in Traefik, you need to use backticks and not single quotation marks.

The following label I’ll set is traefik.http.routers.airportgap.entrypoints, which specifies the EntryPoint to allow for this router. We defined two EntryPoints in Traefik, called web for requests to port 80 and websecure for requests to port 443. Since we want to use secure HTTPS connections, we’ll use the websecure EntryPoint as the value for this label.

Finally, we’ll set up the traefik.http.routers.airportgap.tls.certresolver label to tell Traefik that the airportgap router will use a certificate resolver to trigger an SSL certificate generation challenge. The value we’ll use here is the name of the built-in letsencrypt resolver.

Setting up this label tells Traefik to handle all of the Let’s Encrypt complexities for us:

  • The first time someone makes a request through this router, Traefik will notice there’s no SSL certificate for it and use Let’s Encrypt to generate one that lasts 90 days.
  • After a successful certificate generation, Traefik will automatically track its expiration date and attempt to renew the certificate 30 days before it expires.

If you configure Traefik and the container labels correctly, you won’t ever have to worry about expiring certificates.

Generating Your Let’s Encrypt SSL Certificate Using Kamal

By configuring the traefik settings and adding labels for our web container, our Kamal configuration is all set up to generate an SSL certificate automatically on a single web host.

Generating the certificate for your first deployment

If you haven’t deployed your app yet using Kamal, you only need to use the initial kamal setup command to configure your server and deploy your application for the first time. Once you’ve successfully deployed your application, you should have HTTPS working from the start.

Generating the certificate on an existing application

If you already deployed Kamal before configuring your deployments to handle SSL certificate generation, you’ll need to take a few steps to ensure all the necessary components are up to date.

First, you’ll need to reboot the Traefik instance running on the server so it runs the service with the updated configuration. You can reboot the service using kamal traefik reboot. This command warns you that your application will have a brief outage while Kamal stops the container, removes the old one, and starts a new one. Unless there’s a problem with the configuration, rebooting Traefik only takes a few seconds.

After rebooting Traefik, the container should reload successfully with all the new options and arguments you configured in the Kamal configuration file. The output of this command should show these details so you’ll know the new configuration is ready to go.

Next, you’ll need to reboot the web application so it can set the new container labels as configured. You can do this with kamal app boot --roles=web to limit the reboot to only the containers under the web role. For some reason, there’s no kamal app reboot command, but the kamal app boot command does the same thing as when rebooting Traefik, where it stops and replaces the container with the current configuration.

After restarting the application, you should be able to access it through HTTPS, along with automatic redirects from http:// to https://.

If you encounter issues when rebooting Traefik or the web application through Kamal, double-check that you have correctly set the new configuration updates for your application and that they’re appropriately indented, as YAML is notorious for choking when lines aren’t indented as expected.

Wrap Up

With just a few updates to your Kamal configuration, you’ll have end-to-end encryption between the user and your web application. The Traefik reverse proxy service Kamal uses to route requests to your app has all you need to generate an SSL certificate through Let’s Encrypt and renew it automatically. While it’s not as simple as toggling a setting in Kamal, the process is easy enough to get working quickly once you know how to configure it.

Need help managing your web applications using Kamal?

If you’re stuck configuring Kamal to generate SSL certificates or are interested in using Kamal for your web applications, send me a message and let’s talk about it. With over 20 years of experience working on web applications and focused on DevOps, I can help you get your applications up and running the right way.

Screencast

If this article or video helped you with setting up Let’s Encrypt for your Kamal-deployed web applications, consider subscribing to my YouTube channel for similar videos containing tips on helping Rails developers ship their code with more confidence, from development to deployment.

More articles you might enjoy

Article cover for Deploy Your Rails Applications the Easy Way With Kamal
Rails
Deploy Your Rails Applications the Easy Way With Kamal

Kamal is a new deployment tool that makes it easy to deploy your web applications to any server. Is it a good choice for you?

Article cover for Distributing Docker Images for Rails Apps With GitHub Actions
Rails
Distributing Docker Images for Rails Apps With GitHub Actions

Learn how to automatically build and distribute Docker images for your Rails apps and streamline your development, testing, and deployment workflows.

Article cover for Building Lean Docker Images for Rails Apps
Rails
Building Lean Docker Images for Rails Apps

Are you building Docker images for your Rails apps that take up too much unnecessary space? Learn how to keep your file sizes under control.

About the author

Hi, my name is Dennis! As a freelancer and consultant, I work with tech organizations worldwide to help them build effective, high-quality software. It's my mission to help these companies get their idea off the ground quickly and in the right way for the long haul.

For over 20 years, I've worked with startups and other tech companies across the globe to help them successfully build effective, high-quality software. My experience comes from working with early-stage companies in New York City, San Francisco, Tokyo, and remotely with dozens of organizations around the world.

My main areas of focus are full-stack web development, test automation, and DevOps. I love sharing my thoughts and expertise around test automation on my blog, Dev Tester, and have written a book on the same topic.

Dennis Martinez - Photo
Learn more about my work Schedule a call with me today