Notes to self

Deploying a Next.js application with Kamal 2

Here’s the simplest example to deploy a containerized Next application with Kamal.

What’s Kamal

Kamal is a new tool from 37signals for deploying web applications to bare metal and cloud VMs. It comes with zero-downtime deploys, rolling restarts, asset bridging, remote builds, and more.

Kamal needs SSH configured and Docker installed to run. You also need to create a cloud VM somewhere like Hetzner or Digital Ocean.

SSH configuration

Linux and macOS should come with SSH installed but you’ll need a new key pair for the server:

$ ssh-keygen -t EcDSA -a 100 -b 521 -C "admin@example.com"

This will promt you for the key name and will save the keys to ~/.ssh/ by default.

Add the private key to the SSH agent:

$ ssh-add ~/.ssh/[KEY]

Host provisioning

Create a cloud VM on your favourite provider, choose your prefered Linux operating system (e.g. Ubuntu 24 LTS), and provision the server with your public key from the previous step.

Note the host public IP address.

Once the cloud VM is ready, you can recheck if SSH works by running:

$ ssh root@[IP_ADDRESS]

Docker configuration

Install Docker locally if you don’t have it. If you are on macOS you can install Docker Desktop or OrbStack.

Then sign up for a Docker repository such as Docker Hub and create a first private repository with your application name like my-next-app.

Create an access token and note it down.

Dockerfile

You’ll need to write a Dockerfile for your Next application. A very basic one can look like the following:

FROM node:20 AS base
WORKDIR /app
# RUN npm i -g pnpm
COPY package.json package-lock.json ./

RUN npm install

COPY . .
RUN npm run build

FROM node:20-alpine3.19 AS release
WORKDIR /app
# RUN npm i -g pnpm

COPY --from=base /app/node_modules ./node_modules
COPY --from=base /app/package.json ./package.json
COPY --from=base /app/.next ./.next

EXPOSE 80

CMD ["npm", "start"]

If you use pnpm you’ll need to install it and replace the expected files. Also note that we are exposing port 80. Commit the Dockerfile to your source control.

To start Next we’ll provide the -p 80 argument to next start in package.json:

{
  "name": "my-next-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start -p 80",
    "lint": "next lint"
  },
  "dependencies": {
    "react": "^18",
    "react-dom": "^18",
    "next": "14.2.15"
  }
}

And finally we have to ensure we’ll build the application in standalone mode. Change output to standalone in next.config.mjs:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "standalone",
};

Kamal installation

You likely don’t have Ruby around, so install Kamal as Docker image by creating a command alias.

On macOS for $HOME/.zshrc:

$ alias kamal='docker run -it --rm -v "${PWD}:/workdir" -v "/run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock" -e SSH_AUTH_SOCK="/run/host-services/ssh-auth.sock" -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/basecamp/kamal:v2.2.1'

On Linux for $HOME/.bashrc:

$ alias kamal='docker run -it --rm -v "${PWD}:/workdir" -v "${SSH_AUTH_SOCK}:/ssh-agent" -v /var/run/docker.sock:/var/run/docker.sock -e "SSH_AUTH_SOCK=/ssh-agent" ghcr.io/basecamp/kamal:v2.2.1'

Kamal configuration

Now open a new terminal window and generate the basic Kamal files with kamal init:

$ kamal init

Open .kamal/secrets and provide the registry token as password:

KAMAL_REGISTRY_PASSWORD="dckr_pat..."

And open config/deploy.yml and provide a starting configuration:

# Name of your application. Used to uniquely configure containers.
service: my-next-app

# Name of the container image.
image: [REGISTRY_USER]/my-next-app

# Deploy to these servers.
servers:
  web:
    - [PUBLIC_IP_ADDRESS]

# Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server).
# If using something like Cloudflare, it is recommended to set encryption mode
# in Cloudflare's SSL/TLS setting to "Full" to enable end-to-end encryption.
proxy:
  ssl: true
  host: [DOMAIN_NAME]

# Credentials for your image host.
registry:
  # Specify the registry server, if you're not using Docker Hub
  # server: registry.digitalocean.com / ghcr.io / ...
  username: [REGISTRY_USER]

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

# Configure builder setup.
builder:
  arch: amd64

If you have a domain name, provide a domain name and set ssl to true, otherwise keep it false.

Deploy

Now you can deploy the app by running:

$ kamal setup

Kamal now should be able to do its thing, log in to Docker registry, build the application, run Kamal Proxy on the server, and all of the other required steps to run your application.

Any subsequent deploys can be then done using kamal deploy.

Check out my book
Learn how to use Kamal to deploy your web applications with Kamal Handbook. Visualize Kamal's concepts, understand Kamal's configuration, and deploy practical life examples.
by Josef Strzibny
RSS