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.
Production checklist
Section titled “Production checklist”Security
- Replace every
REPLACE_THIS_VALUE_IN_PRODUCTIONplaceholder inconfig.yml - Pre-encrypt secrets in
config.ymlwith thefernetCLI and provideCONFIG_ENCRYPTION_KEYvia the environment (it decrypts them on load) - Set a strong
JWT_KEY(HS256) orJWT_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-Originto 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_HOSTif 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
Secrets and keys
Section titled “Secrets and keys”Generate strong values and provide the encryption key via the environment:
openssl rand -hex 32 # JWT_KEY (HS256), PG_ENCRYPTION_KEY, LOCAL_STORAGE_SECRETopenssl rand -hex 16 # CONFIG_ENCRYPTION_KEY: exactly 32 printable charactersexport 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.
Example production config
Section titled “Example production config”LOG_LEVEL: 'INFO' # logs go to stderr — capture them via your process manager
HTTP_HOST: '0.0.0.0'HTTP_PORT: 8080HTTP_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: 5432PG_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: 6379Provide the secrets separately:
export PG_PASSWORD="..."export PG_ENCRYPTION_KEY="..."export JWT_KEY="..."export GOOGLE_API_KEY="..." # or OPENAI_API_KEYexport CONFIG_ENCRYPTION_KEY="..."TLS and the reverse proxy
Section titled “TLS and the reverse proxy”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.
Health checks
Section titled “Health checks”curl https://api.yourdomain.com/api/health # -> okGET /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).
Running under Docker / Kubernetes
Section titled “Running under Docker / Kubernetes”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(orTEST/GRAY) — the server then assumes the schema is already applied. - Sentry crash reporting writes to a database dir; the image points
SENTRY_DATABASE_PATHat/tmp/mirobody/sentryso it stays writable under a read-only root filesystem. - Tanka login pulls in
nodejs; disable it (TANKA_LOGIN_ENABLED=false) if unused.
Object storage
Section titled “Object storage”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.
Privacy and regulated workloads
Section titled “Privacy and regulated workloads”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 flow | When it is active | Control |
|---|---|---|
| LLM provider | A hosted model is configured | Use an on-device model or select the cloud project, region, account, and agreement appropriate to your workload. |
| Object storage | S3, OSS, or Azure Blob is configured | Choose the bucket/container region, access policy, encryption, and retention. |
| Health-data vendor or EHR | A vendor integration is enabled | Review the vendor’s authorization scope, destination, and contract. |
| Identity and messaging | Email, Firebase, WeChat, GitHub, or Tanka login is enabled | Limit the identity data sent and disable unused methods. |
| Remote config and crash reporting | Their endpoints or DSNs are configured | Keep 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.
Scaling
Section titled “Scaling”- Horizontal — run multiple instances behind a load balancer; set
REDIS_HOSTso 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.
Monitoring
Section titled “Monitoring”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.
Backups
Section titled “Backups”# Daily PostgreSQL dump (cron)0 2 * * * pg_dump -h <host> -U postgres mirobody | gzip > /backups/mirobody-$(date +\%Y\%m\%d).sql.gzAlso back up your config.yml (encrypted) and the object-storage bucket. Test restores regularly.
Next steps
Section titled “Next steps”Build and run the container image
Every production key in one place
Retention, deletion, and review boundaries
Endpoints your clients will call