Deploying with Docker
Run the official Mira Docker image on any host that runs Docker.
Docker is the simplest path: pull the image, set your env vars, mount a volume for SQLite (or point at Postgres), and you're running.
Image
ghcr.io/miracodeai/mira:latestThe image is multi-stage: it builds the React dashboard, then bundles it into
a Python 3.12 runtime that exposes port 8000 and runs mira serve as
entrypoint. There's no separate UI container.
Recommended run
Put your config in an env file, run detached with auto-restart, and let Docker pull a fresh image every start:
# mira.env (chmod 600, do not commit)
MIRA_GITHUB_APP_ID=123456
MIRA_WEBHOOK_SECRET=replace-with-32-bytes-of-random
OPENROUTER_API_KEY=sk-or-v1-your-key-here
MIRA_MODEL=anthropic/claude-sonnet-4-6
ADMIN_PASSWORD=replace-me
MIRA_DASHBOARD_URL=https://mira.your-org.com
DATABASE_URL=postgresql://mira:mira@db.internal:5432/mira?sslmode=require
MIRA_INDEX_DIR=/data/indexesThe GitHub App PEM is multi-line, which Docker's --env-file can't hold.
Pick one of:
Reference the PEM by path in mira.env and bind-mount the file into the
container. Mira's auth module resolves @/abs/path.pem at JWT-sign time.
# add to mira.env
MIRA_GITHUB_PRIVATE_KEY=@/etc/mira/private-key.pemdocker run -d --name mira --restart unless-stopped --pull=always \
-p 8000:8000 \
--env-file ./mira.env \
-v /etc/mira/private-key.pem:/etc/mira/private-key.pem:ro \
-v mira-indexes:/data/indexes \
ghcr.io/miracodeai/mira:latestRead the PEM from the host filesystem into an environment variable at
docker run time. Keeps the PEM off the container filesystem.
docker run -d --name mira --restart unless-stopped --pull=always \
-p 8000:8000 \
--env-file ./mira.env \
-e MIRA_GITHUB_PRIVATE_KEY="$(cat ./private-key.pem)" \
-v mira-indexes:/data/indexes \
ghcr.io/miracodeai/mira:latestA few notes on the flags:
--pull=alwaysforces Docker to fetch the latest:latestdigest instead of reusing the cached image.DATABASE_URLis optional. Omit it for SQLite atMIRA_INDEX_DIR. This is fine for a single-host smoke test, but use Postgres for anything you care about.MIRA_INDEX_DIRvolume: even with Postgres, Mira keeps per-repo index artifacts on disk. The named volume persists them across image updates.
docker compose
# docker-compose.yml
services:
mira:
image: ghcr.io/miracodeai/mira:latest
restart: unless-stopped
ports:
- "8000:8000"
env_file: .env
environment:
DATABASE_URL: postgresql://mira:mira@db:5432/mira
MIRA_INDEX_DIR: /data/indexes
volumes:
- mira-indexes:/data/indexes
depends_on:
- db
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8000/health"]
interval: 10s
timeout: 2s
retries: 5
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: mira
POSTGRES_PASSWORD: mira
POSTGRES_DB: mira
volumes:
- mira-pgdata:/var/lib/postgresql/data
volumes:
mira-indexes:
mira-pgdata:TLS
The Mira image speaks plain HTTP on port 8000. It does not terminate TLS
(no certificate, no HTTPS listener). Something else has to accept the HTTPS
connection from the internet, decrypt it, and forward HTTP to Mira. GitHub
refuses to deliver webhooks to plain http:// URLs in most configurations,
so this step is effectively required.
Pick one of:
- A reverse proxy on the same host: Caddy, nginx, or Traefik.
- A platform load balancer: AWS ALB, GCP Load Balancer, Cloud Run.
- A CDN or tunnel: Cloudflare proxy, Cloudflare Tunnel.
- A PaaS that handles TLS for you: Railway, Fly.io, or Render. Pick one of those and skip this whole section.
Example: Caddy on the same host
Caddy is the simplest path. It auto-issues and renews Let's Encrypt certs
with no separate cron job and no certbot ceremony. One config file:
# /etc/caddy/Caddyfile
mira.your-org.com {
reverse_proxy localhost:8000
}Caddy fetches and renews the cert on its own. Reload with
sudo systemctl reload caddy.
services:
mira:
image: ghcr.io/miracodeai/mira:latest
restart: unless-stopped
env_file: .env
volumes:
- mira-data:/data
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
volumes:
mira-data:
caddy-data:
caddy-config:With this Caddyfile:
mira.your-org.com {
reverse_proxy mira:8000
}Example: nginx with Let's Encrypt (certbot)
If you're already running nginx, certbot wires up a cert in a couple of commands:
# /etc/nginx/sites-available/mira
server {
listen 80;
server_name mira.your-org.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}sudo ln -s /etc/nginx/sites-available/mira /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# Issue and auto-renew the cert
sudo certbot --nginx -d mira.your-org.comCertbot rewrites the nginx config to listen on 443 with the new
certificate and adds a renewal cron entry. Set it and forget it.
Example: Cloudflare Tunnel (no inbound port required)
Useful when the host doesn't have a public IP, or you don't want to open
ports at all. The tunnel runs cloudflared next to Mira and Cloudflare
forwards encrypted traffic to it:
services:
mira:
image: ghcr.io/miracodeai/mira:latest
restart: unless-stopped
env_file: .env
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel --no-autoupdate run --token ${CF_TUNNEL_TOKEN}Create the tunnel in the Cloudflare Zero Trust dashboard, point it at
http://mira:8000, and Cloudflare handles HTTPS at the edge, with no Let's
Encrypt or nginx config on your end.
Proxy headers and the session cookie
Mira marks the dashboard session cookie Secure when it sees the request
arrive over HTTPS. Behind a reverse proxy that means the proxy must send
X-Forwarded-Proto (Caddy, nginx with the config above, and Cloudflare all
do), and Mira must trust the proxy's address. Proxies on the same host
(127.0.0.1) are trusted out of the box; if the proxy runs elsewhere — e.g.
a Caddy or cloudflared container — tell Mira to trust it:
# in Mira's environment (.env)
FORWARDED_ALLOW_IPS=*Use the proxy's actual IP instead of * if Mira's port is reachable by
anything other than the proxy.
Image versions
| Tag | What you get |
|---|---|
:latest | Latest released version. Fine for production if you watch releases. |
:v0.1.0, :v0.2.0, … | Pinned version. Pair with cosign verification for tamper-resistant deploys. |
Verifying the image (cosign)
Container images are signed with sigstore keyless. Verify before deploying:
cosign verify ghcr.io/miracodeai/mira:v0.1.0 \
--certificate-identity-regexp 'https://github.com/miracodeai/mira/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comLogs
Mira writes structured JSON logs to stdout. Pipe them through docker logs,
your platform's log aggregator, or jq:
docker logs -f mira | jq -R 'fromjson? // .'
Mira