Olympus Docs
CookbookMulti-tenant

Multi-tenant with hard isolation

One Olympus instance per tenant, physical separation

For regulated industries (finance, healthcare) or large enterprise customers, "soft" isolation (one DB, tenant column) often isn't acceptable. They want their data physically isolated.

Approaches

Approach A: Per-tenant container stack

Each tenant gets its own:

  • Postgres database (or DB cluster).
  • Kratos, Hydra, Hera, Athena containers.
  • Caddy vhost.

Routing: subdomain acme.your-app.comacme- container set.

Approach B: Per-tenant schema, shared infra

Single Postgres, single Kratos. Each tenant has a logical schema (Kratos's kratos_acme, kratos_bigcorp).

Olympus does NOT support multi-schema natively, this requires a custom build. Skip unless you have a strong reason.

Approach C: Per-tenant DB, shared services

Single Kratos, Hydra; but they route to per-tenant Postgres based on hostname/subdomain.

Hardest to operate. Not recommended.

Most operationally clean. Each tenant is a complete vertical slice.

Provisioning script

#!/bin/bash
# provision-tenant.sh <tenant-name>
set -e
TENANT=$1
PORT_BASE=$(( 30000 + ($(echo $TENANT | sum | awk '{print $1}') % 1000) * 100 ))

# Create directory
mkdir -p /srv/tenants/$TENANT
cd /srv/tenants/$TENANT

# Per-tenant compose
cat > docker-compose.yml <<EOF
services:
  postgres:
    image: postgres:16
    environment: 
      POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}
    volumes: [data:/var/lib/postgresql/data]
  kratos:
    image: oryd/kratos:v1.2.0
    depends_on: [postgres]
    ports: ["${PORT_BASE}:5000"]
  hydra:
    image: oryd/hydra:v2.2.0
    depends_on: [postgres]
    ports: ["$((PORT_BASE+1)):4444"]
  hera:
    image: ghcr.io/olympusoss/hera:v1.4.0
    ports: ["$((PORT_BASE+2)):3000"]
  athena:
    image: ghcr.io/olympusoss/athena:v1.4.0
    ports: ["$((PORT_BASE+3)):3001"]
volumes: { data: {} }
EOF

# Per-tenant env
openssl rand -base64 32 > .env-secrets
cat > .env <<EOF
TENANT=$TENANT
POSTGRES_PASSWORD=$(cat .env-secrets)
EOF
chmod 600 .env

podman-compose up -d

# Caddy vhost
cat > /etc/caddy/sites-available/$TENANT.conf <<EOF
$TENANT.your-app.com {
  reverse_proxy localhost:$((PORT_BASE+2))
}
$TENANT-api.your-app.com {
  reverse_proxy localhost:$((PORT_BASE+1))
}
EOF
ln -s /etc/caddy/sites-available/$TENANT.conf /etc/caddy/sites-enabled/
caddy reload

Resource constraints

Each tenant stack uses ~500 MB RAM at idle. Plan host sizing:

  • Small (< 50 tenants): single host, ~32 GB RAM.
  • Medium (50-500): multiple hosts behind LB.
  • Large (500+): consider a more abstracted PaaS (Kubernetes per tenant, etc.).

Per-tenant DNS

Wildcard:

*.your-app.com → host (single host)
*.your-app.com → LB → tenant-aware router (multi-host)

For multi-host, route tenant-X to the host running tenant-X's stack. Use Caddy's vars or a Lua plugin.

Costs

Hard isolation is expensive:

  • Each stack = ~500 MB RAM at idle.
  • Each Postgres = a backup target.
  • Each Caddy vhost = a TLS cert.

For a tenant paying $10k+/mo, justified. For free tier, never.

When customers ask for hard isolation

Validate the requirement:

  • "We need our data separate", soft is often fine if you can show RLS + per-tenant encryption.
  • "Our auditor requires physical separation", get the auditor's actual ask in writing. Often soft + good docs satisfies.
  • "We need our own encryption key", hard isolation OR per-tenant encryption keys (custom build).

Usually soft + careful auditing is enough. Hard is only needed when an auditor explicitly demands.

Cross-tenant features

Some features need to span tenants (e.g., a "superadmin" who can impersonate tenant admins). Don't try to backdoor this in the per-tenant stack, keep a separate "control plane" that:

  • Has a list of tenants and their endpoints.
  • Has its own auth (separate Olympus stack?).
  • Calls each tenant's Athena admin API as needed.

Operations

Multi-stack ops:

# Apply same change everywhere
for tenant in /srv/tenants/*/; do
  cd $tenant
  git pull
  podman-compose pull
  podman-compose up -d
done

Wrap in error handling / parallelism. At scale, use Ansible / Pulumi.

On this page