Aira

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-stdin

Step 2: Create the project directory

mkdir -p /opt/aira
cd /opt/aira

Step 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

VariableHow to generateDescription
DB_PASSChoose a strong passwordPostgreSQL password
SECRET_KEYopenssl rand -hex 32JWT signing + encryption key
AUTH_SECRETopenssl rand -hex 32NextAuth session encryption
2 of: OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEYFrom each provider's dashboardMulti-model consensus requires at least 2 providers

Optional variables

VariableDefaultDescription
SIGNING_PRIVATE_KEY_HEXAuto-generatedEd25519 key for receipt signing. Set for persistence across container restarts
API_URLhttp://localhost:8000URL the frontend uses to reach the API
RESEND_API_KEYFor sending verification and password reset emails
EMAIL_FROMAira <customers@softure-ug.de>Email sender address
AUTH_GOOGLE_ID/SECRETGoogle OAuth (optional)
AUTH_GITHUB_ID/SECRETGitHub OAuth (optional)

Variables set automatically

These are configured in docker-compose.yml — do not override:

VariableValueWhy
DEPLOYMENT_MODEselfhostedRemoves cloud-only features, sets unlimited usage
AUTH_TRUST_HOSTtrueRequired when running behind a proxy or non-standard hostname
DATABASE_URLInternal Docker URLConnects API to the Postgres container
REDIS_URLInternal Docker URLConnects 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 -d

Step 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: 200

Reverse 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 -d

Self-hosted vs Cloud

FeatureCloudSelf-hosted
AI provider keysAira-managed or BYOKBYOK only — you provide all keys
Case limits25/month (free tier)Unlimited
Usage displayShows plan limitsShows "Unlimited"
EmailManaged by AiraConfigure your own Resend or SMTP
Data residencyEU (Frankfurt)Your infrastructure
UpdatesAutomaticPull latest images manually
SupportCommunity (free) or dedicated (enterprise)Included in license

Troubleshooting

API won't start

docker logs aira-api-1 --tail 30

Common causes:

  • Missing or incorrect DB_PASS — check the password matches between .env and 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 migrate

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

If it fails, re-authenticate with your provided token.

Support

For self-hosted deployment support, contact customers@softure-ug.de.

On this page