Secrets & Environment Variables Reference
Every secret and configurable variable across all deployment targets, with example values.
Rule: Never commit real credentials. Use the
.examplefiles as templates and keep actual values in untracked files (.env,secrets-values.yaml,all.yml).
Docker Compose (Local Dev)
File: infra/.env (copy from infra/.env.example) — the Makefile reads from it automatically for all commands.
Secrets
These must be changed from defaults in production. The dev defaults below work out of the box for local development.
| Variable | Dev default | Prod guidance | Used by |
|---|---|---|---|
MONGO_ROOT_PASSWORD |
secret |
Strong random password (24+ chars) | mongodb, user-service, content-service |
MONGO_ROOT_USERNAME |
root |
Keep or change | mongodb, user-service, content-service |
JWT_RSA_PUBLIC_KEY / JWT_RSA_PRIVATE_KEY |
Committed dev key pair (in docker-compose.dev.yaml) |
Generate a fresh RSA pair, inject via secret store | user-service (JWT signing) |
SERVICE_CLIENT_SECRET |
dev-service-secret (in docker-compose.dev.yaml) |
Strong random value (openssl rand -hex 32) |
user-service → content-service subscribe/unsubscribe (client_credentials, scope source.write) |
LLM_API_KEY |
(empty) | Logos API key (lg-..., from tutor); TUM network / eduVPN only |
gen-ai |
GRAFANA_ADMIN_PASSWORD |
admin |
Strong random password (openssl rand -hex 16) |
grafana-lgtm admin login (also reachable at /monitoring via the reverse proxy, not just LGTM_GRAFANA_PORT directly) |
GRAFANA_SMTP_USER |
(empty) | Sending email address (Gmail: the account itself) | grafana-lgtm SMTP auth — alert email delivery |
GRAFANA_SMTP_PASSWORD |
(empty) | Gmail: an App Password, NOT the account password (requires 2FA; generate at myaccount.google.com/apppasswords) | grafana-lgtm SMTP auth |
GRAFANA_ALERT_EMAILS |
(empty) | Comma-separated recipient list | email-alerts contact point (infra/grafana/provisioning/alerting/contactpoints.yaml), read via Grafana's $__env{} provisioning expansion |
Configuration
Safe to leave at defaults for local dev. Override as needed.
| Variable | Dev default | Description |
|---|---|---|
REGISTRY |
ghcr.io/aet-devops26/team-the-rolling-restarts |
Container image registry. Use ghcr.io/<github-username>/rolling-restarts for personal dev |
IMAGE_TAG |
latest |
Image tag. Makefile defaults to current commit SHA |
API_BASE_URL |
http://localhost:8080 |
Gateway base URL for the "test" compose profile (infra/docker-compose.yaml itself hardcodes the correct value per deployment target directly, not from this file — see web-client/src/lib/api/client.ts) |
APP_PORT |
8080 (local) / 80 (VM) |
Host port for the nginx reverse proxy — the single entry point; the web client is served through it at /, not on its own port |
GEN_AI_PORT |
8000 |
Host port for GenAI service |
LLM_PROVIDER |
logos |
LLM provider (logos for cloud, ollama for local) |
LLM_BASE_URL |
https://logos.aet.cit.tum.de/v1 |
Logos OpenAI-compatible endpoint (TUM network / eduVPN only) |
LLM_MODEL |
openai/gpt-oss-120b |
Model name (Logos default; use an Ollama model name when LLM_PROVIDER=ollama) |
INTERNAL_API_URL |
http://api-gateway:8080 |
In-network URL gen-ai uses to fetch articles via the gateway |
OLLAMA_BASE_URL |
http://ollama:11434 |
Ollama API URL (when LLM_PROVIDER=ollama + compose profile local-llm) |
OLLAMA_PORT |
11434 |
Host port for Ollama (only with --profile local-llm) |
LOG_LEVEL |
INFO |
GenAI log level |
MONGO_PORT |
27017 |
Host port for MongoDB |
MONGO_DATABASE |
mydatabase |
MongoDB init database name |
LGTM_GRAFANA_PORT |
3001 |
Host port for Grafana |
LGTM_OTLP_GRPC_PORT |
4317 |
Host port for OTLP gRPC |
LGTM_OTLP_HTTP_PORT |
4318 |
Host port for OTLP HTTP |
GF_SMTP_HOST |
smtp.gmail.com:587 |
SMTP relay host:port for alert email delivery — any provider works, Gmail is the default |
WATCHPACK_POLLING |
false |
Set true on some Linux setups for file watching |
DEMO_USERNAME |
demo |
Username for the demo account make compose-up auto-seeds (infra/scripts/seed-demo-data.sh) — a known login with sources already subscribed, ready for a live demo. Not sensitive by design; the Azure/K8s targets source the same default from the DEMO_USERNAME/DEMO_PASSWORD repo secrets. |
DEMO_PASSWORD |
Demo12345! |
Password for the demo account. |
Example infra/.env (dev)
# Works out of the box for local development
API_BASE_URL=http://localhost:8080
APP_PORT=8080
GEN_AI_PORT=8000
LLM_PROVIDER=logos
LLM_BASE_URL=https://logos.aet.cit.tum.de/v1
LLM_API_KEY=lg-your-logos-key-here
LLM_MODEL=openai/gpt-oss-120b
INTERNAL_API_URL=http://api-gateway:8080
OLLAMA_BASE_URL=http://ollama:11434
LOG_LEVEL=INFO
MONGO_PORT=27017
MONGO_DATABASE=mydatabase
MONGO_ROOT_USERNAME=root
MONGO_ROOT_PASSWORD=secret
LGTM_GRAFANA_PORT=3001
LGTM_OTLP_GRPC_PORT=4317
LGTM_OTLP_HTTP_PORT=4318
GRAFANA_ADMIN_PASSWORD=your-strong-password-here
GF_SMTP_HOST=smtp.gmail.com:587
GRAFANA_SMTP_USER=you@gmail.com
GRAFANA_SMTP_PASSWORD=your-app-password-here
GRAFANA_ALERT_EMAILS=you@gmail.com,teammate@example.com
WATCHPACK_POLLING=false
Azure VM Deployment
File: infra/ansible/group_vars/all.yml (copy from all.yml.example)
All the same variables as Docker Compose, passed via app_env. The Ansible playbook templates them into the .env file on the VM.
Azure VM is off the TUM network — Logos (
https://logos.aet.cit.tum.de/v1) is not reachable from the VM. UseLLM_PROVIDER=ollama, enable the Compose profilelocal-llm(starts an Ollama container), pull a model, and setLLM_MODELto that model name. Alternatively, run gen-ai locally on eduVPN withLLM_PROVIDER=logos.
Additional Ansible variables
| Variable | Example | Description |
|---|---|---|
project_root |
/opt/rolling-restarts |
App install directory on the VM |
registry |
ghcr.io/aet-devops26/team-the-rolling-restarts |
Container image registry. Use ghcr.io/<github-username>/rolling-restarts for personal dev |
image_tag |
latest |
Image tag (overridden by make ansible-deploy IMAGE_TAG=...) |
registry_user |
"" |
GHCR username (for private registries) |
registry_token |
"" |
GHCR PAT with read:packages (for private registries) |
demo_username |
demo |
Username for the demo account seeded after every deploy (infra/ansible/roles/app/tasks/main.yml's "Seed demo user" tasks). Not sensitive by design; defaults live in roles/app/defaults/main.yml, not this file — only set here to use different demo credentials. |
demo_password |
Demo12345! |
Password for the demo account. |
Example infra/ansible/group_vars/all.yml (prod)
---
project_root: /opt/rolling-restarts
registry: ghcr.io/aet-devops26/team-the-rolling-restarts
image_tag: latest
app_env:
APP_PORT: "8080"
GEN_AI_PORT: "8000"
LLM_PROVIDER: "ollama"
LLM_BASE_URL: "https://logos.aet.cit.tum.de/v1"
LLM_API_KEY: ""
LLM_MODEL: "llama3.2"
INTERNAL_API_URL: "http://api-gateway:8080"
OLLAMA_BASE_URL: "http://ollama:11434"
OLLAMA_PORT: "11434"
LOG_LEVEL: "INFO"
MONGO_PORT: "27017"
MONGO_DATABASE: "mydatabase"
MONGO_ROOT_USERNAME: "root"
MONGO_ROOT_PASSWORD: "CHANGE-ME-strong-random" # <-- real secret
LGTM_GRAFANA_PORT: "3001"
LGTM_OTLP_GRPC_PORT: "4317"
LGTM_OTLP_HTTP_PORT: "4318"
GRAFANA_ADMIN_PASSWORD: "CHANGE-ME-strong-random" # <-- real secret
GF_SMTP_HOST: "smtp.gmail.com:587"
GRAFANA_SMTP_USER: "you@gmail.com"
GRAFANA_SMTP_PASSWORD: "CHANGE-ME-app-password" # <-- real secret
GRAFANA_ALERT_EMAILS: "you@gmail.com,teammate@example.com"
Kubernetes / Helm
Helm uses multiple values files layered together: values.yaml (base config, checked in), values-prod.yaml (prod overrides, checked in), values-dev.yaml (dev/manual-deploy overrides, checked in), secrets-values.yaml (credentials, not checked in), and image-values.yaml (image tags, set by CI).
File: infra/helm/secrets-values.yaml (copy from secrets-values.example.yaml)
Secrets (secrets-values.yaml)
These are required — values.yaml ships them as empty strings and templates/secrets.yaml
wraps each in Helm's required function, so helm refuses to render unless secrets-values.yaml
provides every one. No credential material is committed to the chart.
Generate a working file in one command with make helm-secrets (fresh RSA pair + random Mongo
password), or copy secrets-values.example.yaml and fill it in by hand.
| Key | Required | Prod guidance | Consumed by |
|---|---|---|---|
mongodb.rootUsername |
yes | Keep root or change |
mongodb + both mongodb-credentials and mongodb-user-credentials Secrets |
mongodb.rootPassword |
yes | Strong random password | mongodb + both Secrets |
userService.jwtKeys.publicKey / userService.jwtKeys.privateKey |
yes | Fresh RSA pair | jwt-keys Secret → user-service JWT signing |
userService.serviceClientSecret |
yes | Strong random value (openssl rand -hex 32) |
service-credentials Secret → user-service's client_credentials token for content-service subscribe/unsubscribe |
genAi.llmApiKey |
no | Logos API key (lg-...); TUM network / eduVPN only |
llm-credentials Secret → gen-ai cloud LLM calls |
monitoring.adminPassword |
yes | Strong random password (openssl rand -hex 16) |
grafana-admin-credentials Secret (in monitoring.namespace) → grafana-lgtm admin login, reachable at /monitoring via the shared ingress |
monitoring.smtpUser |
yes | Sending email address (Gmail: the account itself) | grafana-smtp-credentials Secret → grafana-lgtm SMTP auth for alert email delivery |
monitoring.smtpPassword |
yes | Gmail: an App Password, NOT the account password (requires 2FA; generate at myaccount.google.com/apppasswords) | grafana-smtp-credentials Secret |
monitoring.alertEmails |
yes | Comma-separated recipient list | grafana-smtp-credentials Secret → email-alerts contact point, read via Grafana's $__env{} provisioning expansion |
Rotating
mongodb.rootPassword: MongoDB only applies the root password on first init (empty data dir). Changing it while themongodb-dataPVC still exists leaves the old password in place and the services fail with "Authentication failed". Wipe the volume to re-initialize:kubectl -n deployment delete deploy mongodb && kubectl -n deployment delete pvc mongodb-data, then redeploy. Separately, even ifsecrets-values.yamlis just being corrected to match a password MongoDB already has (not an actual rotation) —helm upgradeupdates themongodb-credentials/mongodb-user-credentialsSecrets, butuser-service/content-serviceread them once at container start viasecretKeyRefand won't pick up the change on their own; a Secret-value-only change doesn't alter their Deployment's rendered YAML at all, so Kubernetes has no reason to restart them. Confirmed live: they kept failing withAuthenticationFaileduntil explicitly restarted withkubectl rollout restart deployment/user-service deployment/content-service -n deployment.Rotating
monitoring.adminPassword: unlike the above, this one is fully automatic — an initContainer (Kubernetes) / one-shot service (docker-compose) runsgrafana cli admin reset-admin-passwordon every deploy, before Grafana starts, so changing the value and redeploying is enough. Two sharp edges were involved in building it: aResourceQuotarequiring explicit limits on the initContainer, and achecksum/grafana-admin-passwordpod-template annotation needed so a Secret-only value change actually triggers a new pod on the Helm path.
Both services share the same MongoDB instance with data isolation via separate databases:
- content-service connects to database
contentvia themongodb-credentialsSecret - user-service connects to database
usersvia themongodb-user-credentialsSecret
Configuration (values.yaml — checked in)
These are set directly in values.yaml or overridden per-environment.
| Key | Dev value | Prod override | Description |
|---|---|---|---|
global.replicas |
1 |
2 (via values-prod.yaml) |
Pod replica count |
global.tag |
latest |
Commit SHA (via image-values.yaml) |
Container image tag |
apiGateway.env CORS_ALLOWED_ORIGINS |
https://rolling-restarts... |
Your domain | Allowed CORS origins |
apiGateway.env JWT_ISSUER_URI |
http://user-service:8081 |
Keep (cluster-internal) | JWT issuer for token validation |
ingress.clusterIssuer |
letsencrypt-staging |
letsencrypt-prod (via values-prod.yaml) |
TLS certificate issuer |
host |
rolling-restarts.stud... |
Your domain | Ingress hostname (path-based routing) |
monitoring.smtpHost |
smtp.gmail.com:587 |
Your SMTP provider | SMTP relay host:port for alert email delivery |
Example infra/helm/secrets-values.yaml (prod)
All fields shown here are mandatory (genAi.llmApiKey is the one optional exception) — see
secrets-values.example.yaml for the full template with generation commands for each value, or
run make helm-secrets to generate the mongodb/JWT/service-client portion automatically.
mongodb:
rootUsername: root
rootPassword: "a-strong-random-password-here"
userService:
jwtKeys:
publicKey: |
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
privateKey: |
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
serviceClientSecret: "a-strong-random-value-here"
monitoring:
adminPassword: "a-strong-random-password-here"
smtpUser: "you@gmail.com"
smtpPassword: "your-app-password-here"
alertEmails: "you@gmail.com,teammate@example.com"
Helm values file layering
values.yaml # Base config (checked in)
├── values-prod.yaml # Prod overrides: replicas=2, letsencrypt-prod (checked in)
├── values-dev.yaml # Dev overrides: dev. host prefix, 1 replica (checked in; ENV=dev path)
├── secrets-values.yaml # Database credentials (NOT checked in)
└── image-values.yaml # Container image tags, set by CI (checked in)
Dev deploy: make helm-deploy
Prod deploy: make helm-deploy ENV=prod
The Makefile auto-detects secrets-values.yaml when present.
Spring Profiles
The Spring services use profiles to switch between dev and production configuration. The profile is selected by SPRING_PROFILES_ACTIVE (set in Docker Compose or Helm values.yaml).
| Profile | How activated | What it does |
|---|---|---|
dev |
SPRING_PROFILES_ACTIVE=dev or default in IDE |
Hardcoded local MongoDB URIs, debug logging, all actuator endpoints exposed |
production |
Set in Helm values.yaml and Docker Compose |
No hardcoded credentials (env vars required), graceful shutdown, minimal actuator endpoints |
| (base) | Always active | Port config, gateway routes, JWT issuer URI — all read from env vars with sensible fallbacks |
Env vars required per service (production profile)
| Service | Required env vars |
|---|---|
| api-gateway | CORS_ALLOWED_ORIGINS, JWT_ISSUER_URI, USER_SERVICE_URL, CONTENT_SERVICE_URL |
| user-service | SPRING_MONGODB_URI, JWT_ISSUER, JWT_RSA_PUBLIC_KEY, JWT_RSA_PRIVATE_KEY, CONTENT_SERVICE_URL, SERVICE_CLIENT_SECRET |
| content-service | SPRING_MONGODB_URI, JWT_ISSUER_URI |
| gen-ai | LLM_PROVIDER, LLM_BASE_URL, LLM_MODEL, INTERNAL_API_URL; LLM_API_KEY optional (Logos key, TUM network / eduVPN only); OLLAMA_BASE_URL when LLM_PROVIDER=ollama |
JWT issuer must match across services. user-service stamps the
issclaim on tokens fromJWT_ISSUERand serves OIDC discovery at that URL. api-gateway and content-service validate tokens viaJWT_ISSUER_URI, which must resolve to the same user-service URL (defaulthttp://user-service:8081in Compose/Helm). A mismatch causes resource servers to reject every token with 401.JWT signing key must be shared across user-service replicas. user-service signs tokens with the RSA key from
JWT_RSA_PUBLIC_KEY/JWT_RSA_PRIVATE_KEYand derives a deterministic JWKkidfrom its thumbprint. Every replica therefore advertises an identical JWKS, so a token signed by one pod validates against the key served by any pod behind the Service — this is what makes user-service horizontally scalable (replicas: 2). If the keys are unset, each pod generates its own ephemeral key (logged as a warning) and auth breaks intermittently with 2+ replicas. All replicas must receive the same key pair (a singlejwt-keysSecret in Kubernetes, one env value in Compose).
Quick Checklist
Before make compose-up (local dev)
- [ ]
cp infra/.env.example infra/.env - [ ] Set
LLM_API_KEY(Logoslg-...key) if on TUM network / eduVPN and you want cloud LLM - [ ] Or set
LLM_PROVIDER=ollamaand runmake compose-upwith--profile local-llmfor local model
Before make deploy-azure
- [ ]
cp infra/ansible/group_vars/all.yml.example infra/ansible/group_vars/all.yml - [ ] Set a real password for
MONGO_ROOT_PASSWORD - [ ] Set
LLM_PROVIDER=ollama(Azure VM is off TUM network — Logos unreachable); use compose profilelocal-llm - [ ] Set
project_repo_urlto your fork/org
Before make helm-deploy
- [ ]
cp infra/helm/secrets-values.example.yaml infra/helm/secrets-values.yaml - [ ] Set a real password for
mongodb.rootPassword - [ ] Optionally set
genAi.llmApiKeyfor Logos cloud LLM (in-cluster TUM network only) - [ ] For prod:
make helm-deploy ENV=prod