chore(approach-c): Phase 0 complete - templates byte-equivalent to canonical
This commit completes Phase 0 of Approach C: the CCP template/env/static
files now produce output structurally byte-identical to canonical
docker-compose.prod.yml + .env.example. Verified by rendering against
marcelle, linda, and pia and diffing against their actual files — all
three show only the 30-line CCP-tenant header comment differing,
zero service/env-var structural differences.
Changes:
- templates/docker-compose.yml.hbs: reverted {{imageTag}} substitutions
back to ${IMAGE_TAG:-latest} so the compose template is now byte-
equivalent to docker-compose.prod.yml (modulo header). CCP controls
per-instance image tag selection via the rendered .env's IMAGE_TAG,
which compose-up picks up at runtime. This single-source-of-truth
via env-substitution matches install.sh tenants exactly.
- templates/env.hbs: rewritten as a near-mirror of .env.example. Adds
27 missing keys (IMAGE_TAG, GITEA_REGISTRY, COMPOSE_PROFILES,
ENABLE_CCP_AGENT, GITEA_ADMIN_*, ENABLE_HLS_TRANSCODE, TZ, etc.)
plus 15 CCP-specific extras (embed ports, dev-mode helpers, etc.).
All 145 compose-template env-var references are now covered.
- templates/nginx/nginx.conf: synced from canonical. Includes recent
security additions: redacted access-log format for token/secret
query params, rate-limit zones (api_global, api_auth, upload),
conditional HSTS via X-Forwarded-Proto map.
- api/scripts/render-for-instance.ts (new): one-off CLI that loads
an Instance row, decrypts secrets if present (or uses empty object
for isRegistered=true tenants), and calls renderAllTemplates() to
a scratch dir. Used in Phase 0.4 to verify the template-vs-prod
contract per tenant.
Usage:
docker compose exec ccp-api npx tsx scripts/render-for-instance.ts \
--slug changemakerlite
Phase 0 acceptance gate met:
- marcelle (release v2.10.2 install): 30-line diff, header-only
- linda (release v2.9.14 install): 30-line diff, header-only
- pia (release v2.9.10 install): 30-line diff, header-only
- env.hbs key coverage: 0 missing vs marcelle's .env
Next phases unblocked:
- Phase 1: add Instance.imageTag column (Prisma migration)
- Phase 2: pre-flight diff endpoint
- Phase 3: startReleaseUpgrade runner
- Phase 4: routes + schemas
- Phase 5: CCP UI "Upgrade to Release" button
- Phase 6: E2E test on marcelle (v2.10.2 -> v2.10.3)
Bunker Admin
This commit is contained in:
parent
f34382ebdd
commit
97444645cb
115
changemaker-control-panel/api/scripts/render-for-instance.ts
Normal file
115
changemaker-control-panel/api/scripts/render-for-instance.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env tsx
|
||||||
|
/**
|
||||||
|
* render-for-instance.ts — Approach C Phase 0 verification harness.
|
||||||
|
*
|
||||||
|
* Loads a CCP-tracked Instance row, builds its template context, and renders
|
||||||
|
* all templates to a scratch directory under /tmp/render-<slug>/. Operator
|
||||||
|
* then diffs the rendered output against the tenant's actual on-disk files
|
||||||
|
* to verify the template-vs-prod-compose equivalence contract.
|
||||||
|
*
|
||||||
|
* Usage (run inside ccp-api container):
|
||||||
|
* docker compose exec ccp-api npx tsx scripts/render-for-instance.ts --slug changemakerlite
|
||||||
|
* docker compose exec ccp-api npx tsx scripts/render-for-instance.ts --id <uuid>
|
||||||
|
*
|
||||||
|
* Output: prints scratch dir path; exits 0 on success, 1 on failure.
|
||||||
|
*
|
||||||
|
* This script does NOT touch any tenant. It only reads from the CCP database
|
||||||
|
* and writes to /tmp on the CCP api container.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { prisma } from '../src/lib/prisma';
|
||||||
|
import { decryptJson } from '../src/utils/encryption';
|
||||||
|
import {
|
||||||
|
buildTemplateContext,
|
||||||
|
renderAllTemplates,
|
||||||
|
} from '../src/services/template-engine';
|
||||||
|
import path from 'node:path';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
|
||||||
|
interface Args {
|
||||||
|
slug?: string;
|
||||||
|
id?: string;
|
||||||
|
outDir?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv: string[]): Args {
|
||||||
|
const args: Args = {};
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const a = argv[i];
|
||||||
|
if (a === '--slug' && argv[i + 1]) { args.slug = argv[++i]; continue; }
|
||||||
|
if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; continue; }
|
||||||
|
if (a === '--out' && argv[i + 1]) { args.outDir = argv[++i]; continue; }
|
||||||
|
if (a === '-h' || a === '--help') {
|
||||||
|
console.log('usage: render-for-instance.ts (--slug X | --id Y) [--out /tmp/render-X]');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
if (!args.slug && !args.id) {
|
||||||
|
console.error('error: --slug or --id is required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = await prisma.instance.findUnique({
|
||||||
|
where: args.id ? { id: args.id } : { slug: args.slug! },
|
||||||
|
});
|
||||||
|
if (!instance) {
|
||||||
|
console.error(`error: instance not found (slug=${args.slug ?? '?'}, id=${args.id ?? '?'})`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For isRegistered tenants there are no encrypted secrets. Use empty stubs
|
||||||
|
// so buildTemplateContext doesn't crash; env.hbs values that read from
|
||||||
|
// {{secrets.*}} will render as blank, which is fine for diff purposes
|
||||||
|
// because the tenant's own .env still has the real values via install.sh.
|
||||||
|
let secrets: Record<string, string> = {};
|
||||||
|
if (instance.encryptedSecrets) {
|
||||||
|
try {
|
||||||
|
secrets = decryptJson<Record<string, string>>(instance.encryptedSecrets);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`warn: decryptJson failed (${(err as Error).message}); using empty secrets`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`(isRegistered=true tenant; using empty secrets for compose/nginx render — env.hbs values will be blank)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outDir = args.outDir ?? path.join('/tmp', `render-${instance.slug}`);
|
||||||
|
await fs.rm(outDir, { recursive: true, force: true });
|
||||||
|
await fs.mkdir(outDir, { recursive: true });
|
||||||
|
|
||||||
|
const context = buildTemplateContext(instance, secrets);
|
||||||
|
await renderAllTemplates(context, outDir);
|
||||||
|
|
||||||
|
// Summarize what we rendered
|
||||||
|
const entries: string[] = [];
|
||||||
|
async function walk(dir: string, rel = '') {
|
||||||
|
const items = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
for (const item of items) {
|
||||||
|
const full = path.join(dir, item.name);
|
||||||
|
const r = path.join(rel, item.name);
|
||||||
|
if (item.isDirectory()) await walk(full, r);
|
||||||
|
else entries.push(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await walk(outDir);
|
||||||
|
|
||||||
|
console.log(`\n=== rendered ${entries.length} files to: ${outDir} ===`);
|
||||||
|
for (const e of entries.sort()) {
|
||||||
|
const stat = await fs.stat(path.join(outDir, e));
|
||||||
|
console.log(` ${e} (${stat.size} bytes)`);
|
||||||
|
}
|
||||||
|
console.log(`\nTo diff against the live tenant:`);
|
||||||
|
console.log(` ssh <tenant> 'cat <basePath>/docker-compose.yml' | diff -u - ${outDir}/docker-compose.yml`);
|
||||||
|
console.log(``);
|
||||||
|
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('render-for-instance.ts failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@ -4,16 +4,21 @@
|
|||||||
# Instance: {{name}} ({{slug}})
|
# Instance: {{name}} ({{slug}})
|
||||||
# Compose project: {{composeProject}}
|
# Compose project: {{composeProject}}
|
||||||
#
|
#
|
||||||
# This template is a near-mirror of changemaker.lite/docker-compose.prod.yml.
|
# This template is a byte-mirror of changemaker.lite/docker-compose.prod.yml
|
||||||
# Approach C (CCP-driven release upgrade) renders this against the tenant's
|
# (modulo this header comment). Approach C (CCP-driven release upgrade)
|
||||||
# context and writes the result to the tenant's filesystem. Variation is
|
# renders this against the tenant's context and writes the result to the
|
||||||
# almost entirely env-var driven (.env file); only image-tag overrides for
|
# tenant's filesystem.
|
||||||
# the four core CCP-built images use Handlebars ({{imageTag}}).
|
#
|
||||||
|
# All per-instance variation flows through env-var substitution from the
|
||||||
|
# tenant's .env file (rendered by env.hbs for CCP-provisioned tenants;
|
||||||
|
# kept as-is for install.sh-registered tenants). The CCP controls image
|
||||||
|
# tag selection by writing IMAGE_TAG to the tenant's .env, which the
|
||||||
|
# compose's ${IMAGE_TAG:-latest} substitution then picks up at compose-up.
|
||||||
#
|
#
|
||||||
# To keep this template in sync with canonical docker-compose.prod.yml:
|
# To keep this template in sync with canonical docker-compose.prod.yml:
|
||||||
# - When a new service is added to changemaker.lite/docker-compose.prod.yml,
|
# - When a new service is added to changemaker.lite/docker-compose.prod.yml,
|
||||||
# copy the same block here. Use Handlebars only where per-instance
|
# copy the same block here verbatim. Handlebars is NOT used in the
|
||||||
# variation is needed (currently only {{imageTag}} for the 5 CCP images).
|
# compose template itself — all variation is env-var driven.
|
||||||
###############################################################################
|
###############################################################################
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
@ -30,7 +35,7 @@ services:
|
|||||||
|
|
||||||
# Unified Express.js API
|
# Unified Express.js API
|
||||||
api:
|
api:
|
||||||
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-api:{{imageTag}}
|
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-api:${IMAGE_TAG:-latest}
|
||||||
container_name: changemaker-v2-api
|
container_name: changemaker-v2-api
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@ -179,7 +184,7 @@ services:
|
|||||||
|
|
||||||
# Fastify Media API (Microservice for Media Management)
|
# Fastify Media API (Microservice for Media Management)
|
||||||
media-api:
|
media-api:
|
||||||
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-media-api:{{imageTag}}
|
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-media-api:${IMAGE_TAG:-latest}
|
||||||
container_name: changemaker-media-api
|
container_name: changemaker-media-api
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@ -240,7 +245,7 @@ services:
|
|||||||
|
|
||||||
# React Admin GUI (Vite dev server)
|
# React Admin GUI (Vite dev server)
|
||||||
admin:
|
admin:
|
||||||
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-admin:{{imageTag}}
|
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-admin:${IMAGE_TAG:-latest}
|
||||||
container_name: changemaker-v2-admin
|
container_name: changemaker-v2-admin
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@ -292,7 +297,7 @@ services:
|
|||||||
|
|
||||||
# Nginx reverse proxy
|
# Nginx reverse proxy
|
||||||
nginx:
|
nginx:
|
||||||
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-nginx:{{imageTag}}
|
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-nginx:${IMAGE_TAG:-latest}
|
||||||
container_name: changemaker-v2-nginx
|
container_name: changemaker-v2-nginx
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@ -1428,7 +1433,7 @@ services:
|
|||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
ccp-agent:
|
ccp-agent:
|
||||||
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-ccp-agent:{{imageTag}}
|
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-ccp-agent:${IMAGE_TAG:-latest}
|
||||||
container_name: ${COMPOSE_PROJECT_NAME:-changemaker-lite}-ccp-agent
|
container_name: ${COMPOSE_PROJECT_NAME:-changemaker-lite}-ccp-agent
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
profiles: ["ccp-agent"]
|
profiles: ["ccp-agent"]
|
||||||
|
|||||||
@ -1,65 +1,95 @@
|
|||||||
# ============================================================
|
# ==============================================================================
|
||||||
# Changemaker Lite — Instance: {{name}}
|
# Changemaker Lite v2 — Tenant .env (CCP-rendered)
|
||||||
|
# Instance: {{name}} ({{slug}})
|
||||||
# Generated by CCP on {{now}}
|
# Generated by CCP on {{now}}
|
||||||
# ============================================================
|
# ==============================================================================
|
||||||
|
# This file is a near-mirror of changemaker.lite/.env.example with Handlebars
|
||||||
|
# overlay for tenant-specific values (DOMAIN, secrets, COMPOSE_PROJECT_NAME).
|
||||||
|
# Static defaults match .env.example so docker-compose.yml.hbs (a mirror of
|
||||||
|
# docker-compose.prod.yml) has every ${VAR} it references.
|
||||||
|
#
|
||||||
|
# Keeping this in sync with .env.example after upstream additions: copy the
|
||||||
|
# new key + default, replace any tenant-specific value with the matching
|
||||||
|
# Handlebars expression. Most additions need no Handlebars.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
# Core
|
# --- General ---
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
DOMAIN={{domain}}
|
DOMAIN={{domain}}
|
||||||
|
COMPOSE_PROJECT_NAME={{composeProject}}
|
||||||
|
TZ=UTC
|
||||||
USER_ID=1000
|
USER_ID=1000
|
||||||
GROUP_ID=1000
|
GROUP_ID=1000
|
||||||
DOCKER_GROUP_ID=984
|
DOCKER_GROUP_ID=984
|
||||||
|
|
||||||
# V2 PostgreSQL
|
# --- V2 PostgreSQL ---
|
||||||
V2_POSTGRES_USER=changemaker
|
V2_POSTGRES_USER=changemaker
|
||||||
V2_POSTGRES_PASSWORD={{secrets.postgresPassword}}
|
V2_POSTGRES_PASSWORD={{secrets.postgresPassword}}
|
||||||
V2_POSTGRES_DB=changemaker_v2
|
V2_POSTGRES_DB=changemaker_v2
|
||||||
V2_POSTGRES_PORT={{ports.postgres}}
|
V2_POSTGRES_PORT={{ports.postgres}}
|
||||||
DATABASE_URL=postgresql://changemaker:{{secrets.postgresPassword}}@{{containerPrefix}}-postgres:5432/changemaker_v2
|
|
||||||
|
|
||||||
# Redis
|
# --- JWT Auth ---
|
||||||
REDIS_PASSWORD={{secrets.redisPassword}}
|
|
||||||
REDIS_URL=redis://:{{secrets.redisPassword}}@{{containerPrefix}}-redis:6379
|
|
||||||
|
|
||||||
# JWT Auth
|
|
||||||
JWT_ACCESS_SECRET={{secrets.jwtAccessSecret}}
|
JWT_ACCESS_SECRET={{secrets.jwtAccessSecret}}
|
||||||
JWT_REFRESH_SECRET={{secrets.jwtRefreshSecret}}
|
JWT_REFRESH_SECRET={{secrets.jwtRefreshSecret}}
|
||||||
JWT_INVITE_SECRET={{secrets.jwtInviteSecret}}
|
JWT_INVITE_SECRET={{secrets.jwtInviteSecret}}
|
||||||
JWT_ACCESS_EXPIRY=15m
|
JWT_ACCESS_EXPIRY=15m
|
||||||
# Reduced 2026-04-12 from 7d → 24h (P2-3). Combined with device-fingerprint
|
# Reduced from 7d → 24h on 2026-04-12 (P2-3 hardening). Combined with
|
||||||
# binding in the refresh JWT payload, this tightens the exploitation window
|
# device-fingerprint binding in the JWT payload, this tightens the
|
||||||
# for stolen refresh tokens.
|
# exploitation window for stolen refresh tokens.
|
||||||
JWT_REFRESH_EXPIRY=24h
|
JWT_REFRESH_EXPIRY=24h
|
||||||
|
|
||||||
# Gitea SSO cookie signing + service password salt — REQUIRED 2026-04-12 (P2-2).
|
# Encryption key for DB-stored secrets (SMTP password, etc.)
|
||||||
# Distinct from JWT secrets; empty values will now fail Zod validation on boot.
|
ENCRYPTION_KEY={{secrets.encryptionKey}}
|
||||||
|
|
||||||
|
# Gitea SSO cookie signing secret + service password salt — REQUIRED 2026-04-12
|
||||||
|
# (P2-2). Distinct from JWT secrets; empty values will fail Zod validation on
|
||||||
|
# boot. Both ≥32 chars, distinct from each other and from JWT_* secrets.
|
||||||
GITEA_SSO_SECRET={{secrets.giteaSsoSecret}}
|
GITEA_SSO_SECRET={{secrets.giteaSsoSecret}}
|
||||||
SERVICE_PASSWORD_SALT={{secrets.servicePasswordSalt}}
|
SERVICE_PASSWORD_SALT={{secrets.servicePasswordSalt}}
|
||||||
|
|
||||||
# Encryption
|
# --- Initial Super Admin User ---
|
||||||
ENCRYPTION_KEY={{secrets.encryptionKey}}
|
|
||||||
|
|
||||||
# Initial Admin
|
|
||||||
INITIAL_ADMIN_EMAIL={{secrets.adminEmail}}
|
INITIAL_ADMIN_EMAIL={{secrets.adminEmail}}
|
||||||
INITIAL_ADMIN_PASSWORD={{secrets.initialAdminPassword}}
|
INITIAL_ADMIN_PASSWORD={{secrets.initialAdminPassword}}
|
||||||
|
|
||||||
# API
|
# --- API ---
|
||||||
API_PORT=4000
|
API_PORT=4000
|
||||||
PORT=4000
|
|
||||||
API_URL=https://api.{{domain}}
|
API_URL=https://api.{{domain}}
|
||||||
CORS_ORIGINS=https://app.{{domain}},http://localhost:{{ports.admin}},http://localhost
|
CORS_ORIGINS=https://app.{{domain}},http://localhost:{{ports.admin}},http://localhost
|
||||||
|
|
||||||
|
# --- Admin GUI ---
|
||||||
|
ADMIN_PORT=3000
|
||||||
ADMIN_URL=https://app.{{domain}}
|
ADMIN_URL=https://app.{{domain}}
|
||||||
|
|
||||||
# Admin GUI
|
# --- Nginx ---
|
||||||
ADMIN_PORT=3000
|
|
||||||
|
|
||||||
# Nginx
|
|
||||||
NGINX_HTTP_PORT={{ports.nginx}}
|
NGINX_HTTP_PORT={{ports.nginx}}
|
||||||
NGINX_HTTPS_PORT=443
|
NGINX_HTTPS_PORT=443
|
||||||
|
|
||||||
# SMTP / Email
|
# --- Embed Proxy Ports ---
|
||||||
|
# Dedicated nginx ports for iframe embedding without DNS/subdomain.
|
||||||
|
# CCP allocates these per-instance via {{ports.embed}} base + offset.
|
||||||
|
NOCODB_EMBED_PORT={{math ports.embed "+" 0}}
|
||||||
|
N8N_EMBED_PORT={{math ports.embed "+" 1}}
|
||||||
|
GITEA_EMBED_PORT={{math ports.embed "+" 2}}
|
||||||
|
MAILHOG_EMBED_PORT={{math ports.embed "+" 3}}
|
||||||
|
MINI_QR_EMBED_PORT={{math ports.embed "+" 4}}
|
||||||
|
EXCALIDRAW_EMBED_PORT={{math ports.embed "+" 5}}
|
||||||
|
HOMEPAGE_EMBED_PORT={{math ports.embed "+" 6}}
|
||||||
|
VAULTWARDEN_EMBED_PORT={{math ports.embed "+" 9}}
|
||||||
|
ROCKETCHAT_EMBED_PORT={{math ports.embed "+" 10}}
|
||||||
|
GANCIO_EMBED_PORT={{math ports.embed "+" 11}}
|
||||||
|
JITSI_EMBED_PORT={{math ports.embed "+" 15}}
|
||||||
|
GRAFANA_EMBED_PORT={{math ports.embed "+" 12}}
|
||||||
|
ALERTMANAGER_EMBED_PORT={{math ports.embed "+" 16}}
|
||||||
|
|
||||||
|
# --- Docker / Container Management ---
|
||||||
|
DOCKER_NETWORK_NAME=changemaker-lite
|
||||||
|
DOCKER_PROXY_URL=http://docker-socket-proxy:2375
|
||||||
|
NEWT_CONTAINER_NAME=newt-changemaker
|
||||||
|
NEWT_COMPOSE_SERVICE=newt
|
||||||
|
|
||||||
|
# --- SMTP / Email ---
|
||||||
{{#if emailTestMode}}
|
{{#if emailTestMode}}
|
||||||
SMTP_HOST={{containerPrefix}}-mailhog
|
SMTP_HOST=mailhog-changemaker
|
||||||
SMTP_PORT=1025
|
SMTP_PORT=1025
|
||||||
SMTP_USER=
|
SMTP_USER=
|
||||||
SMTP_PASS=
|
SMTP_PASS=
|
||||||
@ -75,21 +105,9 @@ SMTP_FROM={{smtpFrom}}
|
|||||||
SMTP_FROM_NAME={{name}}
|
SMTP_FROM_NAME={{name}}
|
||||||
TEST_EMAIL_RECIPIENT={{secrets.adminEmail}}
|
TEST_EMAIL_RECIPIENT={{secrets.adminEmail}}
|
||||||
|
|
||||||
# NocoDB
|
# --- Listmonk ---
|
||||||
NOCODB_V2_PORT=8080
|
LISTMONK_PORT=9001
|
||||||
NOCODB_URL=http://{{containerPrefix}}-nocodb:8080
|
LISTMONK_DB_PORT=5434
|
||||||
NC_ADMIN_EMAIL={{secrets.adminEmail}}
|
|
||||||
NC_ADMIN_PASSWORD={{secrets.nocodbAdminPassword}}
|
|
||||||
|
|
||||||
# Listmonk
|
|
||||||
{{#if enableListmonk}}
|
|
||||||
LISTMONK_SYNC_ENABLED=true
|
|
||||||
LISTMONK_URL=http://{{containerPrefix}}-listmonk:9000
|
|
||||||
{{else}}
|
|
||||||
LISTMONK_SYNC_ENABLED=false
|
|
||||||
LISTMONK_URL=
|
|
||||||
{{/if}}
|
|
||||||
LISTMONK_PORT=9000
|
|
||||||
LISTMONK_DB_USER=listmonk
|
LISTMONK_DB_USER=listmonk
|
||||||
LISTMONK_DB_PASSWORD={{secrets.listmonkAdminPassword}}
|
LISTMONK_DB_PASSWORD={{secrets.listmonkAdminPassword}}
|
||||||
LISTMONK_DB_NAME=listmonk
|
LISTMONK_DB_NAME=listmonk
|
||||||
@ -99,26 +117,41 @@ LISTMONK_API_USER=v2-api
|
|||||||
LISTMONK_API_TOKEN={{secrets.listmonkApiToken}}
|
LISTMONK_API_TOKEN={{secrets.listmonkApiToken}}
|
||||||
LISTMONK_ADMIN_USER=v2-api
|
LISTMONK_ADMIN_USER=v2-api
|
||||||
LISTMONK_ADMIN_PASSWORD={{secrets.listmonkApiToken}}
|
LISTMONK_ADMIN_PASSWORD={{secrets.listmonkApiToken}}
|
||||||
LISTMONK_PROXY_PORT=9002
|
LISTMONK_SYNC_ENABLED={{#if enableListmonk}}true{{else}}false{{/if}}
|
||||||
LISTMONK_WEBHOOK_SECRET=
|
LISTMONK_WEBHOOK_SECRET=
|
||||||
LISTMONK_DB_PORT=5434
|
LISTMONK_PROXY_PORT=9002
|
||||||
LISTMONK_SMTP_HOST={{containerPrefix}}-mailhog
|
LISTMONK_SMTP_HOST=mailhog-changemaker
|
||||||
LISTMONK_SMTP_PORT=1025
|
LISTMONK_SMTP_PORT=1025
|
||||||
LISTMONK_SMTP_USER=
|
LISTMONK_SMTP_USER=
|
||||||
LISTMONK_SMTP_PASSWORD=
|
LISTMONK_SMTP_PASSWORD=
|
||||||
LISTMONK_SMTP_TLS_TYPE=none
|
LISTMONK_SMTP_TLS_TYPE=none
|
||||||
LISTMONK_SMTP_FROM={{name}} <noreply@{{domain}}>
|
LISTMONK_SMTP_FROM={{name}} <noreply@{{domain}}>
|
||||||
|
|
||||||
# Media
|
# --- Represent API (Canadian electoral data) ---
|
||||||
{{#if enableMedia}}
|
REPRESENT_API_URL=https://represent.opennorth.ca
|
||||||
ENABLE_MEDIA_FEATURES=true
|
|
||||||
MEDIA_API_PUBLIC_URL=https://media.{{domain}}
|
# --- NocoDB v2 (read-only data browser) ---
|
||||||
{{else}}
|
NOCODB_V2_PORT=8091
|
||||||
ENABLE_MEDIA_FEATURES=false
|
NOCODB_URL=http://changemaker-v2-nocodb:8080
|
||||||
MEDIA_API_PUBLIC_URL=
|
NOCODB_PORT=8091
|
||||||
{{/if}}
|
NC_ADMIN_EMAIL={{secrets.adminEmail}}
|
||||||
|
NC_ADMIN_PASSWORD={{secrets.nocodbAdminPassword}}
|
||||||
|
NC_PUBLIC_URL=https://db.{{domain}}
|
||||||
|
|
||||||
|
# --- Redis ---
|
||||||
|
REDIS_PASSWORD={{secrets.redisPassword}}
|
||||||
|
REDIS_URL=redis://:${REDIS_PASSWORD}@redis-changemaker:6379
|
||||||
|
|
||||||
|
# --- Payments (Stripe) ---
|
||||||
|
ENABLE_PAYMENTS={{#if enablePayments}}true{{else}}false{{/if}}
|
||||||
|
|
||||||
|
# --- Media Management ---
|
||||||
|
ENABLE_MEDIA_FEATURES={{#if enableMedia}}true{{else}}false{{/if}}
|
||||||
MEDIA_API_PORT=4100
|
MEDIA_API_PORT=4100
|
||||||
MEDIA_ROOT=/media/local
|
MEDIA_API_PUBLIC_URL=https://media.{{domain}}
|
||||||
|
VITE_MEDIA_API_URL=http://changemaker-media-api:4100
|
||||||
|
ENABLE_HLS_TRANSCODE=false
|
||||||
|
MEDIA_ROOT=/media/library
|
||||||
MEDIA_UPLOADS=/media/uploads
|
MEDIA_UPLOADS=/media/uploads
|
||||||
MAX_UPLOAD_SIZE_GB=10
|
MAX_UPLOAD_SIZE_GB=10
|
||||||
PUBLIC_MEDIA_PORT=3100
|
PUBLIC_MEDIA_PORT=3100
|
||||||
@ -129,43 +162,111 @@ VIDEO_SCHEDULE_DEFAULT_TIMEZONE=UTC
|
|||||||
VIDEO_SCHEDULE_NOTIFICATION_ENABLED=true
|
VIDEO_SCHEDULE_NOTIFICATION_ENABLED=true
|
||||||
VIDEO_PREVIEW_LINK_EXPIRY_HOURS=24
|
VIDEO_PREVIEW_LINK_EXPIRY_HOURS=24
|
||||||
|
|
||||||
# NAR Data
|
# --- Container Registry ---
|
||||||
NAR_DATA_DIR=/data
|
GITEA_REGISTRY=gitea.bnkops.com/admin
|
||||||
|
IMAGE_TAG={{imageTag}}
|
||||||
|
COMPOSE_PROFILES={{#if enableMonitoring}}monitoring{{/if}}{{#if enableCcpAgent}}{{#if enableMonitoring}},{{/if}}ccp-agent{{/if}}
|
||||||
|
GITEA_REGISTRY_USER=admin
|
||||||
|
GITEA_REGISTRY_PASS=
|
||||||
|
GITEA_REGISTRY_API_TOKEN=
|
||||||
|
|
||||||
# Platform Service URLs (used for health checks)
|
# --- Gitea (Local Platform Instance) ---
|
||||||
MINI_QR_URL=http://{{containerPrefix}}-mini-qr:8080
|
GITEA_URL=http://gitea-changemaker:3000
|
||||||
EXCALIDRAW_URL=http://{{containerPrefix}}-excalidraw:80
|
GITEA_PORT=3030
|
||||||
|
GITEA_WEB_PORT=3030
|
||||||
|
GITEA_SSH_PORT=2222
|
||||||
|
GITEA_ADMIN_USER=admin
|
||||||
|
GITEA_ADMIN_PASSWORD={{secrets.giteaAdminPassword}}
|
||||||
|
GITEA_DB_TYPE=mysql
|
||||||
|
GITEA_DB_HOST=gitea-db:3306
|
||||||
|
GITEA_DB_NAME=gitea
|
||||||
|
GITEA_DB_USER=gitea
|
||||||
|
GITEA_DB_PASSWD={{secrets.giteaAdminPassword}}
|
||||||
|
GITEA_DB_ROOT_PASSWORD={{secrets.giteaAdminPassword}}
|
||||||
|
GITEA_ROOT_URL=https://git.{{domain}}
|
||||||
|
GITEA_DOMAIN=git.{{domain}}
|
||||||
|
|
||||||
|
# --- Gitea Docs Comments ---
|
||||||
|
GITEA_COMMENTS_ENABLED=false
|
||||||
|
GITEA_API_TOKEN=
|
||||||
|
GITEA_COMMENTS_REPO_OWNER=
|
||||||
|
GITEA_COMMENTS_REPO_NAME=docs-comments
|
||||||
|
GITEA_OAUTH_CLIENT_ID=
|
||||||
|
GITEA_OAUTH_CLIENT_SECRET=
|
||||||
|
# Docs source (Gitea repo containing the mkdocs/ tree)
|
||||||
|
GITEA_DOCS_REPO=admin/changemaker.lite
|
||||||
|
GITEA_DOCS_PREFIX=mkdocs/docs
|
||||||
|
GITEA_DOCS_BRANCH=v2
|
||||||
|
|
||||||
|
# --- n8n ---
|
||||||
|
N8N_URL=http://n8n-changemaker:5678
|
||||||
|
N8N_PORT=5678
|
||||||
|
N8N_HOST=n8n.{{domain}}
|
||||||
|
N8N_ENCRYPTION_KEY={{secrets.n8nEncryptionKey}}
|
||||||
|
N8N_USER_EMAIL={{secrets.adminEmail}}
|
||||||
|
N8N_USER_PASSWORD={{secrets.nocodbAdminPassword}}
|
||||||
|
GENERIC_TIMEZONE=UTC
|
||||||
|
|
||||||
|
# --- MkDocs ---
|
||||||
|
MKDOCS_PORT=4003
|
||||||
|
MKDOCS_SITE_SERVER_PORT=4004
|
||||||
|
BASE_DOMAIN=https://{{domain}}
|
||||||
|
MKDOCS_PREVIEW_URL=http://mkdocs:8000
|
||||||
|
MKDOCS_DOCS_PATH=/mkdocs/docs
|
||||||
|
|
||||||
|
# --- Code Server ---
|
||||||
|
CODE_SERVER_PORT=8888
|
||||||
|
CODE_SERVER_URL=http://code-server-changemaker:8443
|
||||||
|
USER_NAME=coder
|
||||||
|
|
||||||
|
# --- Homepage ---
|
||||||
|
HOMEPAGE_PORT=3010
|
||||||
|
HOMEPAGE_VAR_BASE_URL=http://localhost
|
||||||
|
|
||||||
|
# --- Mini QR ---
|
||||||
|
MINI_QR_PORT=8089
|
||||||
|
MINI_QR_URL=http://mini-qr:8080
|
||||||
|
|
||||||
|
# --- Excalidraw (Collaborative Whiteboard) ---
|
||||||
|
EXCALIDRAW_PORT=8090
|
||||||
|
EXCALIDRAW_URL=http://excalidraw-changemaker:80
|
||||||
EXCALIDRAW_WS_URL=wss://draw.{{domain}}
|
EXCALIDRAW_WS_URL=wss://draw.{{domain}}
|
||||||
HOMEPAGE_URL=http://{{containerPrefix}}-homepage:3000
|
|
||||||
VAULTWARDEN_URL=http://{{containerPrefix}}-vaultwarden:80
|
# --- Vaultwarden (Password Manager) ---
|
||||||
|
VAULTWARDEN_PORT=8445
|
||||||
|
VAULTWARDEN_URL=http://vaultwarden-changemaker:80
|
||||||
VAULTWARDEN_ADMIN_TOKEN={{secrets.vaultwardenAdminToken}}
|
VAULTWARDEN_ADMIN_TOKEN={{secrets.vaultwardenAdminToken}}
|
||||||
VAULTWARDEN_DOMAIN=https://vault.{{domain}}
|
VAULTWARDEN_DOMAIN=https://vault.{{domain}}
|
||||||
VAULTWARDEN_SIGNUPS_ALLOWED=false
|
VAULTWARDEN_SIGNUPS_ALLOWED=false
|
||||||
VAULTWARDEN_WEBSOCKET_ENABLED=true
|
VAULTWARDEN_WEBSOCKET_ENABLED=true
|
||||||
VAULTWARDEN_SMTP_SECURITY=off
|
VAULTWARDEN_SMTP_SECURITY=off
|
||||||
|
|
||||||
# Geocoding
|
# --- MailHog ---
|
||||||
|
MAILHOG_SMTP_PORT=1025
|
||||||
|
MAILHOG_WEB_PORT=8025
|
||||||
|
|
||||||
|
# --- NAR (National Address Register) ---
|
||||||
|
NAR_DATA_DIR=/data
|
||||||
|
|
||||||
|
# --- Overpass / Area Import ---
|
||||||
|
OVERPASS_API_URL=https://overpass-api.de/api/interpreter
|
||||||
|
OVERPASS_MIN_DELAY_MS=30000
|
||||||
|
AREA_IMPORT_MAX_GRID_POINTS=500
|
||||||
|
|
||||||
|
# --- Geocoding ---
|
||||||
MAPBOX_API_KEY=
|
MAPBOX_API_KEY=
|
||||||
GOOGLE_MAPS_API_KEY=
|
|
||||||
GOOGLE_MAPS_ENABLED=false
|
|
||||||
GEOCODING_RATE_LIMIT_MS=1100
|
GEOCODING_RATE_LIMIT_MS=1100
|
||||||
GEOCODING_CACHE_ENABLED=true
|
GEOCODING_CACHE_ENABLED=true
|
||||||
GEOCODING_CACHE_TTL_HOURS=24
|
GEOCODING_CACHE_TTL_HOURS=24
|
||||||
|
GOOGLE_MAPS_API_KEY=
|
||||||
|
GOOGLE_MAPS_ENABLED=false
|
||||||
GEOCODING_PARALLEL_ENABLED=true
|
GEOCODING_PARALLEL_ENABLED=true
|
||||||
GEOCODING_BATCH_SIZE=10
|
GEOCODING_BATCH_SIZE=10
|
||||||
BULK_GEOCODE_ENABLED=true
|
BULK_GEOCODE_ENABLED=true
|
||||||
BULK_GEOCODE_MAX_BATCH=5000
|
BULK_GEOCODE_MAX_BATCH=5000
|
||||||
|
|
||||||
# Represent API
|
# --- Pangolin Tunnel ---
|
||||||
REPRESENT_API_URL=https://represent.opennorth.ca
|
PANGOLIN_API_URL=https://api.bnkserve.org/v1
|
||||||
|
|
||||||
# Overpass / Area Import
|
|
||||||
OVERPASS_API_URL=https://overpass-api.de/api/interpreter
|
|
||||||
OVERPASS_MIN_DELAY_MS=30000
|
|
||||||
AREA_IMPORT_MAX_GRID_POINTS=500
|
|
||||||
|
|
||||||
# Pangolin Tunnel
|
|
||||||
PANGOLIN_API_URL=
|
|
||||||
PANGOLIN_API_KEY=
|
PANGOLIN_API_KEY=
|
||||||
PANGOLIN_ORG_ID=
|
PANGOLIN_ORG_ID=
|
||||||
PANGOLIN_SITE_ID=
|
PANGOLIN_SITE_ID=
|
||||||
@ -174,178 +275,95 @@ PANGOLIN_ENDPOINT={{pangolin.endpoint}}
|
|||||||
PANGOLIN_NEWT_ID={{pangolin.newtId}}
|
PANGOLIN_NEWT_ID={{pangolin.newtId}}
|
||||||
PANGOLIN_NEWT_SECRET={{pangolin.newtSecret}}
|
PANGOLIN_NEWT_SECRET={{pangolin.newtSecret}}
|
||||||
{{else}}
|
{{else}}
|
||||||
PANGOLIN_ENDPOINT=
|
PANGOLIN_ENDPOINT=https://pangolin.bnkserve.org
|
||||||
PANGOLIN_NEWT_ID=
|
PANGOLIN_NEWT_ID=
|
||||||
PANGOLIN_NEWT_SECRET=
|
PANGOLIN_NEWT_SECRET=
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
# Gancio
|
# --- Prisma CLI (host-side only, NOT used by Docker containers) ---
|
||||||
{{#if enableGancio}}
|
DATABASE_URL=postgresql://changemaker:{{secrets.postgresPassword}}@localhost:{{ports.postgres}}/changemaker_v2
|
||||||
GANCIO_SYNC_ENABLED=true
|
|
||||||
GANCIO_URL=http://{{containerPrefix}}-gancio:13120
|
# --- Rocket.Chat (Team Chat) ---
|
||||||
{{else}}
|
ENABLE_CHAT={{#if enableChat}}true{{else}}false{{/if}}
|
||||||
GANCIO_SYNC_ENABLED=false
|
ROCKETCHAT_ADMIN_USER=rcadmin
|
||||||
GANCIO_URL=
|
ROCKETCHAT_ADMIN_PASSWORD={{secrets.rocketchatAdminPassword}}
|
||||||
{{/if}}
|
ROCKETCHAT_URL=http://rocketchat-changemaker:3000
|
||||||
|
MONGO_ROOT_USER=rocketchat
|
||||||
|
MONGO_ROOT_PASSWORD={{secrets.mongoRootPassword}}
|
||||||
|
|
||||||
|
# --- Gancio (Event Management) ---
|
||||||
|
GANCIO_PORT=8092
|
||||||
|
GANCIO_URL=http://gancio-changemaker:13120
|
||||||
GANCIO_BASE_URL=https://events.{{domain}}
|
GANCIO_BASE_URL=https://events.{{domain}}
|
||||||
GANCIO_ADMIN_USER=admin
|
GANCIO_ADMIN_USER=admin
|
||||||
GANCIO_ADMIN_PASSWORD={{secrets.gancioAdminPassword}}
|
GANCIO_ADMIN_PASSWORD={{secrets.gancioAdminPassword}}
|
||||||
GANCIO_PORT=8092
|
GANCIO_SYNC_ENABLED={{#if enableGancio}}true{{else}}false{{/if}}
|
||||||
|
|
||||||
# Chat (Rocket.Chat)
|
# --- Jitsi Meet (Video Conferencing) ---
|
||||||
{{#if enableChat}}
|
|
||||||
ENABLE_CHAT=true
|
|
||||||
ROCKETCHAT_URL=http://{{containerPrefix}}-rocketchat:3000
|
|
||||||
ROCKETCHAT_ADMIN_USER=rcadmin
|
|
||||||
ROCKETCHAT_ADMIN_PASSWORD={{secrets.rocketchatAdminPassword}}
|
|
||||||
MONGO_ROOT_USER=rocketchat
|
|
||||||
MONGO_ROOT_PASSWORD={{secrets.mongoRootPassword}}
|
|
||||||
{{else}}
|
|
||||||
ENABLE_CHAT=false
|
|
||||||
ROCKETCHAT_URL=
|
|
||||||
ROCKETCHAT_ADMIN_USER=
|
|
||||||
ROCKETCHAT_ADMIN_PASSWORD=
|
|
||||||
MONGO_ROOT_USER=
|
|
||||||
MONGO_ROOT_PASSWORD=
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
# Jitsi Meet (Video Conferencing)
|
|
||||||
ENABLE_MEET={{#if enableMeet}}true{{else}}false{{/if}}
|
ENABLE_MEET={{#if enableMeet}}true{{else}}false{{/if}}
|
||||||
{{#if enableMeet}}
|
|
||||||
JITSI_APP_ID=changemaker
|
JITSI_APP_ID=changemaker
|
||||||
JITSI_APP_SECRET={{secrets.jitsiAppSecret}}
|
JITSI_APP_SECRET={{secrets.jitsiAppSecret}}
|
||||||
JITSI_JICOFO_AUTH_PASSWORD={{secrets.jitsiJicofoAuthPassword}}
|
JITSI_JICOFO_AUTH_PASSWORD={{secrets.jitsiJicofoAuthPassword}}
|
||||||
JITSI_JVB_AUTH_PASSWORD={{secrets.jitsiJvbAuthPassword}}
|
JITSI_JVB_AUTH_PASSWORD={{secrets.jitsiJvbAuthPassword}}
|
||||||
JITSI_URL=http://{{containerPrefix}}-jitsi-web:80
|
JITSI_URL=http://jitsi-web-changemaker:80
|
||||||
JVB_ADVERTISE_IP={{jvbAdvertiseIp}}
|
JVB_ADVERTISE_IP={{jvbAdvertiseIp}}
|
||||||
JVB_PORT=10000
|
JVB_PORT=10000
|
||||||
{{else}}
|
|
||||||
JITSI_APP_ID=
|
|
||||||
JITSI_APP_SECRET=
|
|
||||||
JITSI_JICOFO_AUTH_PASSWORD=
|
|
||||||
JITSI_JVB_AUTH_PASSWORD=
|
|
||||||
JITSI_URL=
|
|
||||||
JVB_ADVERTISE_IP=
|
|
||||||
JVB_PORT=10000
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
# SMS Campaigns
|
# --- SMS Campaigns (Termux Android Bridge) ---
|
||||||
ENABLE_SMS={{#if enableSms}}true{{else}}false{{/if}}
|
ENABLE_SMS={{#if enableSms}}true{{else}}false{{/if}}
|
||||||
TERMUX_API_URL=
|
TERMUX_API_URL=
|
||||||
TERMUX_API_KEY=
|
TERMUX_API_KEY=
|
||||||
SMS_DELAY_BETWEEN_MS=3000
|
SMS_DELAY_BETWEEN_MS=3000
|
||||||
SMS_MAX_RETRIES=3
|
SMS_MAX_RETRIES=3
|
||||||
SMS_RESPONSE_SYNC_INTERVAL_MS=30000
|
SMS_RESPONSE_SYNC_INTERVAL_MS=120000
|
||||||
SMS_DEVICE_MONITOR_INTERVAL_MS=30000
|
SMS_DEVICE_MONITOR_INTERVAL_MS=300000
|
||||||
|
|
||||||
# Social Connections
|
# --- Social, People & Analytics ---
|
||||||
ENABLE_SOCIAL={{#if enableSocial}}true{{else}}false{{/if}}
|
ENABLE_SOCIAL={{#if enableSocial}}true{{else}}false{{/if}}
|
||||||
|
|
||||||
# People CRM
|
|
||||||
ENABLE_PEOPLE={{#if enablePeople}}true{{else}}false{{/if}}
|
ENABLE_PEOPLE={{#if enablePeople}}true{{else}}false{{/if}}
|
||||||
|
|
||||||
# Analytics & GeoIP
|
|
||||||
ENABLE_ANALYTICS={{#if enableAnalytics}}true{{else}}false{{/if}}
|
ENABLE_ANALYTICS={{#if enableAnalytics}}true{{else}}false{{/if}}
|
||||||
MAXMIND_ACCOUNT_ID=
|
|
||||||
MAXMIND_LICENSE_KEY=
|
|
||||||
|
|
||||||
# Monitoring
|
# --- Control Panel Agent ---
|
||||||
|
# Tenants registered with CCP have these populated; CCP-provisioned tenants
|
||||||
|
# get them set by the provisioner. Leaving blank if neither applies.
|
||||||
|
ENABLE_CCP_AGENT=true
|
||||||
|
CCP_URL=
|
||||||
|
CCP_INVITE_CODE=
|
||||||
|
CCP_AGENT_URL=
|
||||||
|
CCP_AGENT_PORT=7443
|
||||||
|
|
||||||
|
# --- Monitoring (only used with --profile monitoring) ---
|
||||||
|
PROMETHEUS_PORT=9090
|
||||||
|
GRAFANA_PORT=3005
|
||||||
GRAFANA_ADMIN_PASSWORD={{secrets.grafanaAdminPassword}}
|
GRAFANA_ADMIN_PASSWORD={{secrets.grafanaAdminPassword}}
|
||||||
GRAFANA_ROOT_URL=https://grafana.{{domain}}
|
GRAFANA_ROOT_URL=https://grafana.{{domain}}
|
||||||
PROMETHEUS_PORT=9090
|
|
||||||
GRAFANA_PORT=3000
|
|
||||||
CADVISOR_PORT=8086
|
CADVISOR_PORT=8086
|
||||||
NODE_EXPORTER_PORT=9100
|
NODE_EXPORTER_PORT=9100
|
||||||
REDIS_EXPORTER_PORT=9121
|
REDIS_EXPORTER_PORT=9121
|
||||||
ALERTMANAGER_PORT=9093
|
ALERTMANAGER_PORT=9093
|
||||||
ALERTMANAGER_EMBED_PORT={{math ports.embed "+" 16}}
|
|
||||||
GOTIFY_PORT=8889
|
GOTIFY_PORT=8889
|
||||||
GOTIFY_ADMIN_USER=admin
|
GOTIFY_ADMIN_USER=admin
|
||||||
GOTIFY_ADMIN_PASSWORD=admin
|
GOTIFY_ADMIN_PASSWORD=admin
|
||||||
|
|
||||||
# MkDocs
|
# --- Bunker Ops (Fleet Management) ---
|
||||||
MKDOCS_PORT={{math ports.embed "+" 8}}
|
|
||||||
MKDOCS_SITE_SERVER_PORT={{math ports.embed "+" 14}}
|
|
||||||
MKDOCS_PREVIEW_URL=http://{{containerPrefix}}-mkdocs:8000
|
|
||||||
MKDOCS_DOCS_PATH=/mkdocs/docs
|
|
||||||
CODE_SERVER_PORT={{math ports.embed "+" 7}}
|
|
||||||
CODE_SERVER_URL=http://{{containerPrefix}}-code-server:8443
|
|
||||||
BASE_DOMAIN=https://{{domain}}
|
|
||||||
|
|
||||||
# Gitea
|
|
||||||
GITEA_URL=http://{{containerPrefix}}-gitea:3000
|
|
||||||
GITEA_SSH_PORT=2222
|
|
||||||
GITEA_DB_TYPE=postgres
|
|
||||||
GITEA_DB_HOST={{containerPrefix}}-postgres:5432
|
|
||||||
GITEA_DB_NAME=gitea
|
|
||||||
GITEA_DB_USER=changemaker
|
|
||||||
GITEA_DB_PASSWD={{secrets.postgresPassword}}
|
|
||||||
GITEA_ROOT_URL=https://git.{{domain}}
|
|
||||||
GITEA_DOMAIN=git.{{domain}}
|
|
||||||
GITEA_COMMENTS_ENABLED=false
|
|
||||||
GITEA_API_TOKEN=
|
|
||||||
GITEA_COMMENTS_REPO_OWNER=
|
|
||||||
GITEA_COMMENTS_REPO_NAME=docs-comments
|
|
||||||
GITEA_OAUTH_CLIENT_ID=
|
|
||||||
GITEA_OAUTH_CLIENT_SECRET=
|
|
||||||
|
|
||||||
# n8n
|
|
||||||
N8N_HOST=n8n.{{domain}}
|
|
||||||
N8N_URL=http://{{containerPrefix}}-n8n:5678
|
|
||||||
N8N_ENCRYPTION_KEY={{secrets.n8nEncryptionKey}}
|
|
||||||
N8N_USER_EMAIL={{secrets.adminEmail}}
|
|
||||||
N8N_USER_PASSWORD={{secrets.nocodbAdminPassword}}
|
|
||||||
GENERIC_TIMEZONE=UTC
|
|
||||||
|
|
||||||
# MailHog
|
|
||||||
MAILHOG_URL=http://{{containerPrefix}}-mailhog:8025
|
|
||||||
MAILHOG_SMTP_PORT=1025
|
|
||||||
MAILHOG_WEB_PORT=8025
|
|
||||||
|
|
||||||
# Homepage
|
|
||||||
HOMEPAGE_PORT=3010
|
|
||||||
HOMEPAGE_VAR_BASE_URL=http://localhost
|
|
||||||
|
|
||||||
# Dev Tools
|
|
||||||
{{#if enableDevTools}}
|
|
||||||
ENABLE_DEV_TOOLS=true
|
|
||||||
{{else}}
|
|
||||||
ENABLE_DEV_TOOLS=false
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
# Payments
|
|
||||||
{{#if enablePayments}}
|
|
||||||
ENABLE_PAYMENTS=true
|
|
||||||
{{else}}
|
|
||||||
ENABLE_PAYMENTS=false
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
# Vite (admin build)
|
|
||||||
VITE_API_URL=http://{{containerPrefix}}-api:4000
|
|
||||||
VITE_MKDOCS_URL=http://{{containerPrefix}}-mkdocs:8000
|
|
||||||
{{#if enableMedia}}
|
|
||||||
VITE_MEDIA_API_URL=http://{{containerPrefix}}-media-api:4100
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
# Bunker Ops (Fleet Management)
|
|
||||||
INSTANCE_LABEL={{slug}}
|
INSTANCE_LABEL={{slug}}
|
||||||
BUNKER_OPS_ENABLED=false
|
BUNKER_OPS_ENABLED=false
|
||||||
BUNKER_OPS_REMOTE_WRITE_URL=
|
BUNKER_OPS_REMOTE_WRITE_URL=
|
||||||
|
|
||||||
# Embed proxy ports (nginx proxy for iframe embedding in admin GUI)
|
# --- GeoIP (MaxMind GeoLite2) ---
|
||||||
NOCODB_EMBED_PORT={{math ports.embed "+" 0}}
|
MAXMIND_ACCOUNT_ID=
|
||||||
N8N_EMBED_PORT={{math ports.embed "+" 1}}
|
MAXMIND_LICENSE_KEY=
|
||||||
GITEA_EMBED_PORT={{math ports.embed "+" 2}}
|
|
||||||
MAILHOG_EMBED_PORT={{math ports.embed "+" 3}}
|
# --- CCP-specific (admin GUI iframe embeds + dev-mode helpers) ---
|
||||||
MINI_QR_EMBED_PORT={{math ports.embed "+" 4}}
|
# These are CCP-only — not in canonical .env.example. Kept here because
|
||||||
EXCALIDRAW_EMBED_PORT={{math ports.embed "+" 5}}
|
# admin/vite uses them at build time and the embed proxies reference them.
|
||||||
HOMEPAGE_EMBED_PORT={{math ports.embed "+" 6}}
|
PORT=4000
|
||||||
|
VITE_API_URL=http://changemaker-v2-api:4000
|
||||||
|
HOMEPAGE_URL=http://homepage-changemaker:3000
|
||||||
|
MAILHOG_URL=http://mailhog-changemaker:8025
|
||||||
|
LISTMONK_URL=http://listmonk-app:9000
|
||||||
CODE_SERVER_EMBED_PORT={{math ports.embed "+" 7}}
|
CODE_SERVER_EMBED_PORT={{math ports.embed "+" 7}}
|
||||||
MKDOCS_EMBED_PORT={{math ports.embed "+" 8}}
|
MKDOCS_EMBED_PORT={{math ports.embed "+" 8}}
|
||||||
VAULTWARDEN_EMBED_PORT={{math ports.embed "+" 9}}
|
|
||||||
ROCKETCHAT_EMBED_PORT={{math ports.embed "+" 10}}
|
|
||||||
GANCIO_EMBED_PORT={{math ports.embed "+" 11}}
|
|
||||||
GRAFANA_EMBED_PORT={{math ports.embed "+" 12}}
|
|
||||||
LISTMONK_EMBED_PORT={{math ports.embed "+" 13}}
|
|
||||||
MKDOCS_SITE_EMBED_PORT={{math ports.embed "+" 14}}
|
MKDOCS_SITE_EMBED_PORT={{math ports.embed "+" 14}}
|
||||||
JITSI_EMBED_PORT={{math ports.embed "+" 15}}
|
LISTMONK_EMBED_PORT={{math ports.embed "+" 13}}
|
||||||
|
ENABLE_DEV_TOOLS={{#if enableDevTools}}true{{else}}false{{/if}}
|
||||||
|
|||||||
@ -10,7 +10,14 @@ http {
|
|||||||
include /etc/nginx/mime.types;
|
include /etc/nginx/mime.types;
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
# Redact sensitive query parameters (token, secret) from access logs
|
||||||
|
map $request_uri $redacted_request {
|
||||||
|
~^(?P<path>[^?]*)\?(?P<args>.*token=[^&]*) "$path?<token-redacted>";
|
||||||
|
~^(?P<path>[^?]*)\?(?P<args>.*secret=[^&]*) "$path?<secret-redacted>";
|
||||||
|
default $request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request_method $redacted_request $server_protocol" '
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
@ -25,6 +32,12 @@ http {
|
|||||||
types_hash_max_size 2048;
|
types_hash_max_size 2048;
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
|
|
||||||
|
# Rate limiting zones (defense-in-depth alongside app-level Redis rate limits)
|
||||||
|
limit_req_zone $binary_remote_addr zone=api_global:10m rate=30r/s;
|
||||||
|
limit_req_zone $binary_remote_addr zone=api_auth:10m rate=5r/s;
|
||||||
|
limit_req_zone $binary_remote_addr zone=upload:10m rate=2r/s;
|
||||||
|
limit_req_status 429;
|
||||||
|
|
||||||
# Gzip compression
|
# Gzip compression
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_vary on;
|
gzip_vary on;
|
||||||
@ -32,11 +45,17 @@ http {
|
|||||||
gzip_comp_level 6;
|
gzip_comp_level 6;
|
||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
||||||
|
|
||||||
|
# Only send HSTS when the request arrived over HTTPS (via Pangolin tunnel)
|
||||||
|
map $http_x_forwarded_proto $hsts_header {
|
||||||
|
https "max-age=31536000; includeSubDomains";
|
||||||
|
default "";
|
||||||
|
}
|
||||||
|
|
||||||
# Security headers (applied globally — X-Frame-Options set per server block)
|
# Security headers (applied globally — X-Frame-Options set per server block)
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
add_header Strict-Transport-Security $hsts_header always;
|
||||||
add_header Permissions-Policy "geolocation=(self), microphone=(), camera=()" always;
|
add_header Permissions-Policy "geolocation=(self), microphone=(), camera=()" always;
|
||||||
|
|
||||||
# Docker internal DNS — enables runtime resolution so nginx starts
|
# Docker internal DNS — enables runtime resolution so nginx starts
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user