Self-Host with Pre-Built Images
This is the canonical guide for the self-host kit (the dockerPublish folder). Clone the official repository, configure .env, create the MongoDB keyfile, and run the stack on your server.
Before you start
- Docker & Docker Compose installed on your server (or on your Mac for local testing).
- Hardware: 16GB+ RAM and 8+ CPU recommended for the full stack.
- Domain (optional): For production you’ll set URLs like
https://api.yourdomain.comin.env. For local testing you can usehttp://localhostand ports; see Test the stack on your Mac. - Optional services (you can add these later): Mail (SMTP) (e.g. Gmail), Turnstile (CAPTCHA), Google OAuth (Sign in with Google), LiveKit (voice/video calls). Each has a short “how to get credentials” section in its config guide.
Flow: Get kit → Configure → Run
- Get the kit — You should have a folder containing
docker-compose.yml,.env.example, and config dirs (traefik/,primary/,replica/,scripts/,opensearch-config/). No source code. - Configure — Copy
.env.exampleto.envand set: Docker registry/tag, your domain URLs, JWT secret, and all passwords (Postgres, Redis, Mongo, Neo4j, RabbitMQ, MinIO). The other two env files in the kit (.env.frontend.example,.env.admin.example) are for people who build the app images from source — ignore them. If you deploy with Traefik on a server, also edittraefik/dynamic/traefik-dynamic.yml: replace placeholder domains for the optional www→canonical redirect (or remove those routers if you do not usewww). See Make sure Traefik works when you deploy. Optional: R2, mail (SMTP), Turnstile, Google Login, LiveKit, MinIO bucket policies. See the configuration checklist and Three environment files. - Create MongoDB keyfile — Required. In the folder that contains
docker-compose.yml, run:openssl rand -base64 756 > mongo-keyfile && chmod 600 mongo-keyfile(see Step 3). - Run —
docker compose pull && docker compose up -d. The database app runs automatically (after Postgres and MongoDB are healthy), applies schema and seed, then exits. No manual migration step. To re-run after an upgrade when the publisher says so:docker compose run --rm db-migration.
Security-hardening file checklist (important)
Recent hardening updates added stricter CSP and Elasticsearch auth. When you upgrade to a new image tag, verify these files together (not just .env):
| File | Required action |
|---|---|
.env | Set all new keys from .env.example (especially ELASTICSEARCH_PASSWORD, URLs, storage provider). Existing .env is not auto-merged when you git pull. |
traefik/traefik.yml | Keep static entrypoints/provider config aligned with your deployment model (server vs local). |
traefik/dynamic/traefik-dynamic.yml | Replace placeholder domains (your-domain.com) and verify redirect/canonical host rules. |
Src/frontend/nginx.conf + Src/admin/nginx.conf | CSP is generated at container start from .env (runtime, no rebuild). Keep image tags updated and verify your runtime URL variables are correct. |
With current images, frontend/admin CSP is generated at startup from .env values (API_PUBLIC_URL, ADMIN_PUBLIC_URL, DOCS_PUBLIC_URL, Minio__PublicUrl, R2__PublicUrl, VITE_LIVEKIT_URL) so changing domain usually does not require rebuild.
Overview
- What you have: A folder with
docker-compose.yml,.env.example, and config files (traefik, scripts, primary, replica, opensearch-config). No source code. - What you need: Docker and Docker Compose, 16GB+ RAM and 8+ CPU recommended, and (for production) a domain. For local testing you can use
localhostand ports. - Default storage: MinIO runs inside the stack; no external storage required.
Step 1: Get the self-host kit
Where to get it: Clone the project repository and use the dockerPublish folder, or download a release package that includes this folder. That folder is the self-host kit — you do not need the rest of the source code.
You should have a single folder (e.g. dockerPublish) that contains everything needed to run the stack. Source and setup files are in the official repo: BellamyBook on GitHub.
The folder must include:
docker-compose.yml.env.exampleREADME.mdtraefik/(traefik.yml and dynamic/)primary/(Postgres init scripts)replica/pg_hba.confscripts/mongo-init-replica.shopensearch-config/(e.g. setup-log-retention-docker.sh and ilm policy)
You will create the mongo-keyfile in Step 3.
Step 2: Configure Environment (.env)
You use one env file: .env. The kit also includes .env.frontend.example and .env.admin.example — those are for people who build the app images from source; ignore them. See Three environment files.
2.1 Copy the example file
From the folder that contains docker-compose.yml:
cp .env.example .env
2.2 Edit .env – required values
Open .env in an editor and set at least the following.
Docker images (required)
| Variable | Example | Description |
|---|---|---|
DOCKER_REGISTRY | myuser | Container registry namespace used by your deployment. |
IMAGE_TAG | latest or v1.0.0 | Tag of the images the publisher told you to use. |
Domain and public URLs (required for production)
Set these to your domain so the app and CORS work correctly. If you use Traefik, these hostnames must match your DNS.
| Variable | Example | Description |
|---|---|---|
API_PUBLIC_URL | https://api.yourdomain.com | Full URL of the API. |
FRONTEND_PUBLIC_URL | https://app.yourdomain.com | Full URL of the main app. |
ADMIN_PUBLIC_URL | https://admin.yourdomain.com | Full URL of the admin panel. |
DOCS_PUBLIC_URL | https://docs.bellamybook.com | Docs URL (default points to central Bellamy Book docs). |
TRAEFIK_API_HOST | api.yourdomain.com | Host only (no https://) for API. |
TRAEFIK_FRONTEND_HOST | app.yourdomain.com | Host for frontend. |
TRAEFIK_ADMIN_HOST | admin.yourdomain.com | Host for admin. |
TRAEFIK_DOCS_HOST | docs.bellamybook.com | Host for docs (default). |
For local testing without a domain, you can use http://localhost:5000, http://localhost:8081, etc., and leave Traefik off; access the app via ports (see Step 6).
Optional: Frontend & Admin runtime (no rebuild)
You can set Turnstile, Google OAuth, LiveKit, and Web Push keys in .env; they are applied when the frontend and admin containers start. See Environment — Frontend & Admin runtime. Restart with docker compose up -d frontend admin after changing them.
Passwords and secrets (required)
Generate strong values and set:
POSTGRES_USER— Must bepostgres(notroot; root is for MongoDB).POSTGRES_PASSWORDREPLICATION_PASSWORDREDIS_PASSWORDMONGO_ROOT_PASSWORDRABBITMQ_DEFAULT_PASSRABBITMQ_ERLANG_COOKIENEO4J_AUTH(e.g.neo4j/YourSecurePassword)MINIO_ROOT_USERandMINIO_ROOT_PASSWORDJwtSettings__Secret(e.g.openssl rand -base64 64)Neo4j__Password(same as inNEO4J_AUTH)DatabaseAppAesKey(exactly 32 characters; e.g.openssl rand -base64 32and trim to 32 chars, or use a 32-character string — required for db-migration)
Replace any CHANGE_ME_* placeholders. Connection strings in .env that use ${POSTGRES_PASSWORD}, ${REDIS_PASSWORD}, etc. will pick these up automatically.
MinIO (default storage — required when self-hosting)
When self-hosting with the default setup, you must configure MinIO so the app can store and serve avatars, posts, and media. Set these in .env:
Storage__Provider=MinIOMinio__Endpoint=minio:9000Minio__PublicUrl— Required so the frontend can load media. UseMinio__PublicUrl=http://localhost:9000for local testing, orMinio__PublicUrl=https://${TRAEFIK_MINIO_HOST}when using Traefik (setTRAEFIK_MINIO_HOSTto your MinIO hostname, e.g.minio.yourdomain.com).Minio__AccessKey=${MINIO_ROOT_USER}Minio__SecretKey=${MINIO_ROOT_PASSWORD}
Also set MINIO_ROOT_USER and MINIO_ROOT_PASSWORD in the secrets section above. No external storage is required; MinIO runs inside the stack.
2.3 Optional but recommended
- JWT — Required for login; see JWT Configuration (generate secret, set issuer/audience).
- Mail (SMTP) — To send password reset, notifications, contact form: SMTP (Mail Server).
- Cloudflare Turnstile — CAPTCHA on login/register: Turnstile.
- Google Login — Sign in with Google: Google OAuth.
- LiveKit — Voice and video calls: LiveKit.
- R2 — Use Cloudflare R2 instead of MinIO: R2 Setup.
- MinIO security — If using MinIO, set bucket policies: Storage — MinIO bucket policies.
- GRAFANA_ADMIN_PASSWORD — If you use the monitoring profile.
Save .env. Do not commit it to version control.
2.4 Traefik dynamic file (production with Traefik)
The kit includes traefik/dynamic/traefik-dynamic.yml (loaded by Traefik’s file provider). It defines middlewares and optional HTTP/HTTPS routers that redirect www to your canonical frontend host for SEO. The file ships with placeholders (your-domain.com, www.your-domain.com).
You should configure this file before production traffic uses Traefik:
- If you use
www: Replace every placeholder with your real domain. Align hostnames withTRAEFIK_FRONTEND_HOST,TRAEFIK_FRONTEND_WWW_HOST, andFRONTEND_PUBLIC_URLin.env. Addwwwto CORS and Turnstile as in.env.example. - If you do not use
www: Remove or comment out theredirect-www-to-canonicalmiddleware and thefrontend-www-redirect/frontend-www-redirect-securerouters so Traefik does not define routes for a fake hostname.
Full context (DNS, .env, HTTPS): Make sure Traefik works when you deploy.
2.5 CSP/runtime config alignment (frontend + admin)
Security updates may tighten CSP and break UI if required domains are not allowlisted. Check these files as a bundle:
traefik/dynamic/traefik-dynamic.ymlSrc/frontend/nginx.confSrc/admin/nginx.conf
At minimum, CSP allowlists must include your real runtime endpoints:
- API + WebSocket:
https://api.yourdomain.com,wss://api.yourdomain.com - Storage domain: exactly one public storage hostname:
- R2:
https://r2.yourdomain.com - MinIO (self-host):
https://minio.yourdomain.com
- R2:
- Cloudflare challenge/insights
- Optional analytics/livekit domains if enabled
CSP_EXTRA_* can stay empty. That does not disable CSP. The image already applies a strict runtime baseline policy (default-src self, script/connect/img/media/frame allowlists from your core .env URLs). CSP_EXTRA_* is only for adding extra third-party domains when needed.
Example for a self-host domain test.com:
API_PUBLIC_URL=https://api.test.com
FRONTEND_PUBLIC_URL=https://test.com
ADMIN_PUBLIC_URL=https://admin.test.com
DOCS_PUBLIC_URL=https://docs.test.com
TRAEFIK_API_HOST=api.test.com
TRAEFIK_FRONTEND_HOST=test.com
TRAEFIK_ADMIN_HOST=admin.test.com
TRAEFIK_DOCS_HOST=docs.test.com
TRAEFIK_MINIO_HOST=minio.test.com
# choose ONE storage public domain
Storage__Provider=MinIO
Minio__PublicUrl=https://minio.test.com
# OR:
# Storage__Provider=R2
# R2__PublicUrl=https://r2.test.com
If you need extra CSP allowlists beyond default runtime generation, use .env variables:
CSP_EXTRA_SCRIPT_SRCCSP_EXTRA_SCRIPT_ELEM_SRCCSP_EXTRA_IMG_SRCCSP_EXTRA_CONNECT_SRCCSP_EXTRA_MEDIA_SRCCSP_EXTRA_FRAME_SRC
Each accepts space-separated tokens/URLs, for example:
CSP_EXTRA_CONNECT_SRC=https://sentry.your-domain.com wss://realtime.your-domain.com
2.6 If you self-host with MinIO (instead of R2)
For self-host, MinIO is the default and simplest storage option. Use this when you do not want Cloudflare R2.
- In
.env, set:
Storage__Provider=MinIO
MINIO_ROOT_USER=your_minio_user
MINIO_ROOT_PASSWORD=your_minio_password
Minio__Endpoint=minio:9000
- Set
Minio__PublicUrlbased on your setup:
- Traefik/domain (recommended for production):
Minio__PublicUrl=https://${TRAEFIK_MINIO_HOST} - Set
TRAEFIK_MINIO_HOSTto your real subdomain, e.g.minio.yourdomain.com - Point DNS
minio.yourdomain.comto your server - Keep CSP allowlist in frontend/admin nginx aligned with the same hostname
- Start stack:
docker compose up -d
- Verify MinIO:
- API endpoint:
http://localhost:9000(or your domain via Traefik) - MinIO Console:
http://localhost:9001 - Login with
MINIO_ROOT_USER/MINIO_ROOT_PASSWORD
- Ensure buckets/policies:
publicbucket should allow read access for media URLsprivatebucket for non-public objects
The minio-init service creates these buckets on first run. For security hardening and policy details, see Storage — MinIO bucket policies.
Step 3: Create MongoDB keyfile (required)
The stack uses a MongoDB replica set (used by the API and workers). MongoDB requires a keyfile for replica set authentication. You must create this file before your first docker compose up. The compose file mounts it into the mongo-keyfile-init service, which copies it into a volume used by the MongoDB container.
Where: In the same directory as docker-compose.yml.
Commands (run once):
openssl rand -base64 756 > mongo-keyfile
chmod 600 mongo-keyfile
- File name: Must be exactly
mongo-keyfile(the compose file references./mongo-keyfile). - Permissions:
chmod 600(orchmod 400). MongoDB accepts either; the keyfile must not be readable by others. - Security: Do not commit
mongo-keyfileto git or share it; treat it as a secret.
If you skip this step, the mongo-keyfile-init container will fail with "Source keyfile /tmp/keyfile not found" and MongoDB will not start.
Step 4: Pull and start the stack
From the folder that contains docker-compose.yml (e.g. dockerPublish):
docker compose pull
docker compose up -d
This pulls all images and starts Traefik and all services. Use this on a server (production). For local testing on your Mac or PC (no domain, no port 80/443), use the Test the stack on your Mac guide instead — it uses a different command with docker-compose.local.yml. The first run can take several minutes (databases, Kafka, Elasticsearch, etc.).
Step 5: Database app (runs automatically)
The stack includes a database application service (db-migration) that runs automatically when you start the stack: it waits for Postgres and MongoDB to be healthy, applies schema and seed data, then exits. No manual migration step or EF Core commands are required.
To re-run the database app (e.g. after upgrading to a new image tag when the publisher instructs):
docker compose run --rm db-migration
Default admin account (if seeded by the image): Email [email protected], Password Admin123@. Change this password immediately after first login (Admin Panel → Profile or account settings). Check the publisher’s README for exact defaults.
Step 6: Access the application
- With Traefik (and DNS): Use the hostnames you set in
.env(e.g.TRAEFIK_FRONTEND_HOST,TRAEFIK_API_HOST). Ensure DNS for these hostnames points to your server. To avoid 404s and wrong routing, follow Make sure Traefik works when you deploy. - Without Traefik (e.g. local or IP): When using the local override (
docker-compose.local.yml), an api-gateway exposes one port for API + WebSocket (SignalR). Default port is 5000; override withAPI_PORTandAPI_PUBLIC_URLin.envif needed.- Frontend: http://localhost:8081
- Admin: http://localhost:8084
- API + WebSocket (SignalR): http://localhost:5000 (or
http://localhost:${API_PORT}if you setAPI_PORT) - MinIO (S3): http://localhost:9000 — set
Minio__PublicUrl=http://localhost:9000so the frontend can load media. - MinIO console: http://localhost:9001
For local testing without a server, use the local override and see Test the stack on your Mac.
Docs URL is set via DOCS_PUBLIC_URL / VITE_DOCS_URL; by default it points to docs.bellamybook.com.
Step 7: Updating to a new version
When the publisher releases a new tag (e.g. v1.0.1):
- In
.env, setIMAGE_TAG=v1.0.1(or the tag they provide). - Run:
docker compose pull
docker compose up -d
Re-run the database app only if the publisher instructs (e.g. after upgrading): docker compose run --rm db-migration.
Configuration reference
- Single place for config: All runtime configuration is in
.env. Frontend and admin containers readAPI_PUBLIC_URL,FRONTEND_PUBLIC_URL,ADMIN_PUBLIC_URL,Minio__PublicUrl, etc. at startup and generate/config.js, so one image set works for any domain. - Full list of variables: See Environment Configuration and the comments in
.env.example. - Storage (MinIO vs R2): Default is MinIO (included in the stack). To use Cloudflare R2, set
Storage__Provider=R2and the R2 variables in.env; see Storage Configuration. - Monitoring: Optional stack (Prometheus, Grafana, etc.) is under a profile; see the publisher’s README or
docker compose --profile monitoring up -d.
Troubleshooting
| Issue | What to check |
|---|---|
| Containers exit or unhealthy | Run docker compose logs api (or the service name); check .env for wrong passwords or missing variables. |
| Frontend shows wrong API URL | Ensure API_PUBLIC_URL, FRONTEND_PUBLIC_URL, and ADMIN_PUBLIC_URL are set correctly in .env; they are injected at container start. |
www redirects to the wrong domain or still uses placeholders | Edit traefik/dynamic/traefik-dynamic.yml and replace your-domain.com / www.your-domain.com, or remove the www redirect routers if you do not use www. See Traefik when you deploy. |
| db-migration (database app) fails | Ensure Postgres and MongoDB are healthy: docker compose ps. Check ConnectionStrings__* and Mongo credentials in .env. |
| “mongo-keyfile not found” | Create it in the folder that contains docker-compose.yml: openssl rand -base64 756 > mongo-keyfile && chmod 600 mongo-keyfile. |
| Port already in use | Free the port or set API_PORT (e.g. 5002) and API_PUBLIC_URL=http://localhost:5002 in .env for the API gateway (local override). For other services, change the host port in docker-compose.yml if needed. |
| Postgres "MigrationDb does not exist" / replication errors | Postgres init runs only when the primary volume is empty. Do a clean run: docker compose down -v then up -d (see Local testing for the exact commands with the local override). |
For more, see the main Troubleshooting guide.
Next steps
- Test the stack on your Mac – Run the same stack locally before deploying (no server needed).
- Make sure Traefik works when you deploy – Hostnames, DNS, ports, and HTTPS checklist.
- Environment Configuration – Full list of variables and sections.
- Storage Configuration – MinIO and R2.
- Backups – Backup strategy and retention.