# How to Deploy with Kamal on a Single Server

## **What is Kamal**

[Kamal](https://kamal-deploy.org/) is a new deployment tool designed for the efficient rollout of containerized applications. It's developed by 37Signals, the creators of Basecamp and Hey.

Kamal provides zero-downtime deployments, rolling restarts, remote builds, and extensive service management. It automatically installs Docker, builds containers, pushes them to a container repository, and deploys the new versions on the servers.

## Single server deployment

Most YouTube videos and blog posts focus on deploying across multiple servers (involving load balancers, application servers, and possibly database servers). However, I recently deployed using Kamal on a single server. I believe this article could be helpful if you find yourself in a similar situation.

If you're interested in seeing Kamal's multi-node deployment in action, below is a video of DHH giving a fast-paced and well-structured demo.

%[https://www.youtube.com/watch?v=yWSpjKErnco] 

## Install and deploy with Kamal

Install Kamal on your computer with: `gem install kamal`, then go to your project directory and run: `kamal init`. This command creates two folders in your project: `.kamal` and `config/deploy.yml`.

The `.kamal` folder contains hooks for before and after deployment:

* **Pre-connect hooks** – run before Kamal locks for deployment and before connecting to the remote servers.
    
* **Pre-build hooks** – executed before the build process begins, allowing for checks before building.
    
* **Pre-deploy hooks** – happen just before the deployment process starts.
    
* **Post-deploy hooks** – occur after a deployment, redeployment, or rollback.
    

## Configuration

Kamal operates under the assumption that one image is used for one application. It adds the service name at the beginning of container names.

The primary configuration for Kamal is found in the `servers` directive. In this article, I'll include both a web container and a PostgreSQL container.

In the `env` section, we add both public (`clear`) and private (`secret`) configurations.

I'm developing on macOS, and I specify the architecture to Kamal in the `builder` directive.

```yaml
# config/deploy.yml
​
# Name of your application.
service: my_app
​
image: username/image_name
​
# Deploy to these servers.
servers:
  web:
    hosts:
      - server_public_ip_address
    labels:
      traefik.http.routers.your_domain.rule: Host(`your_domain.tld`)
      traefik.http.routers.your_domain_secure.entrypoints: websecure
      traefik.http.routers.your_domain_secure.rule: Host(`your_domain.tld`)
      traefik.http.routers.your_domain_secure.tls: true
      traefik.http.routers.your_domain_secure.tls.certresolver: letsencrypt
    options:
      network: "private"
​
registry:
  # Specify the registry server
  username: registry_username
  password:
    - KAMAL_REGISTRY_PASSWORD
​
env:
  clear:
    HOSTNAME: your_hostname
  secret:
    - KAMAL_REGISTRY_PASSWORD
    - POSTGRES_PASSWORD
    
# Configure builder setup.
builder:
  local:
    arch: arm64
    host: unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock
```

## Accessories: PostgreSQL

Any other auxiliary services are referred to as `accessory`. In this article, I'll be using PostgreSQL as one of the auxiliary services, but you can add as many as you need.

The configuration is self-explanatory, so I won't go into much detail.

```yaml
# config/deploy.yml
...
accessories:
  db:
    image: postgres:16
    host: server_public_ip_address
    port: 5432
    env:
      clear:
        POSTGRES_USER: "my_user"
        POSTGRES_DB: 'my_app_prod'
      secret:
        - POSTGRES_PASSWORD
    files:
      - scripts/init.sql:/docker-entrypoint-initdb.d/setup.sql
    directories:
      - data:/var/lib/postgresql/data
```

We'll use the `files` directive to specify our entry point, and use `scripts/init.sql` for creating the database.

```sql
# scripts/init.sql
CREATE DATABASE my_app_prod;
```

## Traeffik

Traefik manages TLS termination, HTTPS, redirects, and zero downtime deployments. We'll mount the `acme.json` file needed for Let's Encrypt and set up a Docker private network. The `args` are utilized here to define the entry points and details for the challenge.

```yaml
# config/deploy.yml
...
traefik:
  options:
    publish:
      - "443:443"
    volume:
      - "/letsencrypt/acme.json:/letsencrypt/acme.json"
    network: "private"
  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: "your_email_address"
    certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
    certificatesResolvers.letsencrypt.acme.httpchallenge: true
    certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web
```

## Manual steps

Before we start the deployment process, let's SSH into our server and execute a few commands:

* Setup the directory for Let's encrypt
    
    ```bash
    mkdir -p /letsencrypt && touch /letsencrypt/acme.json && chmod 600 /letsencrypt/acme.json
    ```
    
* create a Docker network called `private`
    
    ```bash
    docker network create -d bridge private
    ```
    

## Deploy

With the config file saved and everything set up on the server, it's time to start the deployment process.

`kamal setup`

This will:

1. Connect to the servers over SSH (using root by default, authenticated by your ssh key)
    
2. Install Docker and curl on any server that might be missing it (using apt-get): root access is needed via ssh for this.
    
3. Log into the registry both locally and remotely
    
4. Build the image using the standard Dockerfile in the root of the application.
    
5. Push the image to the registry.
    
6. Pull the image from the registry onto the servers.
    
7. Push the ENV variables from .env onto the servers.
    
8. Start a new container with the version of the app that matches the current git version hash.
    
9. Stop the old container running the previous version of the app.
    
10. Prune unused images and stopped containers to ensure servers don’t fill up.
    

## **Other commands you might find useful**

`kamal env push` - push env var changes (won't restart services)

`kamal redeploy`

`kamal app logs`

`kamal traefik logs`

```bash
kamal app exec `python3 --version` # run a command on all your servers to check the Python version
```

`kamal app exec -i bash` - new container bash session

`kamal details` - gives you a snapshot of your containers, their status, and how they are performing

`kamal rollback [previous_image_tag]` - execute a rollback

`kamal traefik reboot` - reboot Traefik

`kamal traefik reboot --rolling` - a rolling reboot methodically restarts Traefik containers sequentially, ensuring no downtime and maintaining continuous load-balancing functionality

Deploying with Kamal on a single server streamlines the process of rolling out containerized applications, ensuring minimal downtime and optimal performance. Use this article as inspiration for your own configurations rather than as a strict guide.

Happy deploying!
