Skip to content

Kamal

Reshape ships ready-to-use Kamal configuration for both applications — api/.kamal/ and web/.kamal/ — so you can deploy the whole stack to a single VPS with no orchestration platform required. This guide walks through provisioning a server, configuring both apps, and the day-to-day deploy commands, for a single Ubuntu server. Everything here is one way to do it; the Kamal docs are the canonical reference if you want to go further (multiple servers, multiple regions, Traefik customization, and so on).

A successful Kamal deploy

Two apps, two configs

Everything below is done twice — once from api/ and once from web/ — since each app has its own .kamal/deploy.yml. The API additionally runs a second Kamal role for Reverb (websockets) and a PostgreSQL accessory; the frontend doesn't need either.

1. Provision a server

Get a VPS running Ubuntu from a provider of your choice — OVH, Hetzner, and netcup are all solid, inexpensive options. One server is enough to run both apps plus PostgreSQL for a small-to-medium deployment.

Once it's up:

  1. Create a non-root user with sudo access — don't deploy as root.
  2. Note that user's name; you'll set it as ssh.user in both api/.kamal/deploy.yml and web/.kamal/deploy.yml (it's nik in the shipped example configs — change it to yours).

2. Prepare the server

SSH in as your sudo user and install Docker, which Kamal deploys everything through:

bash
sudo apt update
sudo apt upgrade -y
sudo apt install -y docker.io curl git
sudo usermod -a -G docker $USER

Log out and back in (or run newgrp docker) so the group change takes effect.

3. Install Kamal locally

Kamal is a Ruby gem, run from your own machine — it drives the server over SSH, it doesn't run as a service there:

bash
gem install kamal

4. Configure both apps

Each app's .kamal/deploy.yml is the base config; deploy.production.yml is an overlay merged in when you pass -d production. Before your first deploy, update, in both api/.kamal/deploy.yml and web/.kamal/deploy.yml:

  • ssh.user — the sudo user you created in step 1
  • servers.*.hosts / servers.web — your server's IP address
  • builder.remotessh://<user>@<your-server-ip>
  • proxy.hosts in deploy.production.yml — your real domains (e.g. api.example.com, socket.example.com for the API; example.com for the frontend)

The API's deploy.yml runs two roles on the same server: web (the Laravel app) and reverb (php artisan reverb:start --host=0.0.0.0 --port=9000, proxied separately). It also declares a pgsql accessory (postgres:16) as the production database — Kamal starts, persists, and manages this container for you, so you don't need to install PostgreSQL yourself.

yaml
servers:
  web:
    hosts: [<your-server-ip>]
  reverb:
    hosts: [<your-server-ip>]
    cmd: php artisan reverb:start --host=0.0.0.0 --port=9000
    proxy:
      app_port: 9000

accessories:
  pgsql:
    image: postgres:16
    host: <your-server-ip>
    env:
      clear: { POSTGRES_DB: reshape, POSTGRES_USER: reshape }
      secret: [POSTGRES_PASSWORD:DB_PASSWORD]

The frontend's config is simpler — a single web role serving the built app on port 3000, plus build-time VITE_* args instead of runtime secrets (there's nothing sensitive to inject after the image is built):

yaml
x-env: &env
  VITE_BACKEND_URL: "https://api.example.com"
  VITE_FRONTEND_URL: "https://example.com"
  VITE_REVERB_HOST: "socket.example.com"

5. Set up the registry

Reshape's shipped configs use a local registry (registry.server: localhost:5555), tunneled through the same SSH connection as everything else — no Docker Hub account required. From each app's directory:

bash
cd api
kamal registry setup -c .kamal/deploy.yml -d production

cd ../web
kamal registry setup -c .kamal/deploy.yml -d production

TIP

kamal registry setup sets up a local registry or logs you into a remote one, depending on your registry: config. If you'd rather push to Docker Hub or GHCR, change registry.server (and add credentials) before running this.

6. Create your secrets

Kamal reads runtime secrets from a .kamal/secrets.production file — one KEY=value per line — which is not committed to git (add it to .gitignore if it isn't already). Only the API needs one; the frontend has no runtime secrets to inject.

The keys the shipped api/.kamal/deploy.yml expects, and how to generate reasonable values:

bash
APP_KEY=                 # php artisan key:generate --show
DB_PASSWORD=              # openssl rand -hex 24
REVERB_APP_SECRET=        # openssl rand -hex 32
GOOGLE_CLIENT_SECRET=      # from your Google OAuth app
GITHUB_CLIENT_SECRET=      # from your GitHub OAuth app
LINKEDIN_CLIENT_SECRET=    # from your LinkedIn OAuth app
COMPOSER_AUTH=             # base64 http-basic auth for satis.spatie.be, used only at build time

Keep this file out of version control

Never commit secrets.production. For anything beyond solo/local use, prefer pulling secrets from your shell environment or a password manager rather than hardcoding them in the file — see the Kamal secrets documentation for the supported patterns (1Password, Bitwarden, LastPass, and plain env vars are all built in).

7. First deploy

kamal setup provisions everything for the first time — accessories, the proxy, secrets, and the initial deploy — in one command. Run it once per app:

bash
cd api
kamal setup -c .kamal/deploy.yml -d production

cd ../web
kamal setup -c .kamal/deploy.yml -d production

The API's Docker image (Dockerfile.production, built on serversideup/php:8.5-fpm-nginx) runs database migrations automatically on boot — there's no separate "run migrations" step for a first deploy.

8. Subsequent deploys

Once set up, ship changes with:

bash
kamal deploy -c .kamal/deploy.yml -d production

Run from api/ or web/ depending on which app changed. This builds a new image, pushes it to the registry, and performs a zero-downtime rolling restart with a health check (GET /up on both apps) before old containers are removed.

Day-to-day operations

Tail logs:

bash
kamal app logs -c .kamal/deploy.yml -d production -f

(-f/--follow streams continuously; omit it for a recent snapshot. kamal app logs --help lists further filters like --since and --grep.)

Open a shell in the running container — useful for running one-off Artisan commands (php artisan migrate, php artisan tinker, checking .env values):

bash
kamal app exec -c .kamal/deploy.yml -d production -i "bash"

The API's deploy.yml defines this as a shortcut alias, so from api/ you can just run:

bash
kamal console -c .kamal/deploy.yml -d production

Other useful commandskamal app details (container status), kamal app containers (what's running), kamal rollback (revert to a previous image). Run kamal --help or see the full command reference for everything else.

Learn more

This page covers the path Reshape ships configured for. Kamal supports considerably more — multiple servers per role, multiple regions/destinations, custom Traefik/proxy options, and non-Docker-Hub registries. Start with the Kamal documentation for anything not covered here, particularly: