Skip to content
Get Started

Deployment

Production Deployment

Run the Mirobody C++ binary in production: secrets, backends, health checks, and data-flow controls.

In production, Mirobody is the standalone mirobody binary (or the Docker image) driven by config.yml and environment variables, backed by your own database, cache, and object storage. Below are the settings that matter beyond a local run.

Security
  • Replace every REPLACE_THIS_VALUE_IN_PRODUCTION placeholder in config.yml
  • Pre-encrypt secrets in config.yml with the fernet CLI and provide CONFIG_ENCRYPTION_KEY via the environment (it decrypts them on load)
  • Set a strong JWT_KEY (HS256) or JWT_PRIVATE_KEY (RS256)
  • Set PG_ENCRYPTION_KEY (encrypts sensitive data columns; keep it out of the file)
  • Remove EMAIL_PREDEFINE_CODES (demo codes are test-only)
  • Restrict CORS Access-Control-Allow-Origin to your domain
  • Terminate TLS at a reverse proxy / load balancer in front of the binary
Backends
  • Point PG_* at a managed PostgreSQL (desktop/server default backend)
  • Set REDIS_HOST if you run more than one instance (shared cache)
  • Configure an object-storage backend (S3 / OSS / Azure Blob) instead of local disk
Privacy review
  • Map every outbound data flow: LLM, storage, health-data vendors, identity/email, remote config, and crash reporting
  • Select regions and providers that match your requirements, then obtain any required contracts or agreements
  • Enable optional sharing and external integrations only after reviewing their data scope
Operations
  • Wire liveness/readiness probes to /api/health
  • Capture stderr with your process manager (docker logs, journald, …) and ship it to your aggregator
  • Automate PostgreSQL backups and test restores

Generate strong values and provide the encryption key via the environment:

Terminal window
openssl rand -hex 32 # JWT_KEY (HS256), PG_ENCRYPTION_KEY, LOCAL_STORAGE_SECRET
openssl rand -hex 16 # CONFIG_ENCRYPTION_KEY: exactly 32 printable characters
Terminal window
export CONFIG_ENCRYPTION_KEY="<generated>"

CONFIG_ENCRYPTION_KEY is a raw secret: the loader trims it, takes at most 32 bytes, pads shorter values, and derives the Fernet key used to decrypt gAAAA… config values. Secret-looking values are also masked in the startup summary. The loader only decrypts — it never encrypts plaintext for you. Keep bootstrap secrets such as PG_ENCRYPTION_KEY in environment variables or a secrets manager; if you store Fernet ciphertext in YAML, make sure the encryption tool derives its Fernet key exactly as ConfigStore::derive_fernet_key does.

For RS256 (so other services can verify your tokens against a published JWKS), set JWT_PRIVATE_KEY instead of JWT_KEY; cli/jwt_keygen mints a keypair.

config.yml
LOG_LEVEL: 'INFO' # logs go to stderr — capture them via your process manager
HTTP_HOST: '0.0.0.0'
HTTP_PORT: 8080
HTTP_HEADERS:
Access-Control-Allow-Origin: 'https://yourdomain.com'
Access-Control-Allow-Methods: 'GET, POST, PUT, DELETE, OPTIONS'
Access-Control-Allow-Headers: 'Authorization, Content-Type, X-Timezone'
# Database (managed PostgreSQL)
PG_HOST: 'your-db-host.rds.amazonaws.com'
PG_PORT: 5432
PG_USER: 'postgres'
PG_DBNAME: 'mirobody'
# PG_PASSWORD / PG_ENCRYPTION_KEY: provide via environment
# Cache (only needed for multi-instance)
REDIS_HOST: 'your-redis.cache.amazonaws.com'
REDIS_PORT: 6379

Provide the secrets separately:

Terminal window
export PG_PASSWORD="..."
export PG_ENCRYPTION_KEY="..."
export JWT_KEY="..."
export GOOGLE_API_KEY="..." # or OPENAI_API_KEY
export CONFIG_ENCRYPTION_KEY="..."

The binary serves plain HTTP + WebSocket on HTTP_HOST:HTTP_PORT. Terminate TLS in front of it (nginx, Caddy, a cloud load balancer). If you mount the app under a sub-path at the proxy, set HTTP_URI_PREFIX to match so generated URLs and routes line up. Make sure the proxy forwards WebSocket upgrade headers for the realtime /api/chat route.

Terminal window
curl https://api.yourdomain.com/api/health # -> ok

GET /api/health is an unauthenticated liveness probe. Under Kubernetes, use it for both httpGet liveness and readiness probes (the runtime image has no curl, so a Docker HEALTHCHECK isn’t an option).

The shipped Dockerfile is written for this: it runs as an unprivileged user (uid 10001) and expects secrets/settings from env vars or a mounted config.yml. Notes baked into the image:

  • Skip the in-process schema migration in managed environments by setting ENV=PROD (or TEST / GRAY) — the server then assumes the schema is already applied.
  • Sentry crash reporting writes to a database dir; the image points SENTRY_DATABASE_PATH at /tmp/mirobody/sentry so it stays writable under a read-only root filesystem.
  • Tanka login pulls in nodejs; disable it (TANKA_LOGIN_ENABLED=false) if unused.

Use a durable backend rather than local disk. Fill the S3, Alibaba OSS, or Azure Blob block in config.yml — see Configuration → File storage. Pin the storage region to your compliance region.

Self-hosting gives you control over placement, but it does not make a deployment compliant by itself. Review the complete data path before sending regulated data:

Data flowWhen it is activeControl
LLM providerA hosted model is configuredUse an on-device model or select the cloud project, region, account, and agreement appropriate to your workload.
Object storageS3, OSS, or Azure Blob is configuredChoose the bucket/container region, access policy, encryption, and retention.
Health-data vendor or EHRA vendor integration is enabledReview the vendor’s authorization scope, destination, and contract.
Identity and messagingEmail, Firebase, WeChat, GitHub, or Tanka login is enabledLimit the identity data sent and disable unused methods.
Remote config and crash reportingTheir endpoints or DSNs are configuredKeep health content out of configuration and review telemetry before enabling it.

Vertex AI and Azure OpenAI are deployment choices, not compliance guarantees. Confirm service eligibility and required agreements with the provider and your legal/security team. See Privacy & Compliance for the hosted API’s product controls.

  • Horizontal — run multiple instances behind a load balancer; set REDIS_HOST so they share cache state. All instances talk to the one configured PostgreSQL (PG_HOST + a connection pool) — the engine has no read-replica routing, so scale the database itself if it becomes the bottleneck. Sticky sessions are not required for the SSE/WebSocket chat routes as long as your load balancer keeps each connection on one instance for its lifetime.
  • Vertical — size CPU/memory to your LLM concurrency and document-processing load. PDF work serializes behind one internal mutex, so extra cores won’t speed up a single PDF.

The binary logs to stderr (src/platform/log.cpp); there is no log-file option — capture it with your process manager or docker logs. Metrics worth watching:

  • Chat turn latency and error rate
  • Provider sync success rate
  • Database connection health
  • Cache hit rate (Redis, if enabled)
  • CPU / memory per instance

Mirobody does not ship a built-in Prometheus/metrics endpoint — instrument at the proxy or platform layer.

Terminal window
# Daily PostgreSQL dump (cron)
0 2 * * * pg_dump -h <host> -U postgres mirobody | gzip > /backups/mirobody-$(date +\%Y\%m\%d).sql.gz

Also back up your config.yml (encrypted) and the object-storage bucket. Test restores regularly.