# Self-host Blop Cloud

> Run Blop Cloud on your own server with Docker Compose, set its required variables, and upgrade or roll back safely.

Run Blop Cloud on one server of your own with Docker Compose: the app, Postgres and MinIO for artifacts.

## Prerequisites

- A Linux server with Docker Engine and Docker Compose v2. The app container uses host networking.
- A public domain and a TLS-terminating proxy that forwards to port 3001 on the server. Agent connections need public HTTPS.
- The proxy appends `X-Forwarded-For` exactly once: the Compose file trusts one hop (`XFF_DEPTH=1`) for rate limits and audit records.
- A checkout of the [blop-app repository](https://github.com/blop-oss/blop-app) at the release you deploy.
- Secrets generated by your secret manager. Never commit them.

## Start Blop Cloud

1. Write the [required variables](#required-environment-variables) to `.env` in the repository root, readable only by the deploying user.
2. Build and start:

   ```bash
   docker compose -f docker-compose.dokploy.yml up -d --build
   ```

   The app applies pending database migrations, then listens on port 3001. Postgres (5433) and MinIO (9000, 9001) are published on `127.0.0.1` only.
3. Open your public address and sign up.

On Dokploy, create a **Compose** service (type "Docker Compose", not Swarm) pointed at `docker-compose.dokploy.yml`, and set the same variables in Dokploy.

The root `docker-compose.yml` is local development infrastructure only. To run the app from source for development, see the [repository notes](https://github.com/blop-oss/blop-app/blob/main/docs/agents/cloud/README.md).

## Required environment variables

| Variable | Purpose |
| --- | --- |
| `BASE_URL` | Public HTTPS origin, for example `https://blop.example.com`, with no path. Agents connect to `${BASE_URL}/api/mcp`. |
| `AUTH_SECRET` | Signs session cookies. Changing it signs everyone out. |
| `PROJECT_SECRETS_KEY` | Encrypts saved credentials. Changing it makes them unreadable. |
| `CRON_SECRET` | Bearer token for the [scheduled workers](#schedule-the-workers). |
| `BLOP_OAUTH_SIGNING_KEYS` | Private RSA JWK set (`{"keys":[…]}`), each key with its own `kid`, for agent connections. |
| `BLOP_OAUTH_COOKIE_SECRET` | At least 32 random characters, distinct from every other secret. |
| `PG_PASSWORD` | Postgres password. |
| `MINIO_USER`, `MINIO_PASSWORD` | MinIO credentials for the artifact bucket. |

Set `OPENROUTER_API_KEY` for in-app chat and browser checks started from Blop Cloud, and the `GITHUB_APP_*` variables for GitHub. Keep every secret stable across upgrades. Agent connections work only when `BASE_URL` and both OAuth variables are set. The Compose file passes only the variables it lists to the app: for Slack, Stripe, email, Sentry and other integrations, add each variable to the `web-sk` service's `environment:` block too. [Blop Cloud environment variables](/reference/cloud-environment/) lists the optional ones.

## Schedule the workers

Call each endpoint on your public address with `Authorization: Bearer $CRON_SECRET`, from cron, a systemd timer or any scheduler:

| Endpoint | Schedule |
| --- | --- |
| `/api/scheduler/tick`, `/api/internal/healer/worker` | Every minute |
| `/api/internal/management/tick`, `/api/internal/integrations/deliveries/worker`, `/api/internal/integrations/alerts/worker` | POST every minute |
| `/api/internal/linear/sync/worker`, `/api/internal/jira/sync/worker` | Every minute, if you use them |
| `/api/internal/slack/digest/worker` | Hourly |
| `/api/internal/artifacts/cleanup` | Daily; deletes artifacts after 30 days by default (`ARTIFACT_RETENTION_DAYS`) |

## Keep the deployment safe

- Run one app instance. Each workspace's organization memory has a single writer; never scale the service or share its volume.
- Keep the named volumes `pg_data`, `minio_data`, `memory_data` and `model_cache` across upgrades. Never run `docker compose down -v`. Volumes are not backups.
- The mounted Docker socket lets the app start browser containers for check runs, and gives it control of the host's Docker. Remove that mount if you do not run browser checks from Blop Cloud.
- Point your load balancer at `GET /api/healthz` (process up) and `GET /api/readyz` (database and bucket reachable; 503 otherwise).
- `BLOP_PLUGINS_SAFE_MODE=1` starts only sign-in, permissions, health and machine endpoints, to recover from a bad configuration.

## Upgrade Blop Cloud

There are no down migrations, so back up before every upgrade. Run these commands from the repository root on the server.

1. Pause your scheduler.
2. Stop the app, so nothing writes to memory:

   ```bash
   docker compose -f docker-compose.dokploy.yml stop web-sk
   ```

3. Back up Postgres and every file in the memory volume, including `-wal` and `-shm` files, in this same stopped window:

   ```bash
   B=backup/$(date +%F)
   mkdir -p -m 700 "$B"
   docker compose -f docker-compose.dokploy.yml exec -T postgres \
     pg_dump -U blop -d blop --format=custom > "$B/postgres.dump"
   docker compose -f docker-compose.dokploy.yml run --rm --no-deps -T \
     --entrypoint sh -v "$PWD/$B:/backup" web-sk \
     -c 'mkdir -p /backup/graphs && cp -a /var/lib/blop/memory/. /backup/graphs/'
   ```

   Never copy memory files while the app runs. Encrypt the backup and keep it off the server.
4. Check out the new release.
5. Start the new release; `--build` rebuilds the image first:

   ```bash
   docker compose -f docker-compose.dokploy.yml up -d --build web-sk
   ```

Then check `/api/readyz`, sign-in, a recent check run and organization memory, and resume the scheduler.

## Roll back to a backup

Restore into a new Compose project, so Postgres and memory start from fresh, empty volumes. Never copy restored files over existing memory files. Keep the old project's volumes until the restore checks out.

1. Check out the release the backup was taken with.
2. Stop the current project, keeping its volumes:

   ```bash
   docker compose -f docker-compose.dokploy.yml stop
   ```

3. Start only Postgres in a new project:

   ```bash
   docker compose -p blop-restore -f docker-compose.dokploy.yml up -d --wait postgres
   ```

4. Restore the database into its empty `blop` database. The dump references the `blop_analytics` role, which a fresh Postgres lacks, so create it first:

   ```bash
   B=backup/2026-11-03   # the backup to restore
   docker compose -p blop-restore -f docker-compose.dokploy.yml exec -T postgres \
     psql -U blop -d blop -c 'CREATE ROLE blop_analytics NOLOGIN'
   docker compose -p blop-restore -f docker-compose.dokploy.yml exec -T postgres \
     pg_restore -U blop -d blop --exit-on-error < "$B/postgres.dump"
   ```

5. Copy the memory files into the new, empty volume; the command refuses a volume that already holds files:

   ```bash
   B=backup/2026-11-03   # the same backup as step 4
   docker compose -p blop-restore -f docker-compose.dokploy.yml run --rm --no-deps -T \
     --entrypoint sh -v "$PWD/$B:/backup:ro" web-sk \
     -c 'test -z "$(ls -A /var/lib/blop/memory)" && cp -a /backup/graphs/. /var/lib/blop/memory/'
   ```

6. Start the restored project, and pass `-p blop-restore` to every later command:

   ```bash
   docker compose -p blop-restore -f docker-compose.dokploy.yml up -d --build
   ```

Artifacts in MinIO are not part of this backup, so the restored project's MinIO starts empty. For checksums and per-file validation, use the checkpointed backup in the [operator recipe](https://github.com/blop-oss/blop-app/blob/main/docs/agents/cloud/backup-and-restore.md), which uses the same `graphs/` layout and also covers `memory_migration_required`.

## Check it worked

```bash
curl -fsS https://blop.example.com/api/readyz
```

It returns 200. Then sign in and [connect an agent](/cloud/#connect-a-coding-agent).

## Next

- [Blop Cloud environment variables](/reference/cloud-environment/).
- [Blop Cloud](/cloud/).
