Skip to content

Bootstrapping Production

Eric Fitzgerald edited this page May 22, 2026 · 1 revision

Bootstrapping Production

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 Production Config File

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", "oci", or "vault" for production secret management

Block 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.


Wiring Secrets

Secrets provider

Set secrets.provider to the backend that holds your credentials. Supported values:

Value Backend
env Environment variables (default; suitable for Heroku, Docker, simple deployments)
aws AWS Secrets Manager
azure Azure Key Vault
gcp Google Cloud Secret Manager
oci OCI Vault
vault HashiCorp Vault

Secret reference schemes

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, OCI, Vault, 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"

Three-phase resolution order

Secret resolution happens in three ordered phases at startup. Understanding this prevents a common bootstrap pitfall:

  1. Phase 1 — Resolve the secrets block itself. The secrets provider has not been built yet, so only env:// and file:// references work here. A vault:// reference inside the secrets block — for example, trying to supply a Vault token via vault:// — is rejected with a fatal error. Supply the provider's own credentials via environment variables or mounted files.

  2. Phase 2 — Build the secrets provider. The now-resolved secrets block is used to initialise the provider (AWS client, Vault client, etc.).

  3. Phase 3 — Resolve remaining bootstrap secrets. Fields such as database.url, database.redis.password, and auth.jwt.secret are resolved by dereferencing vault:// 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.


Required Settings and Startup Validation

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:pass@db.internal:5432/tmi"
export TMI_JWT_SECRET="$(cat /run/secrets/jwt-secret)"

First Boot and DB Seeding

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.


Choosing the Database Backend

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.


Platform Notes

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.

Oracle Autonomous Database (OCI)

When database.url uses the oracle:// scheme, the engine is Oracle Autonomous Database — the production target. 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 OCI-specific container configuration, see Database-Setup and OCI-Container-Deployment.

Heroku

On Heroku, configuration comes entirely from config vars (environment variables) — there is no persistent config file on the dyno. Set the bootstrap keys as TMI_* config vars (for example TMI_DATABASE_URL, TMI_JWT_SECRET), which is exactly the env-override path TMI already supports. Secrets resolve via secrets.provider: env (the default) reading those config vars, or via env://NAME references. Operational config still seeds into the database on first boot as usual, so verify it via /admin/settings afterward. Destructive database reset/drop tooling exists for Heroku operators — see the Heroku operator documentation for those procedures rather than running raw SQL by hand.

Kubernetes / Containers

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.


Migrating a Pre-1.4.0 Deployment

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.

What happens during the transition

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.

Importing legacy settings with tmi-dbtool

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.


Worker Components (Forward-Looking)

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.


Post-Bootstrap Checklist

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_name matches the public FQDN.
  • If the server runs behind a reverse proxy or load balancer, server.base_url (an operational setting, set via /admin/settings after 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 OCI/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 setting errors at startup; ValidateRequired passed cleanly.
  • Operational defaults were seeded on first boot; /admin/settings returns 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_dev and logging.is_test are both false; auth.build_mode is production.
  • For pre-1.4.0 migrations: tmi-dbtool --import-legacy has run successfully and the server no longer logs operational-key drift warnings.

See Also

Home

Releases


Getting Started

Deployment

Operation

Troubleshooting

Development

Integrations

Tools

API Reference

Reference

Clone this wiki locally