Docker (self-host)
Copy page
The server image (Dockerfile.server) runs the full Plan Desk server on a host you control — the self-host topology. You bring the database; there is no dependency on asyncdot infrastructure and no GitHub app required.
Quickstart (compose)
Section titled “Quickstart (compose)”export PLANDESK_AUTH_PASSWORD='choose-a-strong-password'docker compose -f docker-compose.hosted.yml up --buildOpen http://127.0.0.1:7526.
By default this uses a local SQLite file on a Docker volume (migrated automatically at boot). For a durable database, point it at your own libSQL/Turso database and apply the schema once:
export PLANDESK_DB_URL='libsql://your-db.turso.io'export PLANDESK_DB_TOKEN='<libSQL auth token>'
# Apply the schema once (you own this database — REQ-8)docker compose -f docker-compose.hosted.yml run --rm plandesk \ migrate --db "$PLANDESK_DB_URL" --db-token "$PLANDESK_DB_TOKEN"
docker compose -f docker-compose.hosted.yml up --buildBuild the image directly
Section titled “Build the image directly”docker build -f Dockerfile.server -t plandesk-server .docker run -p 7526:7526 \ -e PLANDESK_DB_URL='libsql://your-db.turso.io' \ -e PLANDESK_DB_TOKEN='<token>' \ -e PLANDESK_AUTH_PASSWORD='<password>' \ plandesk-serverConfiguration
Section titled “Configuration”Everything the server needs can be set by environment or by a plandesk.server.json file mounted at /data/plandesk.server.json (env always wins). Inspect the resolved config and its source with plandesk doctor — secret values are redacted:
docker compose -f docker-compose.hosted.yml run --rm plandesk doctorSecuring the server
Section titled “Securing the server”The bind address is the trust boundary
Section titled “The bind address is the trust boundary”Plan Desk treats a loopback bind — 127.0.0.1, ::1 or localhost — as proof that only this
machine can reach it. On a loopback bind every request is the org owner, with no login at all. That
is what makes a local board zero-setup: you run plandesk serve, open the browser, and it works.
On any other bind address that trust is gone and better-auth does the authenticating.
So do not bind loopback and put a reverse proxy in front of it. That shape is normally good practice — keep the app port off the network, terminate TLS at nginx or Caddy — but here it defeats the model: the server still believes only this machine can reach it, while the proxy hands the internet an owner session. There is no error and nothing in the UI looks wrong; the board simply has no access control.
Two safe shapes:
| Bind | Who can reach it | Authentication | |
|---|---|---|---|
| Local board | 127.0.0.1 | this machine only, no proxy | none needed — loopback is the boundary |
| Served board | 0.0.0.0 | proxy or network | better-auth, and PLANDESK_AUTH_PASSWORD |
The compose file already does the right thing: PLANDESK_HOST defaults to 0.0.0.0, and the
container’s network isolation — not a loopback bind — is what keeps the port private.
Other controls
Section titled “Other controls”PLANDESK_AUTH_PASSWORDenables HTTP basic-auth on the UI and REST API. Set it for any host reachable beyond your own machine. Without it the server is open — fine on a trusted LAN, not for a public host.- TLS — front the container with nginx/Caddy for HTTPS. The server binds
0.0.0.0inside the container; do the TLS termination at your reverse proxy. - GitHub sign-in is optional — omit the GitHub env/keys and the server runs with token auth only (REQ-20).
Data persistence
Section titled “Data persistence”- Local file topology (no
PLANDESK_DB_URL): state lives in theplandesk-datavolume (/datain the container). Back up the volume. - Remote DB topology (
PLANDESK_DB_URLset): state lives in your database. Back that up. The volume then only holdsplandesk.server.json.
Environment variables
Section titled “Environment variables”| Variable | Default | Purpose |
|---|---|---|
PLANDESK_DB_URL | (unset → local file) | libSQL/Turso URL for the server’s database |
PLANDESK_DB_TOKEN | (unset) | Auth token for a remote libSQL DB (secret) |
PLANDESK_HOST | 0.0.0.0 | Bind address |
PLANDESK_PORT | 7526 | Bind port |
PLANDESK_AUTH_PASSWORD | (unset) | HTTP basic-auth password (secret) |
PLANDESK_STORAGE | local | local (blobs in DB) or s3 |
PLANDESK_S3_* | (unset) | S3 credentials when PLANDESK_STORAGE=s3 |
PLANDESK_GITHUB_CLIENT_ID / _SECRET / _CALLBACK_URL | (unset) | GitHub OAuth (all-or-nothing; omit for no GitHub sign-in) |
- Deployment topologies — local vs self-host vs free-hosted, and who runs migrations.
- Server configuration — the full
plandesk.server.jsonreference. - Cloudflare Workers — edge alternative (Turso + better-auth + R2).
- Collaboration & sharing — guest portal and moderated submissions on the same API (no separate sync-server).