intermediate self-hostingdockerdeploymentproduction

Self-Hosting Guide

Deploy Sigil Auth with Docker Compose — mnemonic initialization, webhooks, TLS, and production configuration

Self-Hosting Guide

Sigil Auth is designed to run on your infrastructure. This guide walks you through deploying the full stack with Docker Compose, from first boot to production configuration.

What You’ll Need

  • Docker and Docker Compose installed
  • A server with 2 CPU cores and 4GB RAM minimum
  • APNs and FCM credentials (for push notifications)
  • A domain name (for production TLS)

Quick Start (Development)

Get the stack running in under 5 minutes.

Step 1: Clone the repository

git clone https://github.com/sigilauth/server.git
cd server

Step 2: Start the stack

make up

This starts three services:

  • sigil — The stateless auth server (port 8443)
  • relay — Push notification relay (port 8080)
  • postgres — Database for relay token storage

Step 3: Complete initialization

On first boot, the server starts in init mode. Open your browser:

https://localhost:8443/init

Your browser warns about the self-signed certificate. That’s expected in development — click through.

The wizard shows you a 24-word mnemonic. This is the server’s master secret. Write it down. Store it securely. You’ll need it if you restart the container or migrate to a new server.

[!WARNING] If you lose the mnemonic, you cannot recover the server’s keypair. All paired devices will stop working. There is no password reset, no backup mechanism. The mnemonic is the only recovery path.

Click Continue. The server generates its keypair from the mnemonic and shows you:

  • The server’s public key
  • The server’s pictogram (five emoji)

Click Generate API Key. Copy the key. This is what your backend will use to call the Sigil API.

The server is now in operational mode. It’s ready to handle challenges.

Step 4: Set your API key

The init wizard gives you an API key. You need to make it available to your application backend so the SDK can authenticate with Sigil.

Option 1: Shell environment variable (Quick test, ephemeral)

export SIGIL_API_KEY='sgk_test_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2'
node your-app.js

Option 2: .env file (Development, persists across runs)

Create a .env file in your project root:

echo "SIGIL_API_KEY=sgk_test_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2" > .env

Add .env to .gitignore:

echo ".env" >> .gitignore

Load it in your app:

Node.js:

import 'dotenv/config';  // npm install dotenv
import { SigilAuth } from '@sigilauth/sdk';

const sigil = new SigilAuth({
  serviceUrl: 'https://localhost:8443'
});
// SDK reads process.env.SIGIL_API_KEY automatically

Go:

import (
	"github.com/joho/godotenv"  // go get github.com/joho/godotenv
	"github.com/sigilauth/server/sdk-go"
)

godotenv.Load()  // Loads .env into environment
client, _ := sigilauth.New(sigilauth.Config{
	ServiceURL: "https://localhost:8443",
	APIKey:     os.Getenv("SIGIL_API_KEY"),
	// ...
})

Option 3: Docker Compose environment (Running app in container)

services:
  app:
    image: your-app:latest
    environment:
      SIGIL_API_KEY: sgk_test_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2

Option 4: Secret manager (Production — use AWS Secrets Manager, HashiCorp Vault, etc.)

Never commit API keys to git. Never hardcode them in source code. The SDK enforces this — it will throw an error if you try to pass the key directly instead of loading from the environment.


Configuration Reference

The stack is configured via environment variables. Check docker-compose.yml for the full list. Here are the important ones:

Sigil Service

Variable Default Description
SIGIL_MODE operational Boot mode: init or operational
SIGIL_MNEMONIC 24-word BIP39 mnemonic (development only — never use in production; insecure to pass secrets via env vars)
SIGIL_MNEMONIC_PATH Path to file containing mnemonic (required in production unless using SIGIL_MNEMONIC for dev; server will not start without one or the other)
RELAY_URL http://relay:8080 Push relay service URL
API_KEY Pre-set API key (optional, wizard generates one)
WEBHOOK_URL Your webhook endpoint for async events
TLS_CERT_PATH /var/lib/sigil/tls/cert.pem Path to TLS certificate
TLS_KEY_PATH /var/lib/sigil/tls/key.pem Path to TLS private key
TLS_AUTO_GENERATE true Auto-generate self-signed cert on first boot
SIGIL_PORT 8443 HTTPS port
SIGIL_TELEMETRY full Telemetry mode: full, minimal, or none
SIGIL_ATTESTATION_REQUIRED false Require device attestation (set true in production)
SIGIL_ALLOW_EMULATORS false Allow emulator/simulator devices (dev only)

