Cover Image: Deploy with Kamal
Photo by Domaintechnik on Unsplash

October 21, 2026

Deploy with Kamal

A crucial step in software development is deployment. Getting the software from the development stage to the public accessible for the end-users is a task that can be quite complex and time-consuming. However, with the help of deployment tools like Kamal, deploying an application can be simplified and made more efficient.

What is Kamal

From Kamals website.

Kamal offers zero-downtime deploys, rolling restarts, asset bridging, remote builds, accessory service management, and everything else you need to deploy and manage your web app in production with Docker. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized.

What they are not mentioning on their website is that they take care of the load balancer and SSL Certificates; you will not need to take care of that, it's all baked into Kamal.

This post

In this little post, I'll show you how to deploy a static website with Kamal, from your local workstation. I'll make a post later on more complex setups but also deployment with CI tools like GitHub Actions and GitLab CI.

Disclaimer:

I'm on Linux, so the command I'll be using may differ from your operating system.

Prerequisites

I will list a bare minimum of requirements:

How to deploy a static website with Kamal

Preparing the project we want to deploy

First, we will create a new directory with our project files. Let's call it my-kamal-website, and enter the directory

Open a terminal and navigate to the directory where you want to create the project. It's essential that we have our files in Git, as Kamal uses Git for deployment.

cd ~/Projects/Demos
mkdir my-kamal-website
cd my-kamal-website
git init

Create an HTML file index.html inside the directory.

<!DOCTYPE html>
<html>
  <head>
    <title>My Kamal Website</title>
  </head>
  <body>
    <h1>Hello, World!</h1>
  </body>
</html>

Now we will need to install Kamal and configure it for our project.

kamal init

kamal init will create several files in your directory, including a config/deploy.yml and a .kamal-directory.

The .kamal-directory holds a files secrets and hooks-directory. The files in .kamal/hooks are examples of hooks that you can use to customise the deployment process. You can modify or delete them as needed, I will not talk about them in this post. The .kamal/secrets we will talk about soon.

╭╴ Projects/Demos/my-kamal-website 
╰─❯ tree -a  
.
├── config
│   └── deploy.yml
├── index.html
└── .kamal
    ├── hooks
    │   ├── docker-setup.sample
    │   ├── post-app-boot.sample
    │   ├── post-deploy.sample
    │   ├── post-proxy-reboot.sample
    │   ├── pre-app-boot.sample
    │   ├── pre-build.sample
    │   ├── pre-connect.sample
    │   ├── pre-deploy.sample
    │   └── pre-proxy-reboot.sample
    └── secrets

4 directories, 12 files

If we look into the file created config/deploy.yml we will see that there are a lot of configuration options available to customise the deployment process. But we will need little of it for this simple deployment.

I have made a simplified version of the file, with information that we need, with comments as well.

# Name of your application. Used to uniquely configure containers.
service: com-kamal-website
# Name of the container image.
image: vendor/my-kamal-website

# Deploy to these servers.
servers:
  web:
    - 192.168.0.100 # Server IP

# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
proxy:
  ssl: true
  host: domain.tld

# Credentials for your image host.
registry:
  # Specify the registry server if you're not using Docker Hub
  # server: registry.domain.tld
  username: docker-hub-username

  # Always use an access token rather than real password (pulled from .kamal/secrets).
  password:
    - KAMAL_REGISTRY_PASSWORD

# Configure builder setup, the platform you are building for. There are other options to set as well.
builder:
  arch: amd64

Let's look into the .kamal/secrets file. It holds the instructions on how to get the secrets for the deployment. You should choose the option that fits your setup the best, I like working with environment Variables (option 1), It should be needless to say, but it's IMPORTANT that you keep your secrets out of Git.

# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.

# Option 1: Read secrets from the environment
# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD

# Option 2: Read secrets via a command
# RAILS_MASTER_KEY=$(cat config/master.key)
# KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)

# Option 3: Read secrets via kamal secrets helpers
# These will handle logging in and fetching the secrets in as few calls as possible
# There are adapters for 1Password, LastPass + Bitwarden
#
# SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)

What to deploy?

As you have seen in the config/deploy.yml, we are deploying a docker image, so we would need to prepare that image before deploying it.

Docker images are built from Dockerfiles, so we would need to create a Dockerfile for our project. An image for a project like this is less complex than images, with PHP and MySQL.

What do we need in our Dockerfile?

We need to define which Docker image we are building on top of, that be nginx:alpine, one can use apache or caddy too if preferred. After defining the FROM we are copying the nginx-default.conf and index.html into the container.

FROM nginx:alpine 
COPY config/nginx-default.conf /etc/nginx/conf.d/default.conf  
COPY index.html /usr/share/nginx/html/index.html 

The config/nginx-default.conf looks like this (see below), one important part is the location = /up endpoint for Kamal health checks, if that's not present, Kamal will not be able to check if the deployment is successful. Kamal uses the health check for zero-downtime rolling deployments.

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;
    server_tokens off;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    # Health check endpoint for Kamal
    location = /up {
        return 200 'OK';
        add_header Content-Type text/plain;
    }

    # Compression
    gzip            on;
    gzip_min_length 256;
    gzip_comp_level 5;
    gzip_proxied    expired no-cache no-store private auth;
    gzip_types      text/plain application/xml text/css application/javascript application/x-javascript text/javascript application/json;
}

Deployment

Before we are ready to deploy, make sure all changes are committed; they don't need to be pushed to a remote repository, just needed to be committed. This is important because Kamal uses Git for deployment.

Now we are ready to deploy our project with Kamal. Let's jump into the project directory and run the kamal deploy command.

kamal deploy

The deployment took on my system 15 seconds, including getting an SSL Certificate and making sure the site is up and running.

That's it! Your website is now live and accessible at https://your-domain.com with a valid SSL Certificate and ready to present to your users.