This guide covers running Spooky directly, as a systemd service, and in Docker — including startup validation, graceful shutdown, and health checking.
Prerequisites
Before starting Spooky you need:
- A valid config file (see 02-configuration.md)
- TLS certificates (see 01-certificates.md)
- The
spookybinary — built from source or installed via package
Build from Source
# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
# Clone and build
git clone https://github.com/Supernova-Labs-Org/spooky.git
cd spooky
cargo build --release
# Binary is at
./target/release/spooky
Run Directly
Basic start
spooky --config /etc/spooky/config.yaml
Validate config without starting
spooky --config /etc/spooky/config.yaml --validate
Spooky exits 0 on success, 1 on validation failure, with a descriptive error message.
Foreground with debug logging (development)
Override the log level at runtime by setting the level in your config or using a dev config:
log:
level: debug
format: plain
spooky --config config/config.development.yaml
Binding port 443 without root
# Grant the binary permission to bind privileged ports
sudo setcap cap_net_bind_service=+ep /usr/bin/spooky
# Now run as a regular user
spooky --config /etc/spooky/config.yaml
Binding port 443 as root with privilege drop
If Spooky starts as root and security.privileges.enabled=true, it drops to the configured user/group after binding the socket:
sudo spooky --config /etc/spooky/config.yaml
# Spooky binds port 443 as root, then drops to user 'spooky'
Run as a systemd Service
Create the system user and directories
sudo useradd --system --no-create-home --shell /usr/sbin/nologin spooky
sudo mkdir -p /etc/spooky/certs /var/log/spooky
sudo chown -R spooky:spooky /etc/spooky /var/log/spooky
Install the binary
sudo cp target/release/spooky /usr/bin/spooky
sudo chmod 755 /usr/bin/spooky
# Grant port 443 binding if not running as root
sudo setcap cap_net_bind_service=+ep /usr/bin/spooky
Copy your config and certificates
sudo cp config/config.reverse.yaml /etc/spooky/config.yaml
sudo cp certs/fullchain.pem /etc/spooky/certs/fullchain.pem
sudo cp certs/privkey.pem /etc/spooky/certs/privkey.pem
sudo chown spooky:spooky /etc/spooky/certs/*
sudo chmod 640 /etc/spooky/certs/*
Create the systemd unit file
Create /etc/systemd/system/spooky.service:
[Unit]
Description=Spooky HTTP/3 Reverse Proxy
Documentation=https://github.com/Supernova-Labs-Org/spooky
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=spooky
Group=spooky
ExecStart=/usr/bin/spooky --config /etc/spooky/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
LimitNOFILE=65536
# Logging — journald captures stdout/stderr
StandardOutput=journal
StandardError=journal
SyslogIdentifier=spooky
# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/log/spooky
ReadOnlyPaths=/etc/spooky
[Install]
WantedBy=multi-user.target
If you run Spooky as root to bind port 443 and rely on privilege drop, change
User=andGroup=torootand letsecurity.privilegeshandle the drop. Otherwise useAmbientCapabilities=CAP_NET_BIND_SERVICEwith thespookyuser.
Enable and start
sudo systemctl daemon-reload
sudo systemctl enable spooky
sudo systemctl start spooky
# Check status
sudo systemctl status spooky
# Follow logs
sudo journalctl -u spooky -f
Graceful reload (after cert renewal)
# Signal spooky to reload (currently triggers restart)
sudo systemctl reload spooky
# Or full restart
sudo systemctl restart spooky
Run in Docker
Dockerfile
FROM debian:bookworm-slim
RUN useradd --system --no-create-home --shell /usr/sbin/nologin spooky
COPY target/release/spooky /usr/bin/spooky
RUN chmod 755 /usr/bin/spooky
RUN mkdir -p /etc/spooky/certs /var/log/spooky \
&& chown -R spooky:spooky /etc/spooky /var/log/spooky
USER spooky
EXPOSE 9889/udp 9889/tcp
ENTRYPOINT ["/usr/bin/spooky", "--config", "/etc/spooky/config.yaml"]
docker-compose.yml
services:
spooky:
build: .
ports:
- "9889:9889/udp"
- "9889:9889/tcp"
volumes:
- ./config/config.reverse.yaml:/etc/spooky/config.yaml:ro
- ./certs:/etc/spooky/certs:ro
- spooky-logs:/var/log/spooky
restart: unless-stopped
volumes:
spooky-logs:
docker compose up -d
docker compose logs -f spooky
Startup Sequence
When Spooky starts, it follows this order:
- Reads and parses the config file
- Initializes logging and tracing
- Validates the config — exits with error on failure
- Checks if root is required (port < 1024)
- Builds shared runtime state (route index, connection pools)
- Binds UDP sockets (one per worker, or SO_REUSEPORT group)
- Starts the bootstrap TLS listener (HTTP/1.1 + HTTP/2 compatibility)
- Drops privileges if running as root and
security.privileges.enabled=true - Spawns worker threads (data plane)
- Spawns control-plane tasks (health checks, metrics)
- Emits structured startup logs for topology, worker layout, and runtime settings — ready to accept connections
Health and Readiness Checks
If observability.control_api.enabled=true:
# Liveness — is the process alive?
curl http://127.0.0.1:9902/health
# Readiness — is Spooky ready to serve traffic?
curl http://127.0.0.1:9902/ready
# Runtime info (requires auth token)
curl -H "Authorization: Bearer <token>" http://127.0.0.1:9902/admin/runtime
Graceful Shutdown
Spooky handles SIGTERM and SIGINT (Ctrl+C):
- Stops accepting new QUIC connections
- Waits for in-flight requests to complete (up to
performance.shutdown_drain_timeout_ms) - Exits cleanly
Set a generous drain timeout for long-lived streaming requests:
performance:
shutdown_drain_timeout_ms: 10000 # 10 seconds
Verifying Spooky is Running
Test HTTP/3 (QUIC)
# Requires curl with HTTP/3 support
curl --http3-only -k https://localhost:9889/
# With a hostname
curl --http3-only -k https://api.example.com/health
Test HTTP/2 (bootstrap TLS listener)
curl --http2 -k https://localhost:9889/
Check Alt-Svc header (upgrade advertisement)
curl -Ik https://localhost:9889/ | grep -i alt-svc
# Should show: alt-svc: h3=":9889"; ma=86400
Check Prometheus metrics
curl http://127.0.0.1:9901/metrics
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Failed to bind UDP socket: Permission denied |
Port < 1024 without root or CAP_NET_BIND_SERVICE | Use sudo or setcap |
Cannot open listen.tls.cert |
Wrong path or permissions | Check path; chown spooky:spooky /etc/spooky/certs/* |
worker_threads > 1 requires reuseport=true |
Config mismatch | Add reuseport: true to performance |
Clients get connection refused on TCP |
Bootstrap TLS listener failed to bind | Check logs for bootstrap bind error |
curl: (35) OpenSSL SSL_connect |
Certificate mismatch or untrusted | See 01-certificates.md |
| Health check always fails | Backend unreachable or wrong health path | Verify backend is up and health path returns 200 |
| High memory usage | max_response_body_bytes too high or streaming not draining |
Tune body caps in performance section |