---
title: Deployment
path: operations/deployment
status: published
---

# Deployment

Operational guide for deploying ScaiDNS. Covers required services, configuration, and the first-time setup flow.

## Prerequisites

- **PowerDNS.** Authoritative server with the HTTP API enabled. Tested against PowerDNS 4.7+.
- **MariaDB or MySQL.** For the application database. ScaiDNS uses `mysql+asyncmy`. A dedicated database is recommended; do not share with PowerDNS's backend.
- **Redis.** For rate limiting and cache. Version 6+ with ACL auth.
- **ScaiKey instance.** For identity. Can be shared with other ScaiLabs services or dedicated.

## Configuration

All configuration is via environment variables. See `.env.example` in the repository for the canonical reference.

### Required

| Variable | Notes |
|----------|-------|
| `DATABASE_URL` | `mysql+asyncmy://user:password@host:3306/scaidns` |
| `REDIS_URL` | `redis://user:password@host:6379/0` |
| `PDNS_API_URL` | e.g., `http://pdns.internal:8081` |
| `PDNS_API_KEY` | Shared secret for PowerDNS HTTP API |
| `SCAIKEY_URL` | e.g., `https://scaikey.scailabs.ai` |
| `SCAIKEY_CLIENT_ID` | OAuth client ID for ScaiDNS |
| `SCAIKEY_CLIENT_SECRET` | OAuth client secret |
| `SCAIKEY_APPLICATION_ID` | ScaiKey application ID for user assignment |
| `SCAIKEY_WEBHOOK_SECRET` | HMAC secret for webhook signature verification |
| `EXTERNAL_URL` | Public URL users hit (e.g., `https://scaidns.example.com`) |
| `SESSION_SECRET` | Cryptographic secret for session management |
| `API_KEY_PEPPER` | Secret added to API key hashes |

### Optional

| Variable | Default | Notes |
|----------|---------|-------|
| `DEBUG` | `false` | Enable in dev; never in production |
| `ENVIRONMENT` | `production` | `development`, `staging`, `production` |
| `LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
| `LOG_FORMAT` | `text` | `text` or `json` |
| `DATABASE_POOL_SIZE` | `20` | Connection pool size |
| `REDIS_MAX_CONNECTIONS` | `20` | |
| `ARQ_MAX_JOBS` | `10` | Background worker concurrency |
| `SCAIKEY_TOKEN_CACHE_TTL` | `60` | JWT validation cache TTL (seconds) |
| `SCAIKEY_JWKS_CACHE_TTL` | `3600` | JWKS cache TTL (seconds) |
| `VALIDATION_CHALLENGE_TTL` | `3600` | Validation challenge expiry (seconds) |
| `VALIDATION_EXPIRY_HOURS` | `72` | Max age before a challenge is expired |
| `VALIDATION_CHECK_INTERVAL_SECONDS` | `60` | Background check cadence |
| `RATE_LIMIT_USER_PER_MINUTE` | `1000` | Default per-JWT rate limit |
| `RATE_LIMIT_API_KEY_PER_MINUTE` | `100` | Default per-API-key rate limit |
| `DNSSEC_DEFAULT_ALGORITHM` | `13` | ECDSA P-256 |
| `DNSSEC_KSK_LIFETIME_DAYS` | `365` | |
| `DNSSEC_ZSK_LIFETIME_DAYS` | `30` | |
| `CORS_ORIGINS` | `[]` | JSON array of allowed origins |

## Services

ScaiDNS runs three processes:

1. **API server.** FastAPI + uvicorn, serves the HTTP API. Stateless, horizontally scalable.
2. **Worker.** `arq` worker, runs background jobs (validation checks, DNSSEC rotation, sync retries).
3. **CLI.** `scaidns` commands for setup and maintenance. Not a long-running process.

Typical systemd units:

```ini
# /etc/systemd/system/scaidns-api.service
[Unit]
Description=ScaiDNS API
After=network.target

[Service]
Type=exec
User=scaidns
WorkingDirectory=/usr/local/scaidns/backend
EnvironmentFile=/usr/local/scaidns/backend/.env
ExecStart=/usr/local/scaidns/backend/.venv/bin/uvicorn app.main:app \
          --host 0.0.0.0 --port 8000 --workers 4
