Guide

Self-host the control plane

Run the server your phone and computers meet at on a machine you control. One command sets up the control plane, the web app and automatic HTTPS.

Helmlane has two halves. The agent runs on your computers and does the work. The control plane is where your phone and your computers meet — accounts, projects, chats, the event feed and notifications. The public one is https://api.helmlane.com; this guide runs your own, so nothing about your work touches a server you do not control. Either way, your files stay on your computers: the control plane relays only what the app asks to see.

What you need

  • A Linux server with a public IPv4 or IPv6 address, 2 GB of RAM and 10 GB of disk, on x86-64 or arm64. Any VPS will do.
  • A domain name such as helmlane.example.com with an A and/or AAAA record pointing at the server.
  • Ports 80 and 443 reachable from the internet, for certificates.
  • Docker Engine with the Compose plugin.

Install

Terminal
curl -fsSL https://raw.githubusercontent.com/RelicDragon/helmlane/main/scripts/selfhost/install.sh | bash

Or, from a checkout, ./scripts/selfhost/install.sh. It asks for:

  1. Your domain. It checks that the name resolves and that ports 80 and 443 are free.
  2. An email address for certificate expiry notices (optional).
  3. Optionally, a DNS provider for a wildcard preview domain and an SMTP relay for account email. Both can be added later.

Then it generates every secret the server needs, builds the images, starts everything, and waits until https://<your domain>/health answers with a real certificate. The first build takes a few minutes. Running the installer again later is safe: it keeps the configuration and brings the stack up to date. Unattended:

Terminal
./scripts/selfhost/install.sh --domain helmlane.example.com --email you@example.com --yes

