changemaker.lite/mkdocs/docs/v2/deployment/environment-variables.md

799 lines
24 KiB
Markdown

# 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=<strong-password>
REDIS_PASSWORD=<strong-password>
JWT_ACCESS_SECRET=<openssl-rand-hex-32>
JWT_REFRESH_SECRET=<openssl-rand-hex-32>
ENCRYPTION_KEY=<openssl-rand-hex-32> # 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://[:<password>@]<host>:<port>[/<db>]`
**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 <noreply@cmlite.org>` | 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