Restart=always

[Install]
WantedBy=multi-user.target
```

```ini
# /etc/systemd/system/scaidns-worker.service
[Unit]
Description=ScaiDNS Worker
After=network.target

[Service]
Type=exec
User=scaidns
WorkingDirectory=/usr/local/scaidns/backend
EnvironmentFile=/usr/local/scaidns/backend/.env
ExecStart=/usr/local/scaidns/backend/.venv/bin/arq app.workers.WorkerSettings
Restart=always

[Install]
WantedBy=multi-user.target
```

## First-time setup

### 1. Database migrations

```bash
cd /usr/local/scaidns/backend
.venv/bin/alembic upgrade head
```

Creates the schema and seeds system roles, templates, and default configuration.

### 2. ScaiKey registration

Register ScaiDNS as an OAuth application in ScaiKey:

- **Application name:** ScaiDNS
- **Redirect URI:** `https://<your-external-url>/oauth/callback`
- **Post-logout URI:** `https://<your-external-url>/`
- **Scopes:** `openid`, `profile`, `email`

Take the resulting `client_id` and `client_secret` and put them in `.env`.

Also in ScaiKey:

- Generate a webhook signing secret. Put it in `SCAIKEY_WEBHOOK_SECRET`.
- Configure the webhook URL: `https://<your-external-url>/api/v1/webhooks/scaikey`.
- Assign users/groups to the ScaiDNS application so they receive user/group events.

There's a helper in the ScaiDNS CLI:

```bash
scaidns register-scaikey --public-url https://<your-external-url>
```

### 3. First sync

Pull the initial user and tenant data from ScaiKey:

```bash
scaidns sync
```

Check the output for created/updated counts. If everything shows zeros, double-check `SCAIKEY_APPLICATION_ID` and that users are assigned to the application.

### 4. Initial platform admin

Creating your first platform admin via the API requires already being one, so use the CLI:

```bash
scaidns assign-admin --user-email your-email@example.com
```

Assigns `platform_admin` at `platform` scope. You can now log in via the web UI and manage further access.

### 5. Nameserver configuration

Set the authoritative nameservers for ScaiDNS-hosted zones:

```bash
curl -X PUT https://<your-external-url>/api/v1/admin/nameservers \
  -H "Authorization: Bearer $YOUR_JWT" \
  -H "Content-Type: application/json" \
  -d '{"nameservers": ["ns1.example.com.", "ns2.example.com."]}'
```

These become NS records in every zone.

## PowerDNS configuration

Recommended PowerDNS settings (in `pdns.conf`):

```
launch=gmysql
gmysql-host=...
gmysql-user=...
gmysql-password=...
gmysql-dbname=pdns

# HTTP API
api=yes
api-key=... # must match PDNS_API_KEY in .env
webserver=yes
webserver-address=0.0.0.0
webserver-port=8081
webserver-allow-from=...  # ScaiDNS host(s) only

# Enable DNSSEC
direct-dnskey=yes

# Recommended
default-soa-content=ns1.@ hostmaster.@ 0 3600 600 604800 600
default-ttl=3600
```

**Important:** Never point ScaiDNS and PowerDNS at the same database. PowerDNS owns its backend; ScaiDNS has its own schema. They communicate via the HTTP API.

## Reverse proxy / TLS

Run ScaiDNS behind a TLS-terminating reverse proxy (nginx, Caddy, HAProxy). An nginx config sketch:

```nginx
server {
    listen 443 ssl http2;
    server_name scaidns.example.com;

    ssl_certificate     /etc/letsencrypt/live/scaidns.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/scaidns.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
```

## Upgrades

For minor version upgrades:

1. Deploy new code.
2. Run migrations: `alembic upgrade head`.
3. Restart API and worker services.
4. Smoke-test `/api/v1/health/ready`.

For major upgrades, check the release notes — schema migrations, config changes, or webhook signature format changes need explicit coordination.

## What's next

- [Health and Monitoring](./health-and-monitoring.md) — readiness checks, metrics, logs.
