Runtime configuration via env vars
Best practices for .env management
Olympus services read configuration from environment variables. Done well, this is clean. Done poorly, it's fragile.
Layers
.env.sample ← committed; placeholders, docs
.env.local ← per-developer, NOT committed
.env ← per-environment, NOT committed, secrets.env.sample
# Olympus configuration sample
# Copy to .env and fill in real values
# Domain
DOMAIN=your-domain.com
HERA_PUBLIC_URL=https://ciam.${DOMAIN}
ATHENA_PUBLIC_URL=https://iam.${DOMAIN}
# Database
POSTGRES_PASSWORD= # 32-char random, required
POSTGRES_DB=olympus
POSTGRES_USER=olympus
# Kratos secrets (32-byte base64)
KRATOS_SECRETS_COOKIE= # required
KRATOS_SECRETS_CIPHER= # required
# Hydra secrets
HYDRA_SECRETS_SYSTEM= # required, 16+ char
HYDRA_SECRETS_COOKIE= # required
# Encryption
OLYMPUS_ENCRYPTION_KEY_PRIMARY= # 32-byte base64, REQUIRED
OLYMPUS_ENCRYPTION_KEY_ID=2026-01 # rotation marker
# Email courier
COURIER_SMTP_HOST=
COURIER_SMTP_PORT=587
COURIER_SMTP_USER=
COURIER_SMTP_PASSWORD= # required for sending email
# Optional: image tag overrides
KRATOS_TAG=v1.4.0
HYDRA_TAG=v2.2.0
HERA_TAG=v1.4.0
ATHENA_TAG=v1.4.0Commented. Marked required vs optional.
Generating secrets
# Random secrets
openssl rand -base64 32
# Or via SDK
node -e "console.log(crypto.randomBytes(32).toString('base64'))".env validation script
#!/bin/bash
# scripts/validate-env.sh
set -e
REQUIRED=(
POSTGRES_PASSWORD
KRATOS_SECRETS_COOKIE
KRATOS_SECRETS_CIPHER
HYDRA_SECRETS_SYSTEM
HYDRA_SECRETS_COOKIE
OLYMPUS_ENCRYPTION_KEY_PRIMARY
)
source .env
for var in "${REQUIRED[@]}"; do
if [ -z "${!var}" ]; then
echo "ERROR: $var is empty"
exit 1
fi
done
# Check key length
if [ $(echo -n "$OLYMPUS_ENCRYPTION_KEY_PRIMARY" | base64 -d | wc -c) -ne 32 ]; then
echo "ERROR: OLYMPUS_ENCRYPTION_KEY_PRIMARY must decode to 32 bytes"
exit 1
fi
echo "✓ .env validated"Run before starting:
./scripts/validate-env.sh && podman-compose up -dTemplating
# .env (template)
SERVICE_X_URL=http://service-x:3000
# At compose time
envsubst < docker-compose.template.yml > docker-compose.ymlFor complex setups. Most Olympus deployments: direct .env is fine.
Per-environment
.env.dev
.env.staging
.env.prodActivate:
podman-compose --env-file .env.staging up -dOr symlink:
ln -sf .env.staging .env
podman-compose up -dSecret rotation
# Rotate POSTGRES_PASSWORD
NEW_PW=$(openssl rand -base64 32)
# 1. Update DB
podman exec ciam-postgres psql -c "ALTER USER olympus PASSWORD '$NEW_PW'"
# 2. Update .env
sed -i "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$NEW_PW/" .env
# 3. Restart services
podman-compose restartFor automated, see Cron secret rotator.
Sourcing from secret vault
For more secure setups, don't store secrets in .env at all. Pull from vault at startup:
# entrypoint.sh
export POSTGRES_PASSWORD=$(vault read secret/olympus/postgres password)
export OLYMPUS_ENCRYPTION_KEY=$(vault read secret/olympus/encryption_key value)
exec podman-compose upSecrets never on disk.
Don't commit secrets
.gitignore:
.env
.env.*
!.env.samplePre-commit hook to prevent accidents:
# .git/hooks/pre-commit
if git diff --cached --name-only | grep -E "^\\.env$|^\\.env\\.local$|^\\.env\\.prod$"; then
echo "ERROR: Don't commit .env files"
exit 1
fiDon't put secrets in source
// BAD
const DB_PASSWORD = "supersecret123";
// GOOD
const DB_PASSWORD = process.env.DB_PASSWORD;
if (!DB_PASSWORD) throw new Error("DB_PASSWORD not set");Audit at startup
Print non-secret config at startup:
[INFO] Starting Hera v1.4.0
[INFO] DOMAIN=your-domain.com
[INFO] HERA_PUBLIC_URL=https://ciam.your-domain.com
[INFO] Kratos URL: http://ciam-kratos:5000DON'T print:
[INFO] DB_PASSWORD=supersecret123 ← neverSane defaults
const PORT = parseInt(process.env.PORT ?? "3000");
const LOG_LEVEL = process.env.LOG_LEVEL ?? "info";Required values: throw, don't default.
const DB_URL = process.env.DB_URL;
if (!DB_URL) throw new Error("DB_URL is required");Type-safe env
Use zod / similar:
import { z } from "zod";
const envSchema = z.object({
POSTGRES_PASSWORD: z.string().min(16),
KRATOS_PUBLIC_URL: z.string().url(),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
});
export const env = envSchema.parse(process.env);Misconfigured env → fails at startup with clear error.
Reload without restart
For some config (template paths, feature flags), in-process reload via signals:
process.on("SIGHUP", () => {
reloadFromVault();
});kill -HUP <pid>Most config requires restart. Don't optimize prematurely.