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.
┌─────────────────────────────────────────────────┐
│ Your Infrastructure │
│ │
│ ┌─────────┐ ┌──────────┐ ┌────────┐ │
│ │ API │ │ Frontend │ │ Docs │ │
│ │ :8000 │ │ :3000 │ │ :3001 │ │
│ └────┬────┘ └────┬─────┘ └───┬────┘ │
│ │ │ │ │
│ ┌────┴─────────────┴────────────┴────┐ │
│ │ Reverse Proxy │ │
│ │ (Caddy / Nginx / Traefik) │ │
│ └────────────────────────────────────┘ │
│ │ │
│ ┌────┴────┐ ┌───────┐ │
│ │Postgres │ │ Redis │ │
│ └─────────┘ └───────┘ │
└─────────────────────────────────────────────────┘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-verify --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-verify/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-verify/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-verify/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-verify/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=
# 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 actual domain or IP)
API_URL=https://api.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 |
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 |
API_URL | http://localhost:8000 | URL the frontend uses to reach the API |
RESEND_API_KEY | — | For sending verification and password reset emails |
EMAIL_FROM | Aira <customers@softure-ug.de> | Email sender address |
AUTH_GOOGLE_ID/SECRET | — | Google OAuth (optional) |
AUTH_GITHUB_ID/SECRET | — | GitHub OAuth (optional) |
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. Example Caddyfile:
api.yourdomain.com {
reverse_proxy localhost:8000
}
app.yourdomain.com {
reverse_proxy localhost:3000
}
docs.yourdomain.com {
reverse_proxy localhost:3001
}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 BYOK | BYOK 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-verify/backend:latestIf it fails, re-authenticate with your provided token.
Support
For self-hosted deployment support, contact customers@softure-ug.de.