Self-Hosted Deployment
Deploy Aira on your own infrastructure with Docker.
Overview
Aira runs as three Docker containers plus PostgreSQL and Redis. You provide your own AI provider keys, and all data stays on your infrastructure.
| Service | Image | Port | Role |
|---|---|---|---|
| API | ghcr.io/aira-proof/backend | 8000 | Backend, consensus engine, receipts |
| Frontend | ghcr.io/aira-proof/frontend | 3000 | Dashboard |
| Docs | ghcr.io/aira-proof/docs | 3001 | Documentation |
| PostgreSQL | postgres:16-alpine | 5432 | Database |
| Redis | redis:7-alpine | 6379 | Cache + job queue |
A reverse proxy (Caddy, Nginx, or Traefik) sits in front for HTTPS.
Prerequisites
- Docker Engine 24+ and Docker Compose v2
- A GHCR access token (provided by Aira — contact customers@softure-ug.de)
- At least 2 AI provider API keys (OpenAI, Anthropic, or Google)
- 4 GB RAM minimum, 8 GB recommended
Step 1: Authenticate with the container registry
Aira images are private. Use the access token provided to you:
echo $AIRA_REGISTRY_TOKEN | docker login ghcr.io -u aira-proof --password-stdinStep 2: Create the project directory
mkdir -p /opt/aira
cd /opt/airaStep 3: Create docker-compose.yml
services:
api:
image: ghcr.io/aira-proof/backend:latest
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
restart: unless-stopped
env_file: .env
environment:
- DATABASE_URL=postgresql+asyncpg://aira:${DB_PASS}@db:5432/aira
- REDIS_URL=redis://redis:6379/0
- ENVIRONMENT=production
- DEPLOYMENT_MODE=selfhosted
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"]
interval: 15s
timeout: 10s
retries: 3
start_period: 10s
ports:
- "8000:8000"
migrate:
image: ghcr.io/aira-proof/backend:latest
command: alembic upgrade head
env_file: .env
environment:
- DATABASE_URL=postgresql+asyncpg://aira:${DB_PASS}@db:5432/aira
depends_on:
db:
condition: service_healthy
restart: "no"
frontend:
image: ghcr.io/aira-proof/frontend:latest
restart: unless-stopped
environment:
- NEXT_PUBLIC_API_URL=${API_URL:-http://localhost:8000}
- AUTH_SECRET=${AUTH_SECRET}
- AUTH_TRUST_HOST=true
ports:
- "3000:3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/"]
interval: 15s
timeout: 5s
retries: 3
docs:
image: ghcr.io/aira-proof/docs:latest
restart: unless-stopped
ports:
- "3001:3000"
db:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=${DB_PASS}
- POSTGRES_DB=aira
- POSTGRES_USER=aira
healthcheck:
test: ["CMD-SHELL", "pg_isready -U aira"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
pgdata:
redisdata:Step 4: Configure environment variables
Create a .env file:
# Database (choose a strong password)
DB_PASS=your-strong-database-password
# Security — generate with: openssl rand -hex 32
SECRET_KEY=your-64-char-random-secret
AUTH_SECRET=your-random-auth-secret
# Signing key — generate with:
# python3 -c "from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey; print(Ed25519PrivateKey.generate().private_bytes_raw().hex())"
SIGNING_PRIVATE_KEY_HEX=
# License key (required for self-hosted — contact customers@softure-ug.de)
AIRA_LICENSE_KEY=your-license-key
# AI Provider Keys — at least 2 required for multi-model consensus
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_API_KEY=AIza...
# Frontend URL (your domain — used for email verification links and CORS)
FRONTEND_URL=https://app.yourdomain.com
# API URL (your API domain)
API_URL=https://api.yourdomain.com
# CORS — add your frontend domain
CORS_ALLOWED_ORIGINS=https://app.yourdomain.com
# Optional: Email (for user verification, password reset)
RESEND_API_KEY=
EMAIL_FROM=Aira <noreply@yourdomain.com>
# Optional: OAuth (for social login — omit for email/password only)
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
AUTH_GITHUB_ID=
AUTH_GITHUB_SECRET=Required variables
| Variable | How to generate | Description |
|---|---|---|
DB_PASS | Choose a strong password | PostgreSQL password |
SECRET_KEY | openssl rand -hex 32 | JWT signing + encryption key |
AUTH_SECRET | openssl rand -hex 32 | NextAuth session encryption |
AIRA_LICENSE_KEY | Provided by Aira (contact customers@softure-ug.de) | Required for self-hosted deployments |
DEPLOYMENT_MODE | Set to selfhosted | Enables self-hosted mode (set automatically in docker-compose) |
FRONTEND_URL | Your frontend domain (e.g. https://app.yourdomain.com) | Used for email verification links and CORS |
API_URL | Your API domain (e.g. https://api.yourdomain.com) | Used by the frontend to reach the backend |
CORS_ALLOWED_ORIGINS | Comma-separated list of allowed origins | Must include your frontend domain |
2 of: OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY | From each provider's dashboard | Multi-model consensus requires at least 2 providers |
Optional variables
| Variable | Default | Description |
|---|---|---|
SIGNING_PRIVATE_KEY_HEX | Auto-generated | Ed25519 key for receipt signing. Set for persistence across container restarts |
RESEND_API_KEY | — | For sending verification and password reset emails (alternative to SMTP) |
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD | — | SMTP email configuration (alternative to Resend) |
EMAIL_FROM | Aira <customers@softure-ug.de> | Email sender address |
ADMIN_API_KEY | — | Secures the /metrics endpoint |
RATE_LIMIT_PER_MINUTE | 60 | Rate limit for authenticated requests |
PUBLIC_RATE_LIMIT_PER_MINUTE | 30 | Rate limit for public requests |
AUTH_GOOGLE_ID/SECRET | — | Google OAuth (leave empty to disable — credentials auth works without it) |
AUTH_GITHUB_ID/SECRET | — | GitHub OAuth (leave empty to disable) |
AUTH_GITLAB_ID/SECRET | — | GitLab OAuth (leave empty to disable) |
OAuth is optional. Email/password authentication works without any OAuth provider configured. OAuth providers are only needed if you want social login buttons on the login page.
Email is required for registration. User registration requires email verification. Configure either Resend (RESEND_API_KEY) or SMTP (SMTP_HOST, SMTP_USER, SMTP_PASSWORD) for email delivery to work.
Variables set automatically
These are configured in docker-compose.yml — do not override:
| Variable | Value | Why |
|---|---|---|
DEPLOYMENT_MODE | selfhosted | Removes cloud-only features, sets unlimited usage |
AUTH_TRUST_HOST | true | Required when running behind a proxy or non-standard hostname |
DATABASE_URL | Internal Docker URL | Connects API to the Postgres container |
REDIS_URL | Internal Docker URL | Connects API to the Redis container |
Step 5: Deploy
cd /opt/aira
# Start infrastructure
docker compose up -d db redis
# Wait for database to be ready
sleep 5
# Run database migrations
docker compose run --rm migrate
# Start all services
docker compose up -dStep 6: Verify
# API health
curl http://localhost:8000/health
# Expected: {"status":"healthy","checks":{"database":"ok"}}
# Frontend (should redirect to login)
curl -o /dev/null -w "%{http_code}" http://localhost:3000/dashboard/cases/new
# Expected: 307
# Docs
curl -o /dev/null -w "%{http_code}" http://localhost:3001/
# Expected: 200Reverse proxy
For HTTPS, place a reverse proxy in front. Aira ships a Caddyfile template for self-hosted deployments at deploy/Caddyfile.selfhosted. It uses environment variables for domain names (Caddy supports {$ENV_VAR} syntax):
{$API_DOMAIN:api.localhost} {
reverse_proxy aira-api:8000
encode gzip
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
{$FRONTEND_DOMAIN:app.localhost} {
reverse_proxy aira-frontend:3000
encode gzip
}
{$DOCS_DOMAIN:docs.localhost} {
reverse_proxy aira-docs:3000
encode gzip
}Set API_DOMAIN, FRONTEND_DOMAIN, and DOCS_DOMAIN environment variables for Caddy, or replace the placeholders with your actual domains.
Updating to a new version
cd /opt/aira
# Pull latest images
docker compose pull
# Run any new migrations
docker compose run --rm migrate
# Restart services
docker compose up -dSelf-hosted vs Cloud
| Feature | Cloud | Self-hosted |
|---|---|---|
| AI provider keys | Aira-managed or Provider Credentials | Provider Credentials only — you provide all keys |
| Case limits | 25/month (free tier) | Unlimited |
| Usage display | Shows plan limits | Shows "Unlimited" |
| Managed by Aira | Configure your own Resend or SMTP | |
| Data residency | EU (Frankfurt) | Your infrastructure |
| Updates | Automatic | Pull latest images manually |
| Support | Community (free) or dedicated (enterprise) | Included in license |
Troubleshooting
API won't start
docker logs aira-api-1 --tail 30Common causes:
- Missing or incorrect
DB_PASS— check the password matches between.envand what Postgres was initialized with - Database not ready — wait for the healthcheck or restart with
docker compose restart api - Missing AI provider keys — need at least 2 of OpenAI, Anthropic, Google
Frontend accessible without login
Verify AUTH_SECRET is set in .env and that AUTH_TRUST_HOST=true is in the frontend environment section of docker-compose.yml.
Migrations fail
# Check current migration version
docker compose exec db psql -U aira -c "SELECT version_num FROM alembic_version;"
# Re-run migrations
docker compose run --rm migrateDatabase password mismatch
If you changed DB_PASS after the database was first created, the old password is stored in the Postgres data volume. Either:
- Reset the volume:
docker compose down && docker volume rm aira_pgdata && docker compose up -d - Or change the password inside Postgres:
docker compose exec db psql -U aira -c "ALTER USER aira PASSWORD 'new-password';"
Container can't pull images
Verify GHCR authentication:
docker pull ghcr.io/aira-proof/backend:latestIf it fails, re-authenticate with your provided token.
Support
For self-hosted deployment support, contact customers@softure-ug.de.