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

24 KiB

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

# 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:

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:

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:

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:

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):

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:

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):

VITE_API_URL=http://changemaker-v2-api:4000  # Container name
VITE_MEDIA_API_URL=http://changemaker-media-api:4100

Development (local):

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):

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):

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):

SMTP_HOST=mailhog-changemaker
SMTP_PORT=1025
SMTP_USER=
SMTP_PASS=
EMAIL_TEST_MODE=true

Production (e.g., ProtonMail):

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:

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):

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:

# 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):

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:

# 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 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# Check Redis is running
docker compose ps redis

# Test connection
docker compose exec redis redis-cli -a "${REDIS_PASSWORD}" ping

Solution:

# 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:

# 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