Self-host with Docker Compose
Any server with Docker, behind a TLS reverse proxy. One command starts Postgres, RustFS and the four services from the images published on each release.
AGPL-3.0 · Six containers · No per-seat fees
Requirements
- A server with DockerDocker and the Compose plugin. The stack runs six containers.
- A TLS reverse proxyCaddy, Traefik, nginx: anything that terminates TLS and forwards to the two ports below.
- A domain you controlA hostname for the web app. The api gets a second hostname under the same registrable domain, or a path on the web app’s host. The session cookie cannot cross to another domain.
This page uses api.example.com for the api and app.example.com for the web app.
Install
git clone https://github.com/croffasia/itsaplan.git
cd itsaplan
cp .env.example .env
docker compose up -dFill in the seven values below in .env before the last command. That starts the whole stack: Postgres, RustFS, api, worker, bot and web. The four services run the images published on each release, so nothing is built.
On a machine with Bun, the setup script generates the secrets for you. Run bun install && bun run setup, answer Generate env, and answer no when it offers to write the files: it prints .env and apps/web/.env for you to copy onto the server.
Environment variables
The stack refuses to start while one of these is missing:
- API_URLPublic origin of the api. Example:
https://api.example.com. - APP_URLPublic origin of the web app. Example:
https://app.example.com. - POSTGRES_PASSWORD
openssl rand -base64 32 - BETTER_AUTH_SECRET
openssl rand -base64 32 - APP_ENCRYPTION_KEY
openssl rand -base64 32. It encrypts the provider keys stored in the database. Change it later and those become unreadable. - S3_ACCESS_KEY_IDThe RustFS root user. Any name over three characters.
- S3_SECRET_ACCESS_KEY
openssl rand -base64 32. At least eight characters.
DATABASE_URL and S3_ENDPOINT are set by the compose file. VERSION pins one release instead of the newest.
Everything optional is documented in .env.example: legal document URLs, passkey and cookie settings, the outbound request policy, telemetry opt-out and worker tuning. Mail, AI provider keys, single sign-on and the Telegram bot token are not variables. You configure them in the interface after you sign in.
Reverse proxy
Terminate TLS at your proxy and forward each hostname to its container port:
- api.example.comthe api, port
3000 - app.example.comthe web app, port
3001
Both origins must be https and must match API_URL and APP_URL exactly. Passkeys bind to the web hostname taken from APP_URL. On a multi-label TLD, a deep subdomain, or an APP_URL on the apex domain, set COOKIE_DOMAIN as well, for example .example.com.
The api can also live on a path of the web host, with API_URL=https://app.example.com/api. The proxy then sends /api/* to port 3000 with the prefix removed, and everything else to port 3001. For MCP clients that sign in with OAuth, it also sends /.well-known/oauth-authorization-server*, /.well-known/oauth-protected-resource* and /.well-known/openid-configuration* to the api. No second hostname or certificate is needed.
First sign-in
The one-shot migrate service applies the migrations before the api starts, so there is no database step. Open APP_URL and register. The first account becomes the instance admin, and admin settings open from there: mail provider, AI providers, single sign-on, the Telegram bot token, and who may register.
Updates
git pull
docker compose pull
docker compose up -dgit pull updates the compose file; the services come from the registry. Changing API_URL or APP_URL afterwards needs only docker compose up -d.
The migrate service applies the migrations on every docker compose up -d, and api, worker and bot start only after it succeeds. Before it applies anything, it dumps the database into the db-backups volume and fails if that dump fails. Dumps are deleted after 30 days; BACKUP_RETENTION_DAYS changes the window, and SKIP_PRE_MIGRATION_BACKUP=1 upgrades without one. After the upgrade the app shows the instance owner where the dump is and what the migrations changed.
If you call the API from your own scripts or from an MCP client, read the release notes for the paths a release removed.
Building from source
docker compose up -d --buildThat builds every service from the checkout and runs those images. Nothing else changes, and the same command picks up local edits.
Troubleshooting
A container exits at once, with a message about a missing value.
One of the seven values above is still empty in .env. The stack refuses to start rather than run with a default secret.
Sign-in returns to the login page.
The api and the web app are not under one registrable domain, so the browser drops the session cookie. Put both hostnames under the same domain, or put the api on a path of the web host. On a multi-label TLD or an apex APP_URL, set COOKIE_DOMAIN.
api, worker and bot do not start after an upgrade.
The pre-migration dump failed, so the migrations were not applied. Read docker compose logs migrate, fix the disk or the permissions on the db-backups volume, and run docker compose up -d again.
Attachments do not upload.
The S3_* values are missing, which switches uploads off, or S3_FORCE_PATH_STYLE was set to false. RustFS needs the default.
Passkeys are refused.
The app is open on a hostname other than the one in APP_URL. Passkeys bind to that hostname. Open the app on it.
For anything else, read the logs: docker compose logs -f api, and the same for migrate, web, worker and bot.
Support
- BugsReport them in GitHub issues.
- Questions and setup helpAsk in GitHub discussions.
- This guideIt ships with the source as docs/self-hosting.md, which also covers single sign-on and SCIM provisioning.
Other ways to run it: Railway, Coolify, Kubernetes.