Bootstrapping Production - ericfitz/tmi GitHub Wiki
This guide walks you through the steps required to bring a new TMI deployment to production for the first time. It focuses on the bootstrap configuration layer — the minimal set of settings the server reads from a YAML file and environment variables at startup — and on how the server transitions from that skeleton into a fully operational state whose settings live in the database.
The TMI configuration model has two layers: bootstrap settings (consumed at startup from config-production.yml and environment variables) and operational settings (everything else — OAuth providers, timeouts, operator info, etc. — that lives in the database and is seeded on first run). The production YAML file covers bootstrap settings only. For a full description of both layers, see Configuration-Model.
The config-production.yml shipped in the repository is a bootstrap-only skeleton. It does not hold OAuth provider definitions, email configuration, operator identity, or any other operational setting. Those values are seeded into the database on first boot and are managed thereafter via the /admin/settings API.
Use the skeleton as your starting point and fill in the values for your environment. Secret values should be supplied via a secrets provider (see Wiring Secrets) — never as inline plaintext in a file committed to version control.
server:
port: "8080"
interface: 0.0.0.0
read_timeout: 10s
write_timeout: 30s
idle_timeout: 2m0s
tls_enabled: true
tls_cert_file: "/etc/tmi/certs/server.crt"
tls_key_file: "/etc/tmi/certs/server.key"
tls_subject_name: "tmi.yourdomain.com"
http_to_https_redirect: true
database:
url: "vault://replace-me/database/url"
redis:
host: "redis.internal"
port: "6379"
password: "vault://replace-me/database/redis/password"
db: 0
auth:
build_mode: production
jwt:
secret: "vault://replace-me/auth/jwt/secret"
signing_method: "HS256"
logging:
level: warn
is_dev: false
is_test: false
log_dir: "/var/log/tmi"
max_age_days: 30
max_size_mb: 500
max_backups: 20
also_log_to_console: false
secrets:
provider: env # Set to "aws" for production secret managementBlock summary:
| Block | Purpose |
|---|---|
server |
Network, TLS, and timeout settings. Set tls_subject_name to your actual FQDN. |
database.url |
Connection URL for the primary database. The URL scheme selects the DB engine (see Choosing the Database Backend). |
database.redis |
Redis host, port, and auth. Used for caching and session management. |
auth |
build_mode: production enables all production security checks. jwt.secret signs and verifies all tokens — rotate it carefully. |
logging |
Structured log output. In production, is_dev and is_test must both be false. |
secrets |
Selects how secret references in this file are resolved (see below). |
Operational settings are not here. If you are looking for where to configure OAuth providers, rate limits, operator name, or similar settings — those live in the database after first boot.
Set secrets.provider to the backend that holds your credentials. Supported values:
| Value | Backend |
|---|---|
env |
Environment variables (default; suitable for Docker and simple deployments) |
aws |
AWS Secrets Manager |
vault |
HashiCorp Vault (not yet implemented; selecting it fails at startup) |
Anywhere a bootstrap setting accepts a secret value, you can use one of three reference schemes instead of an inline literal:
| Scheme | Resolved from | Example |
|---|---|---|
vault://PATH |
Secrets provider (AWS Secrets Manager, etc.) | vault://prod/tmi/jwt-secret |
env://NAME |
Environment variable | env://TMI_JWT_SECRET |
file://PATH |
File contents (whitespace-trimmed) | file:///run/secrets/jwt-secret |
Values with no scheme prefix are treated as inline literals. Inline secrets in production config files are strongly discouraged.
Examples:
# Pull the DB URL from an environment variable
database:
url: "env://DATABASE_URL"
# Pull the JWT secret from AWS Secrets Manager (when secrets.provider = "aws")
auth:
jwt:
secret: "vault://prod/tmi/auth/jwt-secret"
# Pull the Redis password from a mounted Kubernetes secret file
database:
redis:
password: "file:///run/secrets/redis-password"Secret resolution happens in three ordered phases at startup. Understanding this prevents a common bootstrap pitfall:
-
Phase 1 — Resolve the
secretsblock itself. The secrets provider has not been built yet, so onlyenv://andfile://references work here. Avault://reference inside thesecretsblock — for example, trying to supply a Vault token viavault://— is rejected with a fatal error. Supply the provider's own credentials via environment variables or mounted files. -
Phase 2 — Build the secrets provider. The now-resolved
secretsblock is used to initialise the provider (AWS client, Vault client, etc.). -
Phase 3 — Resolve remaining bootstrap secrets. Fields such as
database.url,database.redis.password, andauth.jwt.secretare resolved by dereferencingvault://references through the provider built in phase 2.
An unresolvable secret reference is fatal — the server exits rather than starting with a missing or empty secret.
At startup, TMI validates that every required bootstrap setting has a non-empty effective value (after secret resolution). If any required setting is missing, the server exits with an error that names the offending key:
FATAL required setting "database.url" is empty — set it in config-production.yml or via the TMI_DATABASE_URL environment variable
Required bootstrap settings include at minimum: database.url, auth.jwt.secret, and server.port. Correct these before the server will start.
Environment variables with the TMI_ prefix override any key in the config file. For example:
export TMI_DATABASE_URL="postgres://user:[email protected]:5432/tmi"
export TMI_JWT_SECRET="$(cat /run/secrets/jwt-secret)"On the first boot against an empty system_settings table, TMI seeds operational defaults from the internal classification registry. This includes defaults for OAuth provider placeholders, session timeouts, rate limits, operator metadata, and similar settings. After seeding, the database is the authoritative source for operational configuration — subsequent restarts do not re-seed.
After first boot, verify that the seeded defaults look correct:
# Retrieve a JWT (replace with your actual admin credentials or OAuth flow)
TOKEN=$(curl -s -X POST http://localhost:8080/oauth2/token \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" | jq -r '.access_token')
# List operational settings
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8080/admin/settings | jq .Review the output and update any settings that do not match your environment — particularly OAuth provider configuration, operator display name, and any site-specific limits. Use the /admin/settings API to update individual settings; do not edit the YAML file for operational values.
The database engine is selected by the URL scheme in database.url:
| Scheme | Engine |
|---|---|
postgres:// |
PostgreSQL |
oracle:// |
Oracle Autonomous Database (production target) |
mysql:// |
MySQL / MariaDB |
sqlserver:// |
Microsoft SQL Server |
sqlite:// |
SQLite (development only — not supported in production) |
For Oracle ADB specifics (wallet configuration), see Platform Notes below.
The bootstrap mechanics described above are the same on every platform — what differs is how you supply the config file and where secrets come from. The subsections below give brief, platform-specific guidance and point to the dedicated deployment pages for full detail. See Deploying-TMI-Server for platform-by-platform deployment instructions.
When database.url uses the oracle:// scheme, the engine is Oracle Autonomous Database, a supported alternative to the PostgreSQL default (AWS deployments use RDS PostgreSQL). ADB requires an Oracle wallet in addition to the connection URL; point the server at the wallet directory via the TMI_ORACLE_WALLET_LOCATION environment variable. For the complete wallet provisioning and connection setup, and for the Oracle server image variant, see Database-Setup and Oracle-Container-Images.
In a Kubernetes deployment, mount secrets as files and reference them with file:// (for example auth.jwt.secret: file:///etc/tmi/secrets/jwt-secret), or inject them as environment variables and use env://NAME or the TMI_* overrides. The bootstrap config itself can be a ConfigMap-mounted config-production.yml containing only vault:///env:///file:// placeholders for secret values — the actual secret material comes from Kubernetes Secrets, never from the ConfigMap. For worker components (the forward-looking component platform), workers bootstrap from environment variables only — NATS URL and secret mounts — see Worker Components below.
Before TMI 1.4.0, operational settings (OAuth providers, timeouts, etc.) lived alongside bootstrap settings in the YAML config file. Starting with 1.4.0, those settings move to the database.
When the 1.4.0+ server starts and finds operational keys still present in a YAML file, it warns (but does not fail) and surfaces the drift so you know migration is incomplete. This grace period lets you migrate without an immediate outage.
Use the tmi-dbtool --import-legacy command to read operational settings from your old config file and write them into the database:
# Build the tool
make build-dbtool # PostgreSQL
make build-dbtool-oci # Oracle ADB
# Preview what will be imported (dry run — no writes)
./bin/tmi-dbtool --import-legacy \
--config config-production.yml \
-f config-legacy.yml \
--dry-run
# Import for real
./bin/tmi-dbtool --import-legacy \
--config config-production.yml \
-f config-legacy.yml| Flag | Meaning |
|---|---|
--config <file> |
Config file that provides the DB connection (database.url) |
-f / --input-file <file>
|
The legacy YAML file to import from |
--dry-run |
Preview changes without writing to the database |
--no-backup |
Skip the automatic backup step |
--no-rewrite |
Do not rewrite the input file to remove migrated keys |
After a successful import, remove the operational keys from your YAML file so the server no longer warns about them. For the complete flag reference and advanced usage, see Managing-Operational-Settings.
TMI's component platform (for K8s-style worker deployments) uses a separate, environment-variable-only bootstrap contract. A worker component reads its configuration entirely from environment variables via LoadWorker() — no YAML file, no database connection at startup. The key variables are:
| Variable | Required | Purpose |
|---|---|---|
NATS_URL |
Yes | JetStream connection URL; the worker cannot receive jobs without it |
HEARTBEAT_SUBJECT |
No | NATS subject for liveness heartbeats |
SECRET_MOUNTS |
No | Logical secret name → filesystem path of a mounted Kubernetes Secret |
LOG_LEVEL |
No | Log verbosity (default: info) |
Everything else a worker needs arrives in the job envelope or is read from a mounted secret. This section is forward-looking; consult the component and platform documentation for deployment details as that capability matures.
Before declaring a new deployment production-ready, verify each of the following:
- An admin user exists and can authenticate successfully via OAuth.
- TLS is configured:
tls_enabled: true, valid certificate and key paths set,tls_subject_namematches the public FQDN. - If the server runs behind a reverse proxy or load balancer,
server.base_url(an operational setting, set via/admin/settingsafter first boot) is set to the public-facing URL so that OAuth redirect URIs and self-referential links are correct. -
make build-server(or the equivalent container build) completed without errors and the binary reports the expected version at the root endpoint (curl https://tmi.yourdomain.com/). - The server logged no
FATAL required settingerrors at startup;ValidateRequiredpassed cleanly. - Operational defaults were seeded on first boot;
/admin/settingsreturns the expected keys. - OAuth provider configuration has been reviewed and updated in
/admin/settings(the seeded defaults are placeholders). - All secret references resolve from the secrets provider — no inline secrets appear in the config file or in environment variables that are visible in process listings.
-
logging.is_devandlogging.is_testare bothfalse;auth.build_modeisproduction. - For pre-1.4.0 migrations:
tmi-dbtool --import-legacyhas run successfully and the server no longer logs operational-key drift warnings.
- Configuration-Model — Full description of the bootstrap vs. operational setting layers
-
Managing-Operational-Settings — Runtime settings API,
tmi-dbtoolreference, and migration guide - Deploying-TMI-Server — Platform-specific deployment instructions (Docker Compose, Kubernetes, traditional server)
- Database-Setup — Database provisioning, migrations, and Oracle wallet configuration
- Oracle-Container-Images — Building and running the Oracle ADB server image variant