Relay Service

Variable Default Description
DATABASE_URL PostgreSQL connection string
APNS_KEY_PATH Path to APNs .p8 key file
APNS_KEY_ID APNs key ID
APNS_TEAM_ID APNs team ID
FCM_SERVICE_ACCOUNT Path to FCM service account JSON
RELAY_PORT 8080 HTTP port (internal only)
LOG_LEVEL info Log level: debug, info, warn, error

Production Deployment

Development mode uses self-signed TLS and allows emulators. Production requires real certificates and device attestation.

Step 1: Prepare your mnemonic

Generate a mnemonic or reuse the one from your development setup. Store it in a file, not an environment variable.

echo "your 24 word mnemonic here" > /var/lib/sigil/mnemonic.txt
chmod 600 /var/lib/sigil/mnemonic.txt

In production, use a secret manager (AWS Secrets Manager, HashiCorp Vault, etc.) instead of a file on disk.

Step 2: Get TLS certificates

You need a valid certificate for your domain. Three options:

Option A: Let’s Encrypt with Caddy

Use the provided Caddy reverse proxy configuration at deploy/caddy/Caddyfile. Caddy handles ACME automatically.

cd deploy/caddy
docker-compose up -d

Option B: Let’s Encrypt with Certbot

certbot certonly --standalone -d sigil.example.com

Then mount the cert into the container:

volumes:
  - /etc/letsencrypt/live/sigil.example.com:/var/lib/sigil/tls:ro

Option C: Provide your own certificate

Place cert.pem and key.pem in /var/lib/sigil/tls/ and mount the directory.

Step 3: Configure reverse proxy and trusted IPs

[!WARNING] When deploying behind a reverse proxy (Caddy, nginx, CloudFlare), configure TRUSTED_PROXY_IP or equivalent env var so the server correctly identifies client IPs for rate limiting. Without this, X-Forwarded-For can be spoofed by attackers, bypassing rate limits.

Caddy example:

Caddy automatically sets X-Forwarded-For. Configure Sigil to trust the proxy:

export TRUSTED_PROXY_IP=172.18.0.1  # Your Caddy container IP

Or trust the entire Docker network:

export TRUSTED_PROXY_CIDR=172.18.0.0/16

Nginx example:

Configure nginx to pass the real client IP:

location / {
    proxy_pass https://sigil:8443;
    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;
}

Then configure Sigil to trust nginx:

export TRUSTED_PROXY_IP=172.18.0.2  # Your nginx container IP

CloudFlare:

If you’re behind CloudFlare, trust their IP ranges and use CF-Connecting-IP instead of X-Forwarded-For:

export TRUSTED_PROXY_PROVIDER=cloudflare
export USE_CF_CONNECTING_IP=true

Sigil will automatically fetch and trust CloudFlare’s published IP ranges.

Direct deployment (no proxy):

If Sigil is directly exposed to the internet, leave TRUSTED_PROXY_IP unset. The server will use the connection’s remote address for rate limiting.

Step 4: Configure production overrides

Use the production compose file:

docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

This applies production settings:

  • Resource limits
  • Attestation required
  • No emulators allowed
  • TLS from provided certs (not auto-generated)
  • Named volumes for persistence

Step 5: Set APNs and FCM credentials

APNs (iOS):

  1. Download your .p8 key from Apple Developer
  2. Mount it into the relay container:
volumes:
  - ./apns-key.p8:/var/lib/relay/apns-key.p8:ro
  1. Set environment variables:
export APNS_KEY_PATH=/var/lib/relay/apns-key.p8
export APNS_KEY_ID=ABCD1234EF
export APNS_TEAM_ID=XYZ9876543

