# Run TulipFarm with Docker Compose

> Install, upgrade, back up, restore, and troubleshoot a single-machine TulipFarm install with the bundled Docker Compose stack.

Source: https://tulipfarm.ai/docs/self-hosting/docker-compose/

[`self-host/compose.yaml`](https://github.com/TulipFarm/tulipfarm/blob/main/self-host/compose.yaml) runs the whole product on one machine. It includes PostgreSQL,
S3-compatible object storage, and Temporal. Read the [overview](https://tulipfarm.ai/docs/self-hosting/index.md) first.

## Install

1. Clone the repository and enter `self-host/`:

   ```sh
   git clone https://github.com/TulipFarm/tulipfarm.git
   cd tulipfarm/self-host
   ```

2. Supply the operator settings. Pick one method:

   - **Plain file:** `cp .env.example .env`, fill it in, then `chmod 600 .env`.
   - **Secret store:** store the same names in Doppler, Infisical, 1Password, or
     Vault. See [Secret stores](https://tulipfarm.ai/docs/self-hosting/secrets/index.md).
   - **Encrypted file:** encrypt the file with SOPS and your KMS. See [KMS](https://tulipfarm.ai/docs/self-hosting/kms/index.md).

   Set `TULIPFARM_RELEASE_COMMIT` to the output of `git rev-parse HEAD`.
   Generate passwords with `openssl rand -hex 32`.

3. Start the stack. Prefix the command with your secret-store runner if you use one,
   for example `doppler run -- docker compose up -d --build`:

   ```sh
   docker compose up -d --build
   ```

4. Check health:

   ```sh
   docker compose ps
   curl https://tulipfarm.example.com/v1/health
   ```

   Every service reports `"status":"ok"`. `setup`, `temporal-schema`, and
   `temporal-namespace` exit with code 0. The others keep running.

## Startup order

```text
postgres ─┬─▶ setup ─┬─▶ temporal-schema ─▶ temporal ─▶ temporal-namespace ─┐
storage  ─┘          └──────────────────────────────────────────────────────┴─▶ api, worker, storage-worker ─▶ web
```

`setup` is idempotent and runs on every `up`. It:

1. Creates `state.json` with generated passwords and keys, once.
2. Creates roles and the `tulipfarm`, `temporal`, and `temporal_visibility` databases.
3. Installs extensions and applies migrations.
4. Creates restricted logins for workspace storage and email delivery.
5. Creates the object-storage bucket.
6. Writes `api.env`, `worker.env`, `storage-worker.env`, and `temporal.env` with 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

`web` runs [Caddy](https://caddyserver.com/docs/automatic-https). 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:

```sh
TULIPFARM_SITE_ADDRESS=:80
TULIPFARM_HTTP_PORT=8080   # your proxy forwards here
```

Keep `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

The Temporal UI listens on `127.0.0.1:8233` only. Reach it through an SSH tunnel:

```sh
ssh -L 8233:127.0.0.1:8233 you@your-server
```

## Upgrade

```sh
git pull
# Set TULIPFARM_RELEASE_VERSION and TULIPFARM_RELEASE_COMMIT to the new release.
docker compose up -d --build
```

`setup` applies new migrations before the application services restart. Back up
first. Downgrades are not supported. Restore a backup instead.

## 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](https://tulipfarm.ai/docs/self-hosting/kms/index.md#encrypt-backups):

```sh
docker compose run --rm -T --no-deps --entrypoint cat setup /setup/state.json > state.json
chmod 600 state.json
```

`pg_dumpall` output contains password hashes and encrypted credentials. Protect it like
`state.json`.

## Restore

Restore `state.json` before the first `setup` run on the new host:

```sh
docker compose up -d postgres object-storage
docker 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 -d
```

Setup then resets each role to the password in `state.json`.

## Stop and remove

```sh
docker compose down        # keeps data
docker compose down -v     # deletes all data, including state.json
```

## 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`. |
