# Environment Variables Reference ## Overview Changemaker Lite V2 uses over 100 environment variables to configure services, credentials, and feature flags. This document provides a complete reference organized by functional area. **Configuration File**: `.env` (never committed to Git) **Template**: `.env.example` (committed, safe to share) **Validation**: `api/src/config/env.ts` (Zod schema validates all variables on startup) --- ## Quick Start ### Initial Setup ```bash # Copy template cp .env.example .env # Generate secrets openssl rand -hex 32 # For JWT_ACCESS_SECRET openssl rand -hex 32 # For JWT_REFRESH_SECRET openssl rand -hex 32 # For ENCRYPTION_KEY (must differ from JWT secrets!) openssl rand -hex 16 # For LISTMONK_API_TOKEN # Edit .env nano .env ``` ### Minimal Required Variables **Must set before first start**: ```bash V2_POSTGRES_PASSWORD= REDIS_PASSWORD= JWT_ACCESS_SECRET= JWT_REFRESH_SECRET= ENCRYPTION_KEY= # Production only ``` **All other variables** have safe defaults for development. --- ## General Configuration | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `NODE_ENV` | `development` | No | Environment mode (`development` \| `production`) | | `DOMAIN` | `cmlite.org` | No | Base domain for subdomain routing | | `USER_ID` | `1000` | No | Host user ID for volume permissions | | `GROUP_ID` | `1000` | No | Host group ID for volume permissions | | `DOCKER_GROUP_ID` | `984` | No | Docker group ID (for homepage container) | **Usage**: ```bash NODE_ENV=production docker compose up -d ``` --- ## V2 PostgreSQL | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `V2_POSTGRES_USER` | `changemaker` | No | PostgreSQL username | | `V2_POSTGRES_PASSWORD` | `CHANGE_ME_STRONG_PASSWORD` | **Yes** | PostgreSQL password | | `V2_POSTGRES_DB` | `changemaker_v2` | No | Database name | | `V2_POSTGRES_PORT` | `5433` | No | Host port (container always 5432) | **Connection String** (auto-generated in docker-compose.yml): ``` postgresql://changemaker:PASSWORD@changemaker-v2-postgres:5432/changemaker_v2 ``` **Port Binding**: `127.0.0.1:5433:5432` (localhost only for security) **Important**: Change `V2_POSTGRES_PASSWORD` before production deployment. --- ## JWT Authentication | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `JWT_ACCESS_SECRET` | `GENERATE_WITH_openssl_rand_hex_32` | **Yes** | Access token secret (15min lifespan) | | `JWT_REFRESH_SECRET` | `GENERATE_WITH_openssl_rand_hex_32` | **Yes** | Refresh token secret (7 day lifespan) | | `JWT_ACCESS_EXPIRY` | `15m` | No | Access token expiration (`15m`, `1h`, etc.) | | `JWT_REFRESH_EXPIRY` | `7d` | No | Refresh token expiration (`7d`, `30d`, etc.) | | `ENCRYPTION_KEY` | `GENERATE_WITH_openssl_rand_hex_32` | **Yes (prod)** | DB encryption key for SMTP passwords, etc. | **Security Requirements** (enforced by Zod schema): - `JWT_ACCESS_SECRET` must be 32+ characters - `JWT_REFRESH_SECRET` must be 32+ characters - `ENCRYPTION_KEY` must be 32+ characters **and differ from JWT secrets** **Generation**: ```bash export JWT_ACCESS_SECRET=$(openssl rand -hex 32) export JWT_REFRESH_SECRET=$(openssl rand -hex 32) export ENCRYPTION_KEY=$(openssl rand -hex 32) echo "JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET}" >> .env echo "JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}" >> .env echo "ENCRYPTION_KEY=${ENCRYPTION_KEY}" >> .env ``` **Production Note**: `ENCRYPTION_KEY` required in production (dev mode allows empty for testing). --- ## Redis | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `REDIS_PASSWORD` | `CHANGE_ME_REDIS_PASSWORD` | **Yes** | Redis authentication password | | `REDIS_URL` | `redis://:PASSWORD@redis-changemaker:6379` | No | Full connection URL (auto-generated) | **Format**: `redis://[:@]:[/]` **Example**: ```bash REDIS_PASSWORD=mySecurePassword123 REDIS_URL=redis://:mySecurePassword123@redis-changemaker:6379 ``` **Security Note**: As of Security Audit 2025-02-11, Redis **requires authentication** in production. **Docker Command** (in docker-compose.yml): ```yaml command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru --requirepass "${REDIS_PASSWORD}" ``` --- ## API Configuration | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `API_PORT` | `4000` | No | Express API port (host) | | `API_URL` | `http://localhost:4000` | No | Public API URL (for emails, OAuth redirects) | | `CORS_ORIGINS` | `http://localhost:3000,http://localhost` | No | Allowed CORS origins (comma-separated) | **Production Example**: ```bash API_PORT=4000 API_URL=https://api.cmlite.org CORS_ORIGINS=https://app.cmlite.org,https://cmlite.org ``` **CORS Note**: List all frontend origins (admin, public site, media gallery). --- ## Admin GUI | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `ADMIN_PORT` | `3000` | No | Admin GUI port (host) | | `ADMIN_URL` | `http://localhost:3000` | No | Public admin URL | | `VITE_API_URL` | `http://changemaker-v2-api:4000` | No | API URL for Vite proxy (Docker internal) | | `VITE_MEDIA_API_URL` | `http://changemaker-media-api:4100` | No | Media API URL for Vite proxy | | `VITE_MKDOCS_URL` | `http://mkdocs-changemaker:8000` | No | MkDocs URL for iframe embed | **Development vs Production**: **Development** (Docker): ```bash VITE_API_URL=http://changemaker-v2-api:4000 # Container name VITE_MEDIA_API_URL=http://changemaker-media-api:4100 ``` **Development** (local): ```bash VITE_API_URL=http://localhost:4000 # Localhost VITE_MEDIA_API_URL=http://localhost:4100 ``` **Production**: Vite build embeds these URLs at build time. --- ## Nginx | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `NGINX_HTTP_PORT` | `80` | No | HTTP port | | `NGINX_HTTPS_PORT` | `443` | No | HTTPS port | **Port Mapping** (docker-compose.yml): ```yaml nginx: ports: - "80:80" - "443:443" - "8881:8881" # NocoDB embed proxy - "8882:8882" # n8n embed proxy - "8883:8883" # Gitea embed proxy - "8884:8884" # MailHog embed proxy - "8885:8885" # Mini QR embed proxy ``` **Custom Ports** (if 80/443 occupied): ```bash NGINX_HTTP_PORT=8080 NGINX_HTTPS_PORT=8443 ``` --- ## SMTP / Email | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `SMTP_HOST` | `mailhog-changemaker` | No | SMTP server hostname | | `SMTP_PORT` | `1025` | No | SMTP server port | | `SMTP_USER` | `` | No | SMTP username (empty for MailHog) | | `SMTP_PASS` | `` | No | SMTP password | | `SMTP_FROM` | `noreply@cmlite.org` | No | Default sender email | | `SMTP_FROM_NAME` | `Changemaker Lite` | No | Default sender name | | `EMAIL_TEST_MODE` | `true` | No | Route all emails to MailHog (dev mode) | | `TEST_EMAIL_RECIPIENT` | `admin@cmlite.org` | No | Override recipient in test mode | **Development** (MailHog): ```bash SMTP_HOST=mailhog-changemaker SMTP_PORT=1025 SMTP_USER= SMTP_PASS= EMAIL_TEST_MODE=true ``` **Production** (e.g., ProtonMail): ```bash SMTP_HOST=smtp.protonmail.ch SMTP_PORT=587 SMTP_USER=your@email.com SMTP_PASS=your-app-password EMAIL_TEST_MODE=false ``` **Test Mode Behavior**: - `true`: All emails sent to MailHog (visible at http://localhost:8025) - `false`: Emails sent to real recipients via SMTP **SiteSettings Override**: Admins can override SMTP config via `/app/settings` (stored encrypted in DB). --- ## Listmonk ### Database | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `LISTMONK_DB_PORT` | `5432` | No | Listmonk PostgreSQL port | | `LISTMONK_DB_USER` | `listmonk` | No | Database username | | `LISTMONK_DB_PASSWORD` | `CHANGE_ME_LISTMONK_PASSWORD` | **Yes** | Database password | | `LISTMONK_DB_NAME` | `listmonk` | No | Database name | ### Web Admin | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `LISTMONK_PORT` | `9001` | No | Listmonk web UI port | | `LISTMONK_WEB_ADMIN_USER` | `admin` | No | Web UI username | | `LISTMONK_WEB_ADMIN_PASSWORD` | `CHANGE_ME_LISTMONK_ADMIN` | **Yes** | Web UI password | ### API Integration | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `LISTMONK_API_USER` | `v2-api` | No | API user (auto-created by listmonk-init) | | `LISTMONK_API_TOKEN` | `GENERATE_WITH_openssl_rand_hex_16` | **Yes** | API token (plaintext, not bcrypt) | | `LISTMONK_ADMIN_USER` | `v2-api` | No | Alias for API user (V2 uses this) | | `LISTMONK_ADMIN_PASSWORD` | `SAME_AS_LISTMONK_API_TOKEN` | **Yes** | Alias for API token | | `LISTMONK_SYNC_ENABLED` | `false` | No | Enable participant/location sync | | `LISTMONK_PROXY_PORT` | `9002` | No | OAuth proxy port (for future integrations) | **API User Setup**: The `listmonk-init` container auto-creates the API user by directly inserting into PostgreSQL. **Token Generation**: ```bash export LISTMONK_API_TOKEN=$(openssl rand -hex 16) echo "LISTMONK_API_TOKEN=${LISTMONK_API_TOKEN}" >> .env echo "LISTMONK_ADMIN_PASSWORD=${LISTMONK_API_TOKEN}" >> .env ``` **Sync Behavior**: - `false`: Manual sync only (default) - `true`: Auto-sync participants/locations to Listmonk lists on signup/create ### SMTP Configuration | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `LISTMONK_SMTP_HOST` | `mailhog-changemaker` | No | SMTP server for newsletters | | `LISTMONK_SMTP_PORT` | `1025` | No | SMTP port | | `LISTMONK_SMTP_USER` | `` | No | SMTP username | | `LISTMONK_SMTP_PASSWORD` | `` | No | SMTP password | | `LISTMONK_SMTP_TLS_TYPE` | `none` | No | TLS mode (`none` \| `STARTTLS` \| `TLS`) | | `LISTMONK_SMTP_FROM` | `Changemaker Lite ` | No | Newsletter sender | **listmonk-init Behavior**: Configures **dual SMTP providers** (MailHog + production if credentials set). --- ## Represent API | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `REPRESENT_API_URL` | `https://represent.opennorth.ca` | No | Represent API endpoint (Canadian electoral data) | **Free Public API**: No authentication required. **Usage**: Postal code → representative lookup for Influence campaigns. --- ## NocoDB | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `NOCODB_V2_PORT` | `8091` | No | NocoDB web UI port | | `NOCODB_URL` | `http://changemaker-v2-nocodb:8080` | No | Internal NocoDB URL | | `NC_ADMIN_EMAIL` | `admin@cmlite.org` | No | Admin email | | `NC_ADMIN_PASSWORD` | `CHANGE_ME_NOCODB_PASSWORD` | **Yes** | Admin password | | `NC_PUBLIC_URL` | `http://localhost:8091` | No | Public NocoDB URL | **Database Connection**: Uses separate `nocodb_meta` database (auto-created by `init-nocodb-db.sh`). **Connection String**: ``` pg://changemaker-v2-postgres:5432?u=changemaker&p=PASSWORD&d=nocodb_meta ``` --- ## Media Management | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `ENABLE_MEDIA_FEATURES` | `false` | No | Enable media manager features | | `MEDIA_API_PORT` | `4100` | No | Fastify media API port | | `MEDIA_API_PUBLIC_URL` | `http://media-api:4100` | No | Public media API URL | | `MEDIA_ROOT` | `/media/library` | No | Media library root path | | `MEDIA_UPLOADS` | `/media/uploads` | No | Upload staging directory | | `MAX_UPLOAD_SIZE_GB` | `10` | No | Max video upload size (GB) | | `PUBLIC_MEDIA_PORT` | `3100` | No | Public media gallery port | | `VIDEO_PLAYER_DEBUG` | `false` | No | Enable video.js debug logging | **Feature Flag**: Set `ENABLE_MEDIA_FEATURES=true` to activate media routes. **Volume Mounts** (in docker-compose.yml): ```yaml volumes: - ${MEDIA_ROOT:-./media}:/media:ro # Library (read-only) - ${MEDIA_ROOT:-./media}/local/inbox:/media/local/inbox:rw # Inbox (writable) ``` **Supported Formats**: MP4, MOV, AVI, MKV, WebM, M4V, FLV --- ## Gitea | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `GITEA_URL` | `http://gitea-changemaker:3000` | No | Internal Gitea URL | | `GITEA_WEB_PORT` | `3030` | No | Gitea web UI port | | `GITEA_SSH_PORT` | `2222` | No | Gitea SSH port (for git push/pull) | | `GITEA_DB_TYPE` | `mysql` | No | Database type | | `GITEA_DB_HOST` | `gitea-db:3306` | No | MySQL hostname | | `GITEA_DB_NAME` | `gitea` | No | Database name | | `GITEA_DB_USER` | `gitea` | No | Database username | | `GITEA_DB_PASSWD` | `CHANGE_ME_GITEA_DB` | **Yes** | Database password | | `GITEA_DB_ROOT_PASSWORD` | `CHANGE_ME_GITEA_ROOT` | **Yes** | MySQL root password | | `GITEA_ROOT_URL` | `https://git.cmlite.org` | No | Public Gitea URL | | `GITEA_DOMAIN` | `git.cmlite.org` | No | Gitea domain | **First-Time Setup**: Visit http://localhost:3030 to create admin account. **Git Commands**: ```bash # Clone via HTTP git clone http://localhost:3030/user/repo.git # Clone via SSH git clone ssh://git@localhost:2222/user/repo.git ``` --- ## n8n | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `N8N_URL` | `http://n8n-changemaker:5678` | No | Internal n8n URL | | `N8N_PORT` | `5678` | No | n8n port | | `N8N_HOST` | `n8n.cmlite.org` | No | Public n8n hostname | | `N8N_ENCRYPTION_KEY` | `CHANGE_ME_N8N_KEY` | **Yes** | Workflow encryption key | | `N8N_USER_EMAIL` | `admin@example.com` | No | Default admin email | | `N8N_USER_PASSWORD` | `CHANGE_ME_N8N_PASSWORD` | **Yes** | Default admin password | | `GENERIC_TIMEZONE` | `UTC` | No | Workflow timezone | **First Start**: n8n creates admin user with `N8N_USER_EMAIL`/`N8N_USER_PASSWORD` automatically. **Encryption Key**: Used to encrypt credentials in workflows. --- ## MkDocs | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `MKDOCS_PORT` | `4003` | No | MkDocs live preview port | | `MKDOCS_SITE_SERVER_PORT` | `4001` | No | MkDocs static site port | | `BASE_DOMAIN` | `https://cmlite.org` | No | Site URL for sitemap/canonical | | `MKDOCS_PREVIEW_URL` | `http://mkdocs:8000` | No | Internal preview URL | | `MKDOCS_DOCS_PATH` | `/mkdocs/docs` | No | Documentation source path | **Port Change**: Was 4000 in V1, changed to 4003 to avoid conflict with API. **Live Reload**: http://localhost:4003 (updates on file save) **Static Build**: http://localhost:4001 (Nginx-served production build) --- ## Code Server | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `CODE_SERVER_PORT` | `8888` | No | Code Server port | | `CODE_SERVER_URL` | `http://code-server:8080` | No | Internal Code Server URL | | `USER_NAME` | `coder` | No | Code Server username | **Access**: http://localhost:8888 **Password**: Set in `configs/code-server/.config/code-server/config.yaml` --- ## Homepage | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `HOMEPAGE_PORT` | `3010` | No | Homepage dashboard port | | `HOMEPAGE_VAR_BASE_URL` | `http://localhost` | No | Base URL for service links | **Configuration**: Edit `configs/homepage/services.yaml` to customize dashboard. --- ## Mini QR | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `MINI_QR_PORT` | `8089` | No | Mini QR service port | | `MINI_QR_URL` | `http://mini-qr:8080` | No | Internal Mini QR URL | | `MINI_QR_EMBED_PORT` | `8885` | No | Nginx embed proxy port | **Usage**: Walk sheets + cut exports embed QR codes via API or iframe. --- ## MailHog | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `MAILHOG_SMTP_PORT` | `1025` | No | SMTP port (internal only) | | `MAILHOG_WEB_PORT` | `8025` | No | Web UI port | **Web UI**: http://localhost:8025 **SMTP**: Only accessible from Docker network (not exposed to host). --- ## NAR Import | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `NAR_DATA_DIR` | `/data` | No | Path to NAR data directory (in container) | **Host Mount** (in docker-compose.yml): ```yaml volumes: - ./data:/data:ro # Read-only NAR data ``` **Data Structure**: ``` ./data/ └─ 202501/ (YYYYMM) ├─ Addresses/ │ ├─ Address_10.txt (PEI) │ ├─ Address_24_part_1.txt (Quebec part 1) │ └─ ... └─ Locations/ ├─ Location_10.txt └─ ... ``` **Download**: https://www150.statcan.gc.ca/n1/pub/46-26-0002/462600022022001-eng.htm --- ## Geocoding | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `MAPBOX_API_KEY` | `` | No | Mapbox API key (optional, 100k free/month) | | `GEOCODING_RATE_LIMIT_MS` | `1100` | No | Delay between provider requests (ms) | | `GEOCODING_CACHE_ENABLED` | `true` | No | Enable Redis caching | | `GEOCODING_CACHE_TTL_HOURS` | `24` | No | Cache TTL in hours | | `GOOGLE_MAPS_API_KEY` | `` | No | Google Maps API key (optional, paid) | | `GOOGLE_MAPS_ENABLED` | `false` | No | Enable Google geocoding provider | | `GEOCODING_PARALLEL_ENABLED` | `true` | No | Parallel geocoding for bulk imports | | `GEOCODING_BATCH_SIZE` | `10` | No | Batch size for parallel geocoding | | `BULK_GEOCODE_ENABLED` | `true` | No | Enable bulk re-geocode feature | | `BULK_GEOCODE_MAX_BATCH` | `5000` | No | Max locations per bulk geocode batch | **Providers** (in fallback order): 1. **Nominatim** (OpenStreetMap, free) 2. **ArcGIS** (free tier) 3. **Photon** (free) 4. **Mapbox** (100k free/month, requires API key) 5. **LocationIQ** (free tier) 6. **Google** (paid, most accurate) **Recommendation**: Add `MAPBOX_API_KEY` for better accuracy without cost. --- ## Pangolin Tunnel | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `PANGOLIN_API_URL` | `https://api.bnkserve.org/v1` | No | Pangolin API endpoint | | `PANGOLIN_API_KEY` | `` | No | Pangolin API key | | `PANGOLIN_ORG_ID` | `` | No | Organization ID (from setup wizard) | | `PANGOLIN_SITE_ID` | `` | No | Site ID (from setup wizard) | | `PANGOLIN_ENDPOINT` | `https://pangolin.bnkserve.org` | No | Tunnel endpoint URL | | `PANGOLIN_NEWT_ID` | `` | No | Newt connector ID | | `PANGOLIN_NEWT_SECRET` | `` | No | Newt connector secret | **Setup Workflow**: 1. Visit `/app/pangolin` in admin GUI 2. Enter `PANGOLIN_API_KEY` 3. Create org → site → endpoint → resource 4. Copy `NEWT_ID`/`NEWT_SECRET` to `.env` 5. Restart Newt container **Manual Setup**: ```bash # Set API key export PANGOLIN_API_KEY=your-api-key # Create org (returns ORG_ID) curl -H "Authorization: Bearer $PANGOLIN_API_KEY" \ https://api.bnkserve.org/v1/orgs \ -d '{"name":"My Organization"}' # Create site (returns SITE_ID) curl -H "Authorization: Bearer $PANGOLIN_API_KEY" \ https://api.bnkserve.org/v1/sites \ -d '{"org_id":"ORG_ID","name":"Production Site"}' # Continue setup... ``` See [Tunneling](tunneling.md) for complete guide. --- ## Monitoring ### Prometheus | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `PROMETHEUS_PORT` | `9090` | No | Prometheus port | **Scrape Targets** (configured in `configs/prometheus/prometheus.yml`): - `changemaker-v2-api:4000/api/metrics` (10s interval) - `redis-exporter:9121` (15s interval) - `cadvisor:8080` (15s interval) - `node-exporter:9100` (15s interval) **Retention**: 30 days (configured in docker-compose.yml command). ### Grafana | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `GRAFANA_PORT` | `3001` | No | Grafana port | | `GRAFANA_ADMIN_PASSWORD` | `admin` | No | Admin password | | `GRAFANA_ROOT_URL` | `http://localhost:3001` | No | Public Grafana URL | **Default Login**: admin / admin (change on first login) **Dashboards**: 3 pre-configured dashboards auto-provisioned from `configs/grafana/` ### Exporters | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `CADVISOR_PORT` | `8080` | No | cAdvisor container metrics port | | `NODE_EXPORTER_PORT` | `9100` | No | Node exporter system metrics port | | `REDIS_EXPORTER_PORT` | `9121` | No | Redis exporter port | ### Alertmanager | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `ALERTMANAGER_PORT` | `9093` | No | Alertmanager port | **Configuration**: Edit `configs/alertmanager/alertmanager.yml` for notification receivers. ### Gotify | Variable | Default | Required | Description | |----------|---------|----------|-------------| | `GOTIFY_PORT` | `8889` | No | Gotify push notification server port | | `GOTIFY_ADMIN_USER` | `admin` | No | Gotify admin username | | `GOTIFY_ADMIN_PASSWORD` | `admin` | No | Gotify admin password | **Usage**: Create apps in Gotify UI, add webhook URL to Alertmanager. --- ## Security Checklist **Before production deployment**: - [ ] Change all `CHANGE_ME_*` passwords - [ ] Generate strong `JWT_ACCESS_SECRET` (32+ chars) - [ ] Generate strong `JWT_REFRESH_SECRET` (32+ chars) - [ ] Generate strong `ENCRYPTION_KEY` (32+ chars, **different from JWT secrets**) - [ ] Set strong `REDIS_PASSWORD` - [ ] Set strong `V2_POSTGRES_PASSWORD` - [ ] Set strong `LISTMONK_DB_PASSWORD` - [ ] Set strong `LISTMONK_API_TOKEN` - [ ] Set strong `GITEA_DB_PASSWD` + `GITEA_DB_ROOT_PASSWORD` - [ ] Set strong `N8N_ENCRYPTION_KEY` + `N8N_USER_PASSWORD` - [ ] Set strong `NC_ADMIN_PASSWORD` (NocoDB) - [ ] Set strong `GRAFANA_ADMIN_PASSWORD` - [ ] Disable `EMAIL_TEST_MODE` (set to `false`) - [ ] Configure real SMTP credentials - [ ] Set `NODE_ENV=production` - [ ] Review `CORS_ORIGINS` (whitelist only trusted domains) **Validation**: ```bash # Check for remaining placeholders grep -r "CHANGE_ME" .env # Verify secrets are different echo "JWT_ACCESS_SECRET: $(grep JWT_ACCESS_SECRET .env)" echo "JWT_REFRESH_SECRET: $(grep JWT_REFRESH_SECRET .env)" echo "ENCRYPTION_KEY: $(grep ENCRYPTION_KEY .env)" ``` --- ## Troubleshooting ### Missing .env File **Symptoms**: Containers fail to start with "missing environment variable" errors **Solution**: ```bash # Create from template cp .env.example .env # Verify file exists ls -la .env ``` --- ### Invalid Environment Variables **Symptoms**: API fails to start with Zod validation errors **Diagnosis**: ```bash # View API startup logs docker compose logs api | grep -A10 "Environment validation" ``` **Common errors**: - `JWT_ACCESS_SECRET` too short (must be 32+ chars) - `ENCRYPTION_KEY` same as `JWT_ACCESS_SECRET` (must differ) - Invalid URL format (`API_URL` must start with http:// or https://) **Solution**: ```bash # Regenerate secrets export JWT_ACCESS_SECRET=$(openssl rand -hex 32) export ENCRYPTION_KEY=$(openssl rand -hex 32) # Update .env sed -i "s/^JWT_ACCESS_SECRET=.*/JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET}/" .env sed -i "s/^ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${ENCRYPTION_KEY}/" .env # Restart API docker compose restart api ``` --- ### PostgreSQL Connection Failures **Symptoms**: API logs show `ECONNREFUSED` or `authentication failed` **Diagnosis**: ```bash # Check PostgreSQL is running docker compose ps v2-postgres # Test connection docker compose exec api npx prisma db pull # Verify DATABASE_URL docker compose exec api printenv | grep DATABASE_URL ``` **Solution**: ```bash # Verify password matches in .env grep V2_POSTGRES_PASSWORD .env # Restart PostgreSQL docker compose restart v2-postgres # Wait for healthcheck docker compose ps v2-postgres # Should show (healthy) ``` --- ### Redis Connection Failures **Symptoms**: API logs show `ECONNREFUSED` or `WRONGPASS invalid password` **Diagnosis**: ```bash # Check Redis is running docker compose ps redis # Test connection docker compose exec redis redis-cli -a "${REDIS_PASSWORD}" ping ``` **Solution**: ```bash # Verify password in .env grep REDIS_PASSWORD .env # Ensure REDIS_URL includes password grep REDIS_URL .env # Should be redis://:PASSWORD@redis-changemaker:6379 # Restart Redis docker compose restart redis ``` --- ### Environment Variables Not Updating **Symptoms**: Changed `.env` but service still uses old value **Cause**: Docker Compose reads `.env` at startup, not runtime **Solution**: ```bash # Recreate container (picks up new env vars) docker compose up -d --force-recreate api # Or: stop and start docker compose down docker compose up -d ``` --- ## Related Documentation - **[Docker Compose](docker-compose.md)** — Service orchestration - **[SSL/TLS](ssl-tls.md)** — Certificate management - **[Tunneling](tunneling.md)** — Pangolin setup - **[Backup & Restore](backup-restore.md)** — Data protection - **[Security Audit](../../SECURITY_AUDIT_2025-02-11.md)** — Security requirements