FCM (Android):

  1. Download your service account JSON from Firebase Console
  2. Mount it:
volumes:
  - ./fcm-service-account.json:/var/lib/relay/fcm-service-account.json:ro
  1. Set environment variable:
export FCM_SERVICE_ACCOUNT=/var/lib/relay/fcm-service-account.json

Step 6: Configure webhooks

If you want async notification of challenge results, set a webhook URL:

export WEBHOOK_URL=https://your-app.example.com/webhooks/sigil

Generate a webhook secret:

export WEBHOOK_SECRET=$(openssl rand -hex 32)

Your app will use this secret to verify webhook signatures. See SDK documentation for details.

Step 7: Start the stack

make prod

Or manually:

docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Check logs:

make logs

Verify health:

curl -k https://localhost:8443/health

Expected responses:

In init mode (before completing the wizard):

{
  "status": "initializing",
  "mode": "init",
  "version": "1.0.0"
}

In operational mode (after initialization):

{
  "status": "healthy",
  "mode": "operational",
  "version": "1.0.0"
}

If you see "status": "unhealthy", check the logs with make logs or docker-compose logs sigil.


Webhook Configuration

Webhooks notify your app when challenges complete. This is more efficient than polling.

Setting up webhooks

Set WEBHOOK_URL and WEBHOOK_SECRET environment variables on the Sigil service.

Sigil will POST to your webhook URL when:

  • A challenge is verified
  • A challenge is rejected
  • A challenge expires
  • An MPA request is approved
  • An MPA request is rejected
  • An MPA request times out

Webhook payload format

{
  "type": "challenge.verified",
  "challenge_id": "550e8400-e29b-41d4-a716-446655440000",
  "fingerprint": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
  "status": "verified",
  "timestamp": "2026-04-24T10:32:15Z"
}

Event types:

  • challenge.verified
  • challenge.rejected
  • challenge.expired
  • mpa.approved
  • mpa.rejected
  • mpa.timeout

Verifying webhook signatures

Every webhook includes these headers:

X-Sigil-Signature: v1,<hmac-sha256-hex>
X-Sigil-Timestamp: <unix-timestamp>

Use the SDK to verify:

Node:

const isValid = sigil.webhooks.verify(
  req.headers,
  req.body,  // raw Buffer, not parsed JSON
  process.env.WEBHOOK_SECRET
);

Go:

err := client.Webhooks.Verify(
  r.Header,
  body,
  os.Getenv("WEBHOOK_SECRET"),
)

Reject webhooks older than 5 minutes to prevent replay attacks.


Observability

Sigil Auth provides metrics, logs, and distributed tracing.

Metrics (Prometheus)

The /metrics endpoint exposes Prometheus metrics:

http://localhost:8443/metrics

Key metrics:

  • sigil_challenges_total{status="verified|rejected|expired"}
  • sigil_mpa_requests_total{status="approved|rejected|timeout"}
  • sigil_webhook_deliveries_total{status="success|failure"}
  • sigil_challenge_duration_seconds (histogram)

Hook this up to your Prometheus server. A Grafana dashboard is provided at deploy/observability/grafana/dashboards/sigil.json.

Logs (structured JSON)

Both services log to stdout in structured JSON:

{
  "timestamp": "2026-04-24T10:32:15Z",
  "level": "info",
  "service": "sigil",
  "challenge_id": "550e8400-e29b-41d4-a716-446655440000",
  "event": "challenge_created",
  "fingerprint": "a1b2c3...",
  "action_type": "step_up"
}

Collect with your log aggregator (Loki, Elasticsearch, CloudWatch, etc.).

Set LOG_LEVEL=debug for verbose output (development only — noisy in production).

Distributed Tracing (OpenTelemetry)

Sigil exports traces in OpenTelemetry format. Configure the collector endpoint:

export OTEL_EXPORTER_OTLP_ENDPOINT=http://your-collector:4317

Traces cover:

  • Challenge creation → push → response → verification
  • MPA request → multi-device coordination → quorum
  • Webhook delivery attempts

