Deployment

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:latest

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

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/indexes

The 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.pem
docker 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:latest

Read 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:latest

A few notes on the flags:

  • --pull=always forces Docker to fetch the latest :latest digest instead of reusing the cached image.
  • DATABASE_URL is optional. Omit it for SQLite at MIRA_INDEX_DIR. This is fine for a single-host smoke test, but use Postgres for anything you care about.
  • MIRA_INDEX_DIR volume: 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.com

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

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

TagWhat you get
:latestLatest 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.com

Logs

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? // .'

On this page