Appearance
Installing Crosdesk
Crosdesk ships as one Docker image that holds the whole product — the API, the web app and this manual — and needs one PostgreSQL database beside it. This page sets up both with Docker Compose on one server, behind HTTPS.
It is written for whoever runs the installation (the instance operator). Nothing here is needed by the people who use Crosdesk day to day.
What you need
| A server | Linux on x86-64 (linux/amd64). The image is built for that architecture only; on an ARM machine it runs under emulation, slowly. |
| Docker | Docker Engine with the Compose plugin (docker compose). |
| A domain and HTTPS | Crosdesk refuses to run in production without HTTPS: its session cookie is marked Secure. Put a reverse proxy in front — Caddy, nginx, Traefik or your load balancer — to terminate TLS. |
| A licence key | From Buddhyantra — write to hello@buddhyantra.com. See The licence. You can install without one and add it afterwards. |
| A mail server | SMTP, or a Mailtrap account where the host blocks SMTP, for invitations and sign-in codes. Can be added later: meanwhile the operator console copies an invitation link to send by hand, and can let workspace admins do the same. |
| Optional: S3 storage | Files can live on the server's disk (a Docker volume) or in an S3-compatible bucket — AWS S3, MinIO, Cloudflare R2. A bucket is required if you run more than one copy of the app. |
The Compose file includes PostgreSQL 16. To use a database you already run instead, see Without Compose; any PostgreSQL 14 or newer works.
1. Get the Compose file
Make a directory for the installation and put docker-compose.app.yml from the Crosdesk repository in it — Buddhyantra can also send it, at hello@buddhyantra.com.
sh
mkdir crosdesk && cd crosdesk
# copy docker-compose.app.yml hereIt runs two containers: api (the published image, gsmithun4/cros-desk) and postgres, with two volumes — the database, and uploaded files.
2. Write the .env file
Beside the Compose file, create .env:
sh
# The address people will use, exactly as typed in a browser.
PUBLIC_URL=https://desk.example.com
# Encrypts the licence, mail password and storage credentials in the database.
# Generate once: openssl rand -base64 32 — and keep a copy with your backups.
CONFIG_ENCRYPTION_KEY=paste-the-generated-value-here
# The database's own password. Not published outside the Compose network.
POSTGRES_PASSWORD=another-strong-password
# Recommended while the address is public and unclaimed — see First boot.
BOOTSTRAP_TOKEN=a-long-random-valueRuntime settings for the Compose deployment:
| Variable | Required | What it is |
|---|---|---|
PUBLIC_URL | Yes | The installation's address. Every link Crosdesk sends — invitations, password resets, sign-in callbacks — starts with it. The app refuses to start without it. |
CONFIG_ENCRYPTION_KEY | Yes | Encrypts secret settings at rest. Without it the licence cannot be saved, so nothing works. Changing it later makes the stored secrets unreadable — rotate it with the steps in Upgrading and backups. |
POSTGRES_PASSWORD | The database password. Defaults to postgres — change it. POSTGRES_USER and POSTGRES_DB can be set too. | |
BOOTSTRAP_TOKEN | Asked for when claiming the operator console, so finding the address first is not enough to take it. | |
CROSDESK_TAG | The image version to run. Default latest; pin a version for predictable upgrades. | |
PORT | The port on the server. Default 3000. | |
TRUST_PROXY | How many proxies sit in front. Default 1, right for one reverse proxy. Without it, every visitor looks like the proxy, and the sign-in rate limit is shared by everyone. | |
COOKIE_SECURE | Leave at the default, true. |
Everything else — the licence, mail, storage, sign-in providers, session length — is set in the operator console after the first boot, not here. Variables such as LICENSE_KEY, SMTP_HOST or S3_BUCKET are ignored: there is one place for each setting, and it is the console.
3. Start it
sh
docker compose -f docker-compose.app.yml up -d
docker compose -f docker-compose.app.yml logs -f apiThe first boot applies the database migrations before starting the API. The startup log includes:
▶ node v22.x · PORT=3000 · NODE_ENV=production
▶ Applying migrations...
55 migration(s) applied, 0 already up to date.
▶ Starting API...From there the app logs one JSON line per event. The three to look for are No licence is set (expected until step 2 of First boot), Storage: local files in /app/storage (writable) and Crosdesk API listening on :3000.
Later starts reuse the existing schema and accounts. Claim the operator console in the browser if it has not been claimed yet.
If it stops instead, the log says why — for example Refusing to start: PUBLIC_URL must be the address of this installation. Fix it and start again; the schema and workspace made before the refusal are kept and reused. See When it will not start.
4. Put HTTPS in front
Point your domain at the server and proxy it to port 3000. With Caddy, which fetches a certificate by itself, the whole configuration is:
desk.example.com {
reverse_proxy localhost:3000
}Whatever the proxy, it must pass on the original Host header and X-Forwarded-Proto: https. Crosdesk checks that every form is submitted from its own address, and it learns that address from those two headers; TRUST_PROXY (default 1) tells it to believe them. A proxy that drops them makes every sign-in and every save fail with Cross-origin request rejected. Caddy passes both by default. nginx does not pass Host unless told to:
nginx
server {
listen 443 ssl;
server_name desk.example.com;
# ssl_certificate … ssl_certificate_key …
client_max_body_size 100m; # attachments and screen recordings come through here
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}Then open PUBLIC_URL in a browser. Two sign-ins exist from here:
/login— workspace accounts, once their invitation has been accepted./super-admin— the operator console, claimed on your first visit. Do this next: First boot and the operator console.
Until a licence is installed, the workspace answers every request with an error that says so — that is expected. The operator console works without one.
Without Compose
The image runs anywhere that runs containers. Configure its runtime connection settings:
| Variable | Value |
|---|---|
DATABASE_URL | postgresql://user:password@host:5432/crosdesk. The database is created on first start if the role may create databases; otherwise create it empty first. |
PUBLIC_URL | The installation's address. |
CONFIG_ENCRYPTION_KEY | As above. |
COOKIE_SECURE | true. |
TRUST_PROXY | The number of proxies in front, usually 1 — see Put HTTPS in front. |
The image already sets NODE_ENV=production, SERVE_FRONT_END=true and PORT=3000, runs as an unprivileged user, and has a health check on /api/health. Mount a volume at /app/storage, or use S3 storage, so uploads survive the container being replaced.
A Railway deployment is described step by step in DEPLOY-RAILWAY.md in the repository.
When it will not start
The reason is the first error in the api log.
| The log says | Do this |
|---|---|
PUBLIC_URL must be the address of this installation | Set PUBLIC_URL in .env. |
COOKIE_SECURE must be true in production | Remove any COOKIE_SECURE=false. Crosdesk is only served over HTTPS. |
DATABASE_URL is not set | Set it, or use the Compose file, which builds it. |
Migrations failed. The API has not been started. | Usually the database is unreachable or the role lacks rights. The lines above it name the problem. |
The deployed image is older than the database | You started an older version against a database a newer one has upgraded. Run the newer image. See Upgrading. |
CONFIG_ENCRYPTION_KEY is not set (a warning, the app still starts) | Set it and restart. Until then the licence and other secrets cannot be saved. |
And once it is running:
| What you see | Do this |
|---|---|
| Every sign-in or save fails with Cross-origin request rejected | The proxy is not passing Host and X-Forwarded-Proto, or TRUST_PROXY is unset. See Put HTTPS in front. |
Every workspace screen fails, and /api/health is fine | No valid licence yet — see The licence. |