Backup and Recovery

What to back up

  1. The mnemonic — This is the only thing you need. Store it offline, encrypted, in multiple locations.
  2. Relay database — PostgreSQL backups for push token mappings. Losing this means devices won’t get push notifications until they re-register.

What you don’t need to back up

  • Sigil service state (it’s stateless — everything derives from the mnemonic)
  • TLS certificates (regenerate with ACME or re-issue)
  • API keys (regenerate via the init wizard)

Disaster recovery procedure

If your server is destroyed:

  1. Spin up a new server
  2. Restore the mnemonic to SIGIL_MNEMONIC_PATH
  3. Start the stack with SIGIL_MODE=operational
  4. The server regenerates the same keypair from the mnemonic
  5. Devices see the same server pictogram and continue working
  6. Restore the relay database from backup (or let devices re-register their push tokens)

The mnemonic is the single source of truth. Protect it like a root password.


Security Checklist

Before going to production:

  • [ ] Mnemonic stored in a secret manager, not a file or environment variable
  • [ ] SIGIL_ATTESTATION_REQUIRED=true (rejects devices without hardware attestation)
  • [ ] SIGIL_ALLOW_EMULATORS=false (blocks simulators and emulators)
  • [ ] TLS certificate from a trusted CA (not self-signed)
  • [ ] Webhook secret is 32+ random bytes
  • [ ] Relay database not exposed to the public internet
  • [ ] API keys rotated and stored securely
  • [ ] Logs scrubbed of sensitive data (Sigil does this by default)
  • [ ] Metrics endpoint not publicly accessible (or protected by auth)
  • [ ] Resource limits set in docker-compose.prod.yml

Scaling

Sigil Auth is stateless. You can run multiple replicas behind a load balancer without coordination.

The relay service is also stateless except for PostgreSQL. Scale horizontally and use a managed Postgres instance (RDS, Cloud SQL, etc.).

For high availability:

  • Run 3+ Sigil replicas across availability zones
  • Use a managed PostgreSQL cluster with replication
  • Distribute the relay service geographically for lower push latency

Typical load capacity per instance (2 CPU, 4GB RAM):

  • 1000 challenges/second
  • 500 MPA requests/second
  • 10,000 concurrent challenge sessions

Upgrading

Sigil Auth follows semantic versioning. Minor and patch updates are backwards-compatible.

Upgrade procedure

  1. Pull the latest images:
docker-compose pull
  1. Restart the stack:
docker-compose up -d
  1. Check health:
curl -k https://localhost:8443/health

Database migrations (relay only) run automatically on startup.

Before major version upgrades, check the changelog for breaking changes.


Troubleshooting

“SIGIL_MNEMONIC or SIGIL_MNEMONIC_PATH required”

The server needs a mnemonic to derive its keypair. Either:

  • Set SIGIL_MNEMONIC (dev only), or
  • Set SIGIL_MNEMONIC_PATH to a file containing the mnemonic

“Failed to connect to relay”

The Sigil service can’t reach the relay. Check:

  • Is the relay container running? (docker ps)
  • Is RELAY_URL correct? (default: http://relay:8080)
  • Are they on the same Docker network?

“Push notification failed: 404 device not found”

The device hasn’t registered its push token with the relay. The app registers on first launch. If you reset the relay database, devices need to re-register (restart the app or wait for the next background refresh).

“Certificate verify failed”

In development, the Sigil service uses a self-signed certificate. Your SDK client needs to skip verification (Node: rejectUnauthorized: false, Go: InsecureSkipVerify: true) or trust the self-signed cert.

In production, use a real certificate from Let’s Encrypt or a trusted CA.

Logs show “challenge expired before response”

The device took longer than 5 minutes to respond. Challenges have a hard 5-minute TTL. Check:

  • Is the device online?
  • Did the user get the push notification?
  • Is the relay delivering pushes? (check relay logs)

Next Steps

For production support, monitoring, and SLA guarantees, see the hosted offering at sigilauth.com (coming soon).