Environment Configuration
Configure environment variables for Bellamy Book. After you clone and run (see Self-Host with Pre-Built Images), use this page and the linked guides to set everything in .env.
Configuration checklist
| Step | What to set | Guide |
|---|---|---|
| 1 | Docker images (registry + tag), domain URLs | Below (your .env) |
| 1b | traefik/dynamic/traefik-dynamic.yml (production with Traefik on a server) | Traefik when you deploy |
| 2 | JWT secret (required for login) | JWT Configuration |
| 3 | Database & infra passwords (Postgres, Redis, Mongo, Neo4j, RabbitMQ, MinIO) | Database, below |
| 4 | Storage: MinIO (default) or R2 | Storage, R2 Setup |
| 5 | MinIO security (bucket policies) if using MinIO | Storage — MinIO bucket policies |
| 6 | Mail (SMTP) to send email | SMTP (Mail Server) |
| 7 | Cloudflare Turnstile (CAPTCHA) | Turnstile |
| 8 | Google Login (Sign in with Google) | Google OAuth |
| 9 | LiveKit (voice/video calls) | LiveKit |
| 10 | CSP allowlist domains (frontend/admin nginx) for API, WebSocket, storage (R2/MinIO), GA, Turnstile | Self-Host with Pre-Built Images |
Three environment files
The self-host kit includes three env templates. You only use one when self-hosting with pre-built images.
| File | Who uses it | Purpose |
|---|---|---|
.env.example → .env | You (self-hoster) | Runtime config: copy to .env and set domain, secrets, DB passwords. This is the only file you need. |
.env.frontend.example | Image builders only | Used when building the frontend Docker image from source. Ignore when using pre-built images. |
.env.admin.example | Image builders only | Used when building the admin Docker image from source. Ignore when using pre-built images. |
Create and edit only .env (from .env.example). Schema and seed run automatically when you start the stack (docker compose up -d).
Your .env (self-host with pre-built images)
All configuration is in a single .env file in the folder that contains docker-compose.yml. Follow these steps.
Step 1: Copy the example
From the folder that contains docker-compose.yml:
cp .env.example .env
Step 2: Set Docker image source
| Variable | Example | Description |
|---|---|---|
DOCKER_REGISTRY | myuser | Container registry namespace used by your deployment. |
IMAGE_TAG | latest or v1.0.0 | Image tag provided by the publisher. |
Step 3: Set your domain and public URLs
Use your domain so CORS, redirects, and links work. If you use Traefik, these hostnames must match your DNS.
| Variable | Example |
|---|---|
API_PUBLIC_URL | https://api.yourdomain.com |
FRONTEND_PUBLIC_URL | https://app.yourdomain.com |
ADMIN_PUBLIC_URL | https://admin.yourdomain.com |
DOCS_PUBLIC_URL | https://docs.bellamybook.com (default; central docs) |
TRAEFIK_API_HOST | api.yourdomain.com |
TRAEFIK_FRONTEND_HOST | app.yourdomain.com |
TRAEFIK_FRONTEND_WWW_HOST | www.yourdomain.com (optional; for SEO: www redirects 301 to main domain) |
TRAEFIK_ADMIN_HOST | admin.yourdomain.com |
TRAEFIK_DOCS_HOST | docs.bellamybook.com |
SEO (www): To support www.yourdomain.com and avoid duplicate content, set TRAEFIK_FRONTEND_WWW_HOST=www.yourdomain.com. You must also edit traefik/dynamic/traefik-dynamic.yml and replace the placeholder domain in the www→canonical redirect with yours (or remove those routers if you do not use www). Add www to CORS and Turnstile in .env (see .env.example). Point DNS for www to the same server. See Traefik when you deploy.
For local testing without a domain, you can use http://localhost:5000, http://localhost:8081, etc.
Step 4: Set all passwords and secrets
Replace every CHANGE_ME_* placeholder. At minimum set:
- PostgreSQL:
POSTGRES_USER=postgres(required; do not useroot),POSTGRES_PASSWORD,REPLICATION_PASSWORD - Redis:
REDIS_PASSWORD - MongoDB:
MONGO_ROOT_PASSWORD - RabbitMQ:
RABBITMQ_DEFAULT_PASS,RABBITMQ_ERLANG_COOKIE - Neo4j:
NEO4J_AUTH(e.g.neo4j/YourSecurePassword); setNeo4j__Passwordto the same password - MinIO:
MINIO_ROOT_USER,MINIO_ROOT_PASSWORD(and in app config:Minio__Endpoint=minio:9000,Minio__PublicUrlfor browser access) - JWT:
JwtSettings__Secret(e.g. generate withopenssl rand -base64 64) - Database app encryption:
DatabaseAppAesKey(exactly 32 characters; required for db-migration — e.g. generate withopenssl rand -base64 32and use the first 32 characters) - Elasticsearch auth:
ELASTICSEARCH_PASSWORD(required withxpack.security.enabled=true)
Avoid
$in.envsecrets when possible. Docker Compose treats$VARas interpolation unless escaped and this can silently break passwords/tokens.
Connection strings in .env that use ${POSTGRES_PASSWORD}, ${REDIS_PASSWORD}, etc. will pick these up automatically.
Step 5: MinIO (default storage)
Default CDN/storage is MinIO inside the stack. Ensure:
Storage__Provider=MinIOMinio__Endpoint=minio:9000Minio__PublicUrl= URL users’ browsers use (e.g.http://localhost:9000orhttps://cdn.yourdomain.com)- MinIO access/secret keys match the MinIO container (
MINIO_ROOT_USER,MINIO_ROOT_PASSWORD)
If you use R2 (Storage__Provider=R2), ensure frontend/admin CSP allowlists include your R2 public domain in:
img-src(avatars/thumbnails)media-src(video/audio playback)connect-src(media fetch/resolution checks)
Step 6: Optional services
-
SMTP: Set
Smtp__Server,Smtp__Port,Smtp__User,Smtp__Pass,Smtp__DefaultFromfor email. -
Turnstile / OAuth / Web Push / LiveKit: See comments in
.env.example; set only if you use those features. Frontend and admin read these at runtime — set them in.envand they are applied when the containers start (no image rebuild). -
Monitoring: If using the monitoring profile, set
GRAFANA_ADMIN_PASSWORDand related Traefik hosts. -
Google Analytics 4 (GA4) – runtime: Optional page view analytics for the frontend SPA:
Variable Applies to Description GA4_FRONTEND_MEASUREMENT_IDbellamybook-frontendcontainerYour GA4 web data stream Measurement ID for the app (format: G-XXXXXXXXXX). If left empty, the image runs without GA tracking.The docs site at
https://docs.bellamybook.comis hosted by the publisher. You do not need to set a docs GA ID in your.env.
Build‑time flag (VITE_ENABLE_ANALYTICS)
For image builders (when building the frontend from source), there is an additional Vite build-time flag:
VITE_ENABLE_ANALYTICS=true
- When
true, the frontend bundle enables the internalanalyticsfeature flag so the app is allowed to send GA4 events. - When
false, analytics is disabled in the built bundle even ifGA4_FRONTEND_MEASUREMENT_IDis set at runtime.
When using pre-built images, you do not set VITE_ENABLE_ANALYTICS; you only control runtime analytics via GA4_FRONTEND_MEASUREMENT_ID in your .env.
Frontend & Admin runtime (optional – no rebuild)
With pre-built images, you can set these in .env and they are applied when the frontend and admin containers start. Restart the containers after editing .env (e.g. docker compose up -d frontend admin).
| Variable | Applies to | Description |
|---|---|---|
VITE_TURNSTILE_SITE_KEY | Frontend, Admin | Cloudflare Turnstile site key (public). Get it from Cloudflare Turnstile. |
VITE_TURNSTILE_THEME | Frontend, Admin | light or dark (optional). |
VITE_TURNSTILE_SIZE | Frontend, Admin | normal or compact (optional). |
VITE_TURNSTILE_ENABLED_ENDPOINTS | Frontend, Admin | Comma-separated, e.g. login,register,forgot-password,reset-password,contact (optional). |
VITE_GOOGLE_CLIENT_ID | Frontend only | Google OAuth client ID for “Sign in with Google”. |
VITE_LIVEKIT_URL | Frontend only | LiveKit server URL for voice/video calls (e.g. wss://your-livekit.example.com). |
VITE_VAPID_PUBLIC_KEY | Frontend only | Web Push VAPID public key; must match backend WebPush:PublicKey. |
CSP_EXTRA_* | Frontend, Admin | Optional space-separated extra CSP allowlist tokens/URLs. Leave empty in most setups. |
Leave any variable empty to use the value baked into the image (or disable the feature). See Turnstile, Google OAuth, LiveKit.
CSP_EXTRA_* empty does not mean CSP is disabled or weak. Default runtime CSP is still strict and built from your main .env URLs (API_PUBLIC_URL, FRONTEND_PUBLIC_URL, ADMIN_PUBLIC_URL, DOCS_PUBLIC_URL, storage public URL). Use CSP_EXTRA_* only when adding extra external domains (for example Sentry or custom CDN).
Default CSP policy at a glance (frontend nginx + Traefik middleware):
| Directive | Default | Why |
|---|---|---|
script-src / script-src-elem | 'self' + Cloudflare Turnstile/Insights + GTM + JSON-LD sha256 hash | No 'unsafe-eval', no 'unsafe-inline'. Tight to keep XSS surface near zero. |
connect-src | 'self' + your API / Admin / Docs / WebSocket / LiveKit / analytics | Tight allowlist; wss:// derived from your *_PUBLIC_URL. |
frame-src | Cloudflare Turnstile + YouTube (nocookie) + Vimeo + TikTok + X (Twitter) + Spotify + Twitch (player + clips) + SoundCloud | Backend LinkPreviewService auto-detects these providers and the frontend renders direct iframes (no third-party JS loaded). Add more hosts via CSP_EXTRA_FRAME_SRC if you extend the backend with another provider. |
img-src | 'self' data: blob: https: + storage / GA / ytimg / vimeocdn | The https: scheme is intentionally allowed so Open Graph thumbnails of any website (link previews) and external OAuth avatars load. Images cannot execute JS — the only realistic risk is third-party tracking pixels; same trade-off Facebook/Twitter/Mastodon accept for user-submitted links. |
media-src | 'self' blob: https: + storage | Allows HTML5 audio/video to play direct external media URLs without breaking link previews. |
object-src | 'none' | Blocks legacy Flash/plugin embeds entirely. |
style-src | 'self' 'unsafe-inline' | React uses inline style={…} attributes; cannot remove 'unsafe-inline' without rewriting components. |
frame-ancestors | 'self' | Site cannot be iframed by other origins (anti-clickjacking). |
If a deployment must satisfy a stricter compliance regime (e.g. PCI / no scheme wildcards on img-src), the recommended path is to extend the backend to mirror Open Graph images into your own storage (R2 / MinIO) at link-preview time, then remove https: from img-src in Src/frontend/docker-entrypoint.sh and the Traefik middleware. Out of the box we ship the practical default so user link sharing works on first deploy.
Adding a new embed provider (e.g. Reddit, Mixcloud, Loom):
- Backend
Src/backend/API/Services/LinkPreviewService.cs— add detection inTryDetectDirectEmbed(or a newTry{Provider}OEmbedAsyncif the provider returns a working iframe via oEmbed) and emit a uniqueEmbedType+VideoIdencoding. - Frontend
Src/frontend/src/components/cards/PostCard.jsx— extendgetLinkEmbedIframeConfigto map the newEmbedTypeto its iframe URL + aspect ratio. - CSP
Src/frontend/docker-entrypoint.sh(FRAME_SRC=) anddockerProd|dockerPublish/traefik/dynamic/traefik-dynamic.yml(frame-src) — append the provider host(s). - For one-off self-host without a code change, set
CSP_EXTRA_FRAME_SRC="https://www.example.com https://embed.example.com"in.env— but the embed will only render if the backend already produces it.
Do not commit .env to version control. Full walkthrough: Self-Host with Pre-Built Images.
Backend Environment Variables (Reference)
Database Configuration
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DATABASE=bellamybook
POSTGRES_USER=facebookuser
POSTGRES_PASSWORD=your-password
MONGODB_URI=mongodb://bellamyuser:password@localhost:27017/bellamybook
REDIS_HOST=localhost
REDIS_PORT=6379
JWT Configuration
JWT_SECRET_KEY=your-very-secure-secret-key-minimum-32-characters
JWT_ISSUER=BellamyBook
JWT_AUDIENCE=BellamyBook
JWT_EXPIRATION_MINUTES=60
JWT_REFRESH_EXPIRATION_DAYS=7
Storage Configuration
Local Storage
STORAGE_PROVIDER=Local
STORAGE_LOCAL_PATH=/var/www/uploads
AWS S3
STORAGE_PROVIDER=S3
S3_BUCKET=your-bucket-name
S3_REGION=us-east-1
S3_ACCESS_KEY=your-access-key
S3_SECRET_KEY=your-secret-key
Email Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_PASSWORD=your-app-password
Application Settings
ASPNETCORE_ENVIRONMENT=Production
ASPNETCORE_URLS=http://localhost:5000
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001
Frontend Environment Variables
Create .env.production:
VITE_API_URL=https://api.your-domain.com
VITE_WS_URL=wss://api.your-domain.com
VITE_APP_NAME=Bellamy Book
VITE_ENABLE_ANALYTICS=true
Docker Environment
For Docker Compose, use .env file:
# Database
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your-secure-password
POSTGRES_DB=bellamybook
# MongoDB
MONGO_INITDB_ROOT_USERNAME=admin
MONGO_INITDB_ROOT_PASSWORD=your-password
# Redis
REDIS_PASSWORD=your-redis-password
# Application
JWT_SECRET_KEY=your-jwt-secret
STORAGE_PROVIDER=Local
Security Best Practices
- Never commit secrets: Use environment variables or secret management
- Use strong passwords: Generate secure random passwords
- Rotate secrets regularly: Change JWT secrets periodically
- Limit access: Restrict database access to application servers only
- Use HTTPS: Always use SSL/TLS in production