Host requirements
A Linux host with Docker Engine and the Compose plugin 2.24.4 or newer, Git, and outbound access to the model provider your key belongs to. No GPU is needed. The stack runs three containers: Postgres with pgvector, the server and the worker, plus a one-shot container that fixes upload-volume ownership on each start. Capacity depends on concurrent users and file processing; this release does not publish a measured minimum host size.First start
Decide the posture before the first start: demo data is seeded on first boot, and the database password is set when its volume is created.1
Clone the repository
2
Write .env
.env
ENV names the overlay, config.prod.yaml. Any one of the supported model keys works; see Configuration.3
Write the overlay
Create
config.prod.yaml with the production posture below. deploy.sh wires in an overlay file already present under that name; it does not create one for you. Bind the app port to loopback in a compose.override.yaml before starting if the reverse proxy runs on this host.4
Start the stack
deploy.sh generates PG_PASSWORD, PG_ENCRYPTION_KEY, CONFIG_ENCRYPTION_KEY, LOG_ENCRYPTION_KEY and JWT_KEY into .env when they are missing, pulls the application image (or builds it from this checkout when it cannot), and starts Postgres, the server and the worker. Continue with the checks below before directing users to this host.After a local demo
Use a fresh deployment for real records. Turning onPRODUCTION in the demo checkout stops future demo seeding, but it does not erase existing demo accounts or readings. Its Postgres password was fixed when the original volume was created, and changing encryption keys would make stored content unreadable. Reusing that volume is not a clean production start.
- If you entered anything you need to keep in the demo, run
shell/backup.shthere and store the backup and its matching.envand config files somewhere protected. This is a safety copy, not the starting database for production. - Prefer a new Linux host or VM. Clone the engine there and follow First start from the beginning, including
SEED_DEMO_DATA=false, the production overlay,DATABASE_DECRYPTION_KEYand HTTPS before running./deploy.sh. - If you must reuse the same host, run
docker compose downin the demo checkout first. It stops the demo without deleting its volumes. Clone into a different directory name for production, keepCOMPOSE_PROJECT_NAMEunset or distinct, and follow First start there. Compose prefixes named volumes with the project name; a different project gives the new stack empty Postgres and upload volumes. The old demo remains stopped and separate. - After the deployment checks, register a new account and enter only the real records you intend to keep. Check that no demo accounts or demo readings appear. Keep the demo backup separate; restoring it into the new stack would bring the demo data back.
PRODUCTION alone is not that transfer.
Production posture
config.prod.yaml
PRODUCTION: true the server refuses to start while any predefined sign-in code or any REPLACE_THIS_VALUE_IN_PRODUCTION placeholder remains, and it never seeds demo data; the error names what is left. EMAIL_PREDEFINE_CODES: {} removes the two demo accounts, because an overlay replaces the whole value.
People then sign in in one of three ways: by registering with a password in the web client (POST /password/register), with an email code once a mail server is configured (EMAIL_FROM and EMAIL_SMTP_HOST, EMAIL_SMTP_PORT, EMAIL_SMTP_USER, EMAIL_SMTP_PASS), or with Google or Apple sign-in, configured in config.devices.yaml.
Set BOOTSTRAP_SCHEMA: false as well if you provision the database schema yourself.
Secrets
./deploy.sh writes PG_PASSWORD, PG_ENCRYPTION_KEY, CONFIG_ENCRYPTION_KEY, LOG_ENCRYPTION_KEY and JWT_KEY into .env on first run, each openssl rand -hex 32, and never overwrites a value already there (.env itself is written chmod 600). Compose passes .env to every container through env_file, so PG_PASSWORD reaches both the pg service (POSTGRES_PASSWORD) and the app; nothing else needs to carry it.
DATABASE_DECRYPTION_KEY is the one secret deploy.sh does not generate: it ships in config.devices.yaml as a REPLACE_THIS_VALUE_IN_PRODUCTION placeholder, and PRODUCTION: true refuses to start while that placeholder remains. Set it yourself, in .env, where it overrides the config file (environment variables take precedence over every config source):
.env
openssl rand -hex 16: the resulting 32 ASCII characters are used directly as a 32-byte AES key, not decoded from hex.
Postgres applies POSTGRES_PASSWORD only when it creates an empty data volume. To move PG_PASSWORD onto an existing volume, change it with ALTER USER first, then update .env to match.
Keep .env out of version control, and do not reuse any of these keys between environments: data encrypted under one key cannot be read with another. Preserve .env alongside database and upload backups so a restore can read encrypted records. CONFIG_ENCRYPTION_KEY encrypts secret-shaped values (_KEY, _PASSWORD, and similar) that you add to the overlay itself, such as a device provider credential in config.prod.yaml; it has no effect on .env.
Public URL and HTTPS
Terminate TLS in a reverse proxy in front of the server, which listens on port . This example runs Caddy on the same host. Point the DNS name at that host and allow inbound ports 80 and 443 for certificate issuance and HTTPS. Incompose.override.yaml, merge this service into the override from the Secrets section so the app is reachable only from the host and trusts the host-side proxy at the Compose network’s configured gateway. The !override tag is essential: without it, Compose retains the base file’s public port alongside the loopback port (merge rules).
compose.override.yaml
Caddyfile
health.example.com with your domain. Caddy obtains HTTPS certificates when DNS and ports are ready; it forwards the original Host and proxy scheme. See Caddy’s reverse proxy guide. With another proxy or network layout, set FORWARDED_ALLOW_IPS to the address the app container actually sees for that proxy.
config.prod.yaml
MCP_PUBLIC_URL is the deployment’s public origin; the server uses it for the links it prints and for file URLs. Restrict CORS with HTTP_HEADERS to the origins you actually serve.
Do not expose Postgres. compose.yaml already publishes it on the loopback interface only (), for local inspection, so nothing to change there. The application port is public in the base Compose file, so retain the loopback override above. Run docker compose config and confirm the effective mirobody.ports contains only the loopback binding.
Verify the deployment
From the repository directory, check the containers and the local health response:mirobody service should report healthy. The response contains version and agent fields. Then open the public HTTPS URL, register or sign in with a real account, and confirm that the personal MCP URL also begins with https://. Production mode rejects the shipped demo codes; if startup fails, read docker compose logs --tail 80 mirobody and Troubleshooting.
Backups and upgrades
The data lives in the Postgres volume and, when uploads are stored on local disk rather than in a bucket, in the upload volume. Use Verify and Restore a Backup for a restore rehearsal and recovery procedure; the engine’s Backup & Restore reference explains the volumes in detail. Use Upgrade a Deployment for the sequence: back up data and keys, review the target release, change the checkout, restart and verify.deploy.sh runs docker compose up -d --remove-orphans, which recreates a changed service in place; plan a maintenance window for the brief restart.