Self-hosting
Crewhall runs on one Linux server with Docker Compose. This page covers what you need, how to start the stack, the first-run setup wizard, optional services, and how to back up, restore and update. The examples use hub.example.com; replace it with your own domain.
Requirements
- A Linux server with at least 2 vCPU, 4 GB RAM and enough disk for your files.
- Docker Engine 24 or later with Compose v2 (
docker compose version). - DNS A/AAAA records that point to the server:
hub.example.comfor the app and the API.s3.example.comfor file uploads and downloads. Browsers connect to this address directly.- Optional:
docs.example.com(portal site),lk.example.com(meetings) andoffice.example.com(Office documents).
- Ports 80 and 443 open to the internet.
- An SMTP account for outgoing email. It is optional but recommended.
The stack
The stack is defined in docker/compose.yml. These services always run:
| Service | What it does |
|---|---|
caddy | TLS and routing. Sends /api, /ws, /mcp, /dav and the OAuth paths to the backend, /collab to collab, and the rest to web. |
web | The web app (static files). |
backend | The API, sign-in, real-time events (/ws) and the MCP server. |
worker | Background jobs: previews, link checks, push, email digests, nightly cleanup. |
collab | Real-time co-editing of pages and board cards. |
postgres | The database (PostgreSQL 17). |
seaweedfs | S3-compatible file storage. |
backend, worker and collab use the same image with different commands.
Prepare the environment file
git clone https://github.com/Milvasoft/crewhall.git crewhall && cd crewhall
cp docker/.env.example docker/.env
Edit docker/.env. These values matter most:
| Variable | Value |
|---|---|
HUB_DOMAIN, S3_DOMAIN | The two domains, for example hub.example.com and s3.example.com. |
ACME_EMAIL | Email address for certificate notices. |
POSTGRES_PASSWORD | A long random password. |
AUTH_SECRET | Output of openssl rand -base64 32. |
S3_ACCESS_KEY, S3_SECRET_KEY | A random key and secret. SeaweedFS starts with these. |
SMTP_URL, MAIL_FROM | For example smtp://user:[email protected]:587 and the sender address. |
SETUP_TOKEN | Can stay empty. A random token is then printed in the logs on first start. |
If AUTH_SECRET changes, all sessions end and stored 2FA secrets can't be read. Back up docker/.env and keep it out of version control.
If ports 80 and 443 are taken, or another reverse proxy sits in front, set HTTP_PORT and HTTPS_PORT, and set the public addresses HUB_URL, S3_PUBLIC_URL, COLLAB_PUBLIC_URL and PORTAL_PUBLIC_URL. Include the port when it is not 443, for example https://hub.example.com:8443.
Start the stack
docker compose -f docker/compose.yml --env-file docker/.env up -d --build
docker compose -f docker/compose.yml --env-file docker/.env ps
The first start builds the images, which takes a few minutes. The backend runs database migrations on start. The worker starts when the backend is healthy. If you use prebuilt images, set CREWHALL_REGISTRY and CREWHALL_VERSION and use pull instead of --build.
Check that it is ready:
curl -s https://hub.example.com/readyz
The response is {"status":"ready"}.
First-run setup wizard
- Get the setup token from the logs:
docker compose -f docker/compose.yml --env-file docker/.env logs backend | grep -i "setup" - Open
https://hub.example.com. The wizard asks for the setup token, then walks you through four steps: Organization (name, ID and default language), First admin (the owner account), Shared spaces and Invite the team. A system check shows whether the database, storage and email are connected. - The owner must turn on two-step verification (TOTP or a passkey) at first sign-in.
After setup the token no longer works. You can change the name, logo and colors later in Admin › Brand. See Administration.
DNS and TLS
Caddy gets certificates from Let's Encrypt on its own. It needs the DNS records to point at the server and ports 80 and 443 to be reachable. If a certificate is not issued, check DNS and the firewall, then read docker compose ... logs caddy.
If you put another proxy in front of Caddy, it must pass /ws and /collab as WebSockets (forward the Upgrade header). Otherwise chat and co-editing do not update live.
Storage
Files go to the bundled SeaweedFS by default. Uploads and downloads use presigned URLs, so the browser talks to S3_DOMAIN directly. Opening https://s3.example.com in a browser shows an XML error page; that is normal.
You can use any other S3-compatible service instead by changing the S3_* settings in the compose file. The bucket's CORS settings must allow your hub domain and expose the ETag header.
Email
With SMTP_URL set, Crewhall sends invitations, password resets and notifications. Without it, invitations are shown as links that you send yourself, and password reset and notification emails do not go out. Admins can also set a separate SMTP server in Admin › Notification delivery and send a test email there.
Optional profiles
Turn on extra services with --profile. Each one needs its own settings in docker/.env.
| Profile | Services | Use it for |
|---|---|---|
meet | LiveKit, egress, coturn, Valkey | Video meetings and calls |
portal | Next.js portal site | A public docs and blog site |
office | Euro-Office document server | Editing Word, Excel and PowerPoint files |
ai | Speech recognition server | Meeting notes and dictation |
Meetings (meet)
- Set
LK_DOMAIN,LIVEKIT_API_KEY,LIVEKIT_API_SECRET,TURN_USERandTURN_PASSWORD. SetMEET_EGRESS_ENABLED=truefor recordings. Egress needs 2-4 GB of RAM. - Open 7881/tcp, 50000-60000/udp, 3478/udp and 3478/tcp, and 49160-49200/udp.
- Start with
--profile meet.
The LiveKit DNS record must point straight at the server. If you use a CDN proxy, turn the proxy off for that record, because media uses UDP. Without keys, meetings are unavailable and everything else works. See Meetings.
Portal site (portal)
- In Portal › Settings, create the read key under the Next.js connection.
- Set
PORTAL_DOMAIN,PORTAL_CONTENT_TOKEN(the read key) andPORTAL_REVALIDATE_SECRET. - Start with
--profile portal. Published changes show on the site within seconds. See Portal.
Office documents (office)
The document server is a separate, unmodified service under its own license (AGPLv3). Crewhall only talks to it over the network.
- Set
OFFICE_DOMAINandOFFICE_JWT_SECRET(32 or more random characters). - Start with
--profile office. It needs at least 4 GB of memory. - In Admin › Integrations › Office, enter the server address
https://office.example.com, the internal addresshttp://officeand the same JWT secret. Select Test the connection, then turn on Office editing.
Speech recognition (ai)
The ai profile runs a speech recognition server with an OpenAI-compatible API. In Admin › AI › Meeting notes, set the speech recognition server to http://whisper:8000/v1 and choose a model. The model downloads on first use.
Backups, restore and update
The crewhall command in tools/crewhall backs up the database and files, restores them and updates the stack. Run it on the server from the repository root.
tools/crewhall/crewhall backup
tools/crewhall/crewhall list
tools/crewhall/crewhall restore <id>
tools/crewhall/crewhall export <id> > crewhall-backup.tar
tools/crewhall/crewhall import < crewhall-backup.tar
tools/crewhall/crewhall prune --keep 14
- Backups are stored in the
backupsDocker volume. Files are stored once by content, so each backup only adds what changed. - Copy backups off the server regularly with
export. restorestops the app services, replaces the database and files with the backup, and starts them again.docker/.envis not included by default. Add--with-configto include it encrypted; the passphrase is read fromCREWHALL_BACKUP_PASSPHRASE.- Restore with the same
AUTH_SECRETas the installation the backup came from. - Pass
--profile meetor--profile portalif you use them.
You can also turn on a nightly backup in Admin › Backups.
To update:
tools/crewhall/crewhall update 1.5.0 # prebuilt images
git pull && tools/crewhall/crewhall update --build # built from source
The command takes a backup first, then starts the new version. If the backend does not become healthy, it puts back the old images, the old .env and the backup.
Logs and traces can go to any OTLP receiver with OTEL_EXPORTER_OTLP_ENDPOINT. Without it, docker compose ... logs -f backend worker is enough.