Run TulipFarm with Docker Compose
Install, upgrade, back up, restore, and troubleshoot a single-machine TulipFarm install with the bundled Docker Compose stack.
self-host/compose.yaml runs the whole product on one machine. It includes PostgreSQL,
S3-compatible object storage, and Temporal. Read the overview first.
Install
Section titled “Install”-
Clone the repository and enter
self-host/:Terminal window git clone https://github.com/TulipFarm/tulipfarm.gitcd tulipfarm/self-host -
Supply the operator settings. Pick one method:
- Plain file:
cp .env.example .env, fill it in, thenchmod 600 .env. - Secret store: store the same names in Doppler, Infisical, 1Password, or Vault. See Secret stores.
- Encrypted file: encrypt the file with SOPS and your KMS. See KMS.
Set
TULIPFARM_RELEASE_COMMITto the output ofgit rev-parse HEAD. Generate passwords withopenssl rand -hex 32. - Plain file:
-
Start the stack. Prefix the command with your secret-store runner if you use one, for example
doppler run -- docker compose up -d --build:Terminal window docker compose up -d --build -
Check health:
Terminal window docker compose pscurl https://tulipfarm.example.com/v1/healthEvery service reports
"status":"ok".setup,temporal-schema, andtemporal-namespaceexit with code 0. The others keep running.
Startup order
Section titled “Startup order”postgres ─┬─▶ setup ─┬─▶ temporal-schema ─▶ temporal ─▶ temporal-namespace ─┐storage ─┘ └──────────────────────────────────────────────────────┴─▶ api, worker, storage-worker ─▶ websetup is idempotent and runs on every up. It:
- Creates
state.jsonwith generated passwords and keys, once. - Creates roles and the
tulipfarm,temporal, andtemporal_visibilitydatabases. - Installs extensions and applies migrations.
- Creates restricted logins for workspace storage and email delivery.
- Creates the object-storage bucket.
- Writes
api.env,worker.env,storage-worker.env, andtemporal.envwith mode 0600.
All files go to the generated volume. Each service mounts it read-only. Node services
load their file with node --env-file. A variable already set in the container
environment takes precedence over the file.
HTTPS and reverse proxies
Section titled “HTTPS and reverse proxies”web runs Caddy. When
TULIPFARM_SITE_ADDRESS is a domain name, Caddy obtains and renews certificates
itself. Ports 80 and 443 must be reachable.
If TulipFarm runs behind another proxy or load balancer:
TULIPFARM_SITE_ADDRESS=:80TULIPFARM_HTTP_PORT=8080 # your proxy forwards hereKeep TULIPFARM_PUBLIC_URL as the public HTTPS origin. The API uses
API_PROXY_MODE=reverse-proxy. It trusts only the rightmost X-Forwarded-For hop,
which Caddy appends.
Temporal UI
Section titled “Temporal UI”The Temporal UI listens on 127.0.0.1:8233 only. Reach it through an SSH tunnel:
ssh -L 8233:127.0.0.1:8233 you@your-serverUpgrade
Section titled “Upgrade”git pull# Set TULIPFARM_RELEASE_VERSION and TULIPFARM_RELEASE_COMMIT to the new release.docker compose up -d --buildsetup applies new migrations before the application services restart. Back up
first. Downgrades are not supported. Restore a backup instead.
Back up
Section titled “Back up”Back up three things together:
| What | Why | Command |
|---|---|---|
state.json |
Holds every generated password and encryption key | See below |
| PostgreSQL | All application, workspace, and Temporal data | docker compose exec -T postgres pg_dumpall -U postgres > tulipfarm.sql |
object-storage-data volume |
Workspace repositories | Copy the volume, or use your storage service’s backups |
Copy state.json out of the volume. Encrypt it before storing it; see
KMS:
docker compose run --rm -T --no-deps --entrypoint cat setup /setup/state.json > state.jsonchmod 600 state.jsonpg_dumpall output contains password hashes and encrypted credentials. Protect it like
state.json.
Restore
Section titled “Restore”Restore state.json before the first setup run on the new host:
docker compose up -d postgres object-storagedocker compose run --rm -T --no-deps --entrypoint sh setup \ -c 'umask 077 && cat > /setup/state.json' < state.json# Restore PostgreSQL and object storage, then:docker compose up -dSetup then resets each role to the password in state.json.
Stop and remove
Section titled “Stop and remove”docker compose down # keeps datadocker compose down -v # deletes all data, including state.jsonTroubleshoot
Section titled “Troubleshoot”| Symptom | Check |
|---|---|
required variable … is missing |
The setting is not in the shell environment or self-host/.env. |
setup exits 1 |
docker compose logs setup, then docker compose logs postgres. Setup never prints credentials. |
| Sign-in fails | Turnstile keys must be real and allow your hostname. The Google redirect URI must be <public URL>/v1/auth/callback/google. |
/v1/health reports workers down |
docker compose logs worker storage-worker. |