Connect the apps and computers

  1. Open https://<your domain> in a browser — the web app is served there — or enter your server’s address on the phone app’s sign-in screen, and create your account.
  2. Verify your email. Without an SMTP relay the link is in the server log:
    Terminal
    ./scripts/selfhost/manage.sh mail-links
    Open it within 30 minutes.
  3. In the app, Computers → Add computer. The command it shows already points at your server (--origin https://<your domain>). Linux, WSL and Docker are covered in the Linux guide; on a Mac, sign in to the Helmlane app with your server’s address.

The MCP server takes the same origin:

Terminal
npx -y @helmlane/mcp-server login --origin https://helmlane.example.com

Email

Account email — verification and password reset — goes out through, in order of preference:

  1. SMTP, when HOSTED_SMTP_HOST is set in docker/.env: any relay with authenticated TLS (port 465, or 587 with STARTTLS).
    docker/.env
    HOSTED_SMTP_HOST=smtp.example.com
    HOSTED_SMTP_PORT=465
    HOSTED_SMTP_USER=helmlane@example.com
    HOSTED_SMTP_PASSWORD=…
    HOSTED_EMAIL_FROM=helmlane@example.com
  2. The log, otherwise (HOSTED_EMAIL_TRANSPORT=log, the default here). Links are written to the server log and manage.sh mail-linksshows them. Fine for a server only you use; add SMTP before inviting anyone.

After editing docker/.env, apply it:

Terminal
docker compose -f docker/docker-compose.yml up -d

Preview domain

Previews — a dev server on one of your computers, opened on your phone — are each served at their own origin under a wildcard domain,*.preview.<your domain>, so a dev server’s code can never reach the control plane or another preview. Without that domain the apps say previews are unavailable instead of offering them. A wildcard certificate can only be issued through a DNS challenge, so the installer asks for your DNS provider and an API token:

ProviderToken needs
cloudflareAn API token with Zone → DNS → Edit on the zone
digitaloceanA personal access token with write scope
hetznerA DNS API token
duckdnsYour DuckDNS token

Also add a DNS record *.preview.<your domain> pointing at the server. The installer builds Caddy with the matching plugin and writesdocker/caddy/sites/preview.caddy; delete that file and restartcaddy to turn it off again.

Push notifications

Push reaches a phone through Apple (APNs) and Google (FCM) with keys that belong to the app’s publisher. The published Helmlane apps only accept pushes signed with Helmlane’s own keys, which a self-hosted server does not have. On a self-hosted server the apps therefore show notifications while they are open and report push as unavailable, rather than promising notifications that cannot arrive.

If you build the iOS or Android app yourself under your own identifiers, give the server your keys: place them in docker/secrets/(owner-only, chmod 600) and set in docker/.env:

docker/.env
HOSTED_APNS_TEAM_ID=ABCDE12345
HOSTED_APNS_KEY_ID=XYZ9876543
HOSTED_APNS_KEY_PATH=/run/helmlane-secrets/apns.p8
HOSTED_FCM_SERVICE_ACCOUNT_FILE=/run/helmlane-secrets/firebase-admin.json

Apply with docker compose -f docker/docker-compose.yml up -d; the apps’ notification settings then show push as ready.

Operating it

Terminal
./scripts/selfhost/manage.sh status          # containers, and whether HTTPS answers
./scripts/selfhost/manage.sh logs            # follow the control plane (or: logs caddy)
./scripts/selfhost/manage.sh mail-links      # account links from the log transport
./scripts/selfhost/manage.sh update          # pull the latest source, rebuild, restart
./scripts/selfhost/manage.sh backup          # database + configuration + secrets
./scripts/selfhost/manage.sh restore FILE    # onto a fresh server

Updates apply database migrations on start. Update the server before the agents on your computers: a newer agent talking to an older server is refused until the server catches up. The reverse is fine.

Backups are a single tar.gz in docker/backups/ with a database dump, docker/.env and docker/secrets/. It contains every secret of the server — store it encrypted and off the machine. Images and attachments live in the blobs volume and are not included; back that volume up with your usual tooling if you need them.

Restoring onto a new server: install Docker, clone the repository, copy the backup over, run ./scripts/selfhost/manage.sh restore helmlane-….tar.gz, and point DNS at the new server. Computers reconnect on their own; nothing needs to be enrolled again.

What runs, and where your data is

docker/docker-compose.yml runs four services on a private network:

ServiceRoleExposed
caddyHTTPS with automatic certificates, the web app, reverse proxy80, 443
hostedThe control planeOnly to caddy
postgresAccounts, projects, chats, machinesNo
blobsImages and attachments (S3-compatible, TLS with a private CA)No
WhereWhat
docker/.envConfiguration and generated passwords (0600)
docker/secrets/Signing key, object-store certificates, your push keys
Volume helmlane_postgresThe database
Volume helmlane_blobsUploaded images
Volume helmlane_caddy-dataTLS certificates

To use a managed bucket instead of the bundled store, setHOSTED_S3_ENDPOINT (HTTPS only), HOSTED_S3_REGION,HOSTED_S3_BUCKET and the keys in docker/.env.

Security notes

  • HOSTED_SECRET_KEY is the root secret. Machine credentials and enrollment tokens derive from it. If it is lost, every computer must be enrolled again; if it leaks, rotate it with a new install and re-enroll.
  • Only Caddy listens on the internet. The control plane, database and object store are reachable only on the Compose network.
  • Enrollment tokens are single-use, expire within fifteen minutes, and are stored only as a hash.
  • The control plane never stores your files. It relays what the app requests from a computer you enrolled, and each computer serves only the folders you shared on it.
  • Keep the server patched (unattended-upgrades or your distribution’s equivalent) and update Helmlane with manage.sh update.

Troubleshooting

You seeWhat to do
The installer waits for HTTPS and gives upDNS does not point at the server yet, or a firewall blocks 80/443. manage.sh logs caddy shows the certificate attempts.
hosted keeps restartingmanage.sh logs hosted. A missing or unreadable secret names itself in the first lines.
Registration answers “email delivery unavailable”Neither SMTP nor the log transport is configured; check HOSTED_EMAIL_TRANSPORT in docker/.env.
A computer enrolls but never comes onlineIt must reach https://<your domain> on outbound 443. helmlane-agent status on it shows the origin it uses.
Images fail to uploadmanage.sh logs blobs and docker compose -f docker/docker-compose.yml logs blobs-init.