Context: this is part of Guides — Deploy. Start with the primer if you haven't.
The simplest production path: one authserver binary, one systemd unit, one reverse proxy in front. Suitable for single-instance deployments with SQLite or single-instance Postgres. For multi-node, see Helm or Raw manifests.
- A hardened systemd unit running
authserver serve. - TLS terminated by Caddy (auto) or nginx (Let's Encrypt).
- A systemd timer running
authserver purge(see Backup & purge).
- Linux host with systemd 245+ (Ubuntu 22.04 / Debian 12 / RHEL 9 or newer).
openssl;curl; a publicly resolvable DNS name pointing at the host.- Reverse-proxy binary: Caddy 2 (simplest TLS) or nginx + certbot.
- Read Configuration first — pick storage / signing-key store.
# Verified against the GitHub Releases asset naming
curl -fsSL https://github.com/authplane/authserver/releases/latest/download/authserver-linux-amd64 \
-o /usr/local/bin/authserver
chmod +x /usr/local/bin/authserver
authserver version # see docs/reference/cli.md#cli-versionuseradd --system --no-create-home --shell /usr/sbin/nologin authserver
mkdir -p /var/lib/authserver/keys /etc/authserver
chown -R authserver:authserver /var/lib/authserver
chmod 700 /var/lib/authserver/keysMinimum viable production config (verified against docs/reference/configuration.md):
server:
issuer: https://auth.example.com # AUTHPLANE_SERVER_ISSUER
address: ":9000"
shutdown_wait: 20s # match TimeoutStopSec below
storage:
driver: sqlite # or "postgres" — see configuration.md
sqlite:
path: /var/lib/authserver/authserver.db
wal: true
signing:
algorithm: ES256
key_store: keyfile
key_path: /var/lib/authserver/keys
session:
secret: "" # injected via systemd EnvironmentFile
secure: true
same_site: lax
admin:
enabled: true
address: "127.0.0.1:9001" # loopback-only; reverse proxy never touches it
api_key: "" # injected via systemd EnvironmentFile
observability:
logging: { level: info, format: json }
metrics: { provider: prometheus, path: /metrics }
server:
allowed_origins: ["https://app.example.com"] # required for browser MCP clientsLock the file down (it does not contain secrets, but it does contain the resource layout):
chown authserver:authserver /etc/authserver/config.yaml
chmod 600 /etc/authserver/config.yamlcat >/etc/authserver/secrets.env <<EOF
AUTHPLANE_SESSION_SECRET=$(openssl rand -hex 32)
AUTHPLANE_ADMIN_API_KEY=$(openssl rand -hex 32)
EOF
chown root:authserver /etc/authserver/secrets.env
chmod 640 /etc/authserver/secrets.envEnv var names verified against docs/reference/env-vars.md.
/etc/systemd/system/authserver.service:
[Unit]
Description=Authplane MCP Authorization Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=authserver
Group=authserver
EnvironmentFile=/etc/authserver/secrets.env
ExecStart=/usr/local/bin/authserver serve --config /etc/authserver/config.yaml
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
# Match server.shutdown_wait + a small margin
TimeoutStopSec=30s
KillSignal=SIGTERM
# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/authserver
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
[Install]
WantedBy=multi-user.targetEnable and start:
systemctl daemon-reload
systemctl enable --now authserver
journalctl -u authserver -fCaddy — TLS handled automatically via Let's Encrypt:
auth.example.com {
reverse_proxy 127.0.0.1:9000
}nginx — TLS via certbot:
server {
listen 443 ssl http2;
server_name auth.example.com;
ssl_certificate /etc/letsencrypt/live/auth.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/auth.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}The admin port (:9001) stays on loopback — access the Admin UI via ssh -L 9001:127.0.0.1:9001 then visit http://localhost:9001/admin/ui/.
ufw allow 443/tcp
ufw deny 9000/tcp
ufw deny 9001/tcpauthserver purge is not run automatically by serve. Wire a systemd.timer — see Backup & purge → systemd timer.
# Public discovery (through the reverse proxy)
curl -fsS https://auth.example.com/.well-known/oauth-authorization-server | jq -r .issuer
# Health probe
curl -fsS https://auth.example.com/health | jq .
# Admin (loopback only)
source /etc/authserver/secrets.env
curl -fsS -H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY" \
http://127.0.0.1:9001/admin/stats | jq .
# systemd state
systemctl is-active authserver && echo OKauthserver serve traps SIGTERM / SIGINT, drains the public server then the admin server, bounded by server.shutdown_wait (default 10s, env AUTHPLANE_SERVER_SHUTDOWN_WAIT). Keep TimeoutStopSec in the unit ≥ shutdown_wait + a small margin; the systemd default of 90s is comfortably above the authserver default.
systemctl kill -s HUP authserver
journalctl -u authserver --since '1 minute ago' | grep 'signing keys reloaded'Useful after rotating on-disk keys out of band (e.g. Vault Transit token roll, manual key file replacement). No connections are dropped.
# Verified against docs/reference/cli.md#cli-admin-key-rotate
sudo -u authserver authserver admin key rotate --config /etc/authserver/config.yamlZero-downtime. The previous key stays in the JWKS until its tokens expire; the new key becomes current and is signed-with immediately. See Operate → Key rotation for policy guidance.
serve runs Migrate(ctx) on boot before binding listeners — no manual step is required for a normal upgrade. If you want a pre-flight gate:
sudo -u authserver authserver migrate --config /etc/authserver/config.yaml
# Verified against docs/reference/cli.md#cli-migrateMigrations are idempotent — repeated runs no-op when the schema is current.
systemctl stop authserver
cp /usr/local/bin/authserver /usr/local/bin/authserver.bak
curl -fsSL https://github.com/authplane/authserver/releases/latest/download/authserver-linux-amd64 \
-o /usr/local/bin/authserver
chmod +x /usr/local/bin/authserver
systemctl start authserverSQLite — online backup, no downtime:
sqlite3 /var/lib/authserver/authserver.db ".backup /var/backups/authserver-$(date +%Y%m%d).db"
tar czf /var/backups/keys-$(date +%Y%m%d).tar.gz -C /var/lib/authserver keysSigning keys must be backed up — without them every outstanding JWT is unverifiable. For full coverage (Postgres, Vault) see Backup & purge.
| Symptom | Likely cause | Fix |
|---|---|---|
boot rejected: admin.api_key is required in journal |
AUTHPLANE_ADMIN_API_KEY missing from secrets.env or < 16 chars |
Regenerate (openssl rand -hex 32), restart unit. Default-blocklist tokens (changeme, dev-admin-key, etc.) are also rejected. |
Permission denied on /var/lib/authserver/keys |
wrong owner after chown step skipped |
chown -R authserver:authserver /var/lib/authserver && chmod 700 /var/lib/authserver/keys. |
In-flight requests SIGKILLed on systemctl restart |
TimeoutStopSec < server.shutdown_wait |
Raise TimeoutStopSec to match, or lower shutdown_wait. |
| Browser MCP client fails CORS preflight | startup WARN CORS is disabled because server.allowed_origins is empty |
Set AUTHPLANE_SERVER_ALLOWED_ORIGINS to your origin list (or * for dev). |
All clients get invalid_token after rotate |
JWKS-cache TTL on the verifier (SDKs default to 1 h) | Wait for the verifier's cache, or force-refresh; rotation itself is zero-downtime on the authserver side. |
| DB tables grow unbounded | No purge timer wired | Follow Backup & purge → systemd timer. |
- Docker Compose — same workloads in containers.
- Helm — Kubernetes path.
- HashiCorp Vault Transit — replace the keyfile store with Vault.
- Backup & purge — purge timer + SQLite/Postgres backup recipes.
docs/reference/cli.md— every subcommand and flag.