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
.