Skip to main content

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

StepWhat to setGuide
1Docker images (registry + tag), domain URLsBelow (your .env)
1btraefik/dynamic/traefik-dynamic.yml (production with Traefik on a server)Traefik when you deploy
2JWT secret (required for login)JWT Configuration
3Database & infra passwords (Postgres, Redis, Mongo, Neo4j, RabbitMQ, MinIO)Database, below
4Storage: MinIO (default) or R2Storage, R2 Setup
5MinIO security (bucket policies) if using MinIOStorage — MinIO bucket policies
6Mail (SMTP) to send emailSMTP (Mail Server)
7Cloudflare Turnstile (CAPTCHA)Turnstile
8Google Login (Sign in with Google)Google OAuth
9LiveKit (voice/video calls)LiveKit
10CSP allowlist domains (frontend/admin nginx) for API, WebSocket, storage (R2/MinIO), GA, TurnstileSelf-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.

FileWho uses itPurpose
.env.example.envYou (self-hoster)Runtime config: copy to .env and set domain, secrets, DB passwords. This is the only file you need.
.env.frontend.exampleImage builders onlyUsed when building the frontend Docker image from source. Ignore when using pre-built images.
.env.admin.exampleImage builders onlyUsed 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

VariableExampleDescription
DOCKER_REGISTRYmyuserContainer registry namespace used by your deployment.
IMAGE_TAGlatest or v1.0.0Image 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.

VariableExample
API_PUBLIC_URLhttps://api.yourdomain.com
FRONTEND_PUBLIC_URLhttps://app.yourdomain.com
ADMIN_PUBLIC_URLhttps://admin.yourdomain.com
DOCS_PUBLIC_URLhttps://docs.bellamybook.com (default; central docs)
TRAEFIK_API_HOSTapi.yourdomain.com
TRAEFIK_FRONTEND_HOSTapp.yourdomain.com
TRAEFIK_FRONTEND_WWW_HOSTwww.yourdomain.com (optional; for SEO: www redirects 301 to main domain)
TRAEFIK_ADMIN_HOSTadmin.yourdomain.com
TRAEFIK_DOCS_HOSTdocs.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 use root), 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); set Neo4j__Password to the same password
  • MinIO: MINIO_ROOT_USER, MINIO_ROOT_PASSWORD (and in app config: Minio__Endpoint=minio:9000, Minio__PublicUrl for browser access)
  • JWT: JwtSettings__Secret (e.g. generate with openssl rand -base64 64)
  • Database app encryption: DatabaseAppAesKey (exactly 32 characters; required for db-migration — e.g. generate with openssl rand -base64 32 and use the first 32 characters)
  • Elasticsearch auth: ELASTICSEARCH_PASSWORD (required with xpack.security.enabled=true)

Avoid $ in .env secrets when possible. Docker Compose treats $VAR as 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=MinIO
  • Minio__Endpoint=minio:9000
  • Minio__PublicUrl = URL users’ browsers use (e.g. http://localhost:9000 or https://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__DefaultFrom for 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 .env and they are applied when the containers start (no image rebuild).

  • Monitoring: If using the monitoring profile, set GRAFANA_ADMIN_PASSWORD and related Traefik hosts.

  • Google Analytics 4 (GA4) – runtime: Optional page view analytics for the frontend SPA:

    VariableApplies toDescription
    GA4_FRONTEND_MEASUREMENT_IDbellamybook-frontend containerYour 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.com is 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 internal analytics feature flag so the app is allowed to send GA4 events.
  • When false, analytics is disabled in the built bundle even if GA4_FRONTEND_MEASUREMENT_ID is 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).

VariableApplies toDescription
VITE_TURNSTILE_SITE_KEYFrontend, AdminCloudflare Turnstile site key (public). Get it from Cloudflare Turnstile.
VITE_TURNSTILE_THEMEFrontend, Adminlight or dark (optional).
VITE_TURNSTILE_SIZEFrontend, Adminnormal or compact (optional).
VITE_TURNSTILE_ENABLED_ENDPOINTSFrontend, AdminComma-separated, e.g. login,register,forgot-password,reset-password,contact (optional).
VITE_GOOGLE_CLIENT_IDFrontend onlyGoogle OAuth client ID for “Sign in with Google”.
VITE_LIVEKIT_URLFrontend onlyLiveKit server URL for voice/video calls (e.g. wss://your-livekit.example.com).
VITE_VAPID_PUBLIC_KEYFrontend onlyWeb Push VAPID public key; must match backend WebPush:PublicKey.
CSP_EXTRA_*Frontend, AdminOptional 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):

DirectiveDefaultWhy
script-src / script-src-elem'self' + Cloudflare Turnstile/Insights + GTM + JSON-LD sha256 hashNo 'unsafe-eval', no 'unsafe-inline'. Tight to keep XSS surface near zero.
connect-src'self' + your API / Admin / Docs / WebSocket / LiveKit / analyticsTight allowlist; wss:// derived from your *_PUBLIC_URL.
frame-srcCloudflare Turnstile + YouTube (nocookie) + Vimeo + TikTok + X (Twitter) + Spotify + Twitch (player + clips) + SoundCloudBackend 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 / vimeocdnThe 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: + storageAllows 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):

  1. Backend Src/backend/API/Services/LinkPreviewService.cs — add detection in TryDetectDirectEmbed (or a new Try{Provider}OEmbedAsync if the provider returns a working iframe via oEmbed) and emit a unique EmbedType + VideoId encoding.
  2. Frontend Src/frontend/src/components/cards/PostCard.jsx — extend getLinkEmbedIframeConfig to map the new EmbedType to its iframe URL + aspect ratio.
  3. CSP Src/frontend/docker-entrypoint.sh (FRAME_SRC=) and dockerProd|dockerPublish/traefik/dynamic/traefik-dynamic.yml (frame-src) — append the provider host(s).
  4. 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

  1. Never commit secrets: Use environment variables or secret management
  2. Use strong passwords: Generate secure random passwords
  3. Rotate secrets regularly: Change JWT secrets periodically
  4. Limit access: Restrict database access to application servers only
  5. Use HTTPS: Always use SSL/TLS in production

Next Steps