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).

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:
- Create a non-root user with
sudoaccess — don't deploy asroot. - Note that user's name; you'll set it as
ssh.userin bothapi/.kamal/deploy.ymlandweb/.kamal/deploy.yml(it'snikin 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:
sudo apt update
sudo apt upgrade -y
sudo apt install -y docker.io curl git
sudo usermod -a -G docker $USERLog 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:
gem install kamal4. 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 1servers.*.hosts/servers.web— your server's IP addressbuilder.remote—ssh://<user>@<your-server-ip>proxy.hostsindeploy.production.yml— your real domains (e.g.api.example.com,socket.example.comfor the API;example.comfor 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.
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):
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:
cd api
kamal registry setup -c .kamal/deploy.yml -d production
cd ../web
kamal registry setup -c .kamal/deploy.yml -d productionTIP
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:
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 timeKeep 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:
cd api
kamal setup -c .kamal/deploy.yml -d production
cd ../web
kamal setup -c .kamal/deploy.yml -d productionThe 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:
kamal deploy -c .kamal/deploy.yml -d productionRun 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:
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):
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:
kamal console -c .kamal/deploy.yml -d productionOther useful commands — kamal 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:
- Configuration reference — every option in
deploy.yml - Secrets — password manager integrations
- Accessories — databases, Redis, and other sidecar services
- Commands — the full CLI reference