This guide covers running Impulse directly, as a systemd service, and in Docker — including startup validation, graceful shutdown, and health checking.
For the fastest first successful request, use Quickstart or Docker Installation first. This page is the broader run-mode reference once you already have a working config.
Prerequisites
Before starting Impulse you need:
- A valid config file (see 02-configuration.md)
- TLS certificates (see 01-certificates.md)
- The
impulsebinary — 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/impulse.git
cd impulse
cargo build --release
# Binary is at
./target/release/impulse
Run Directly
Basic start
impulse --config /etc/impulse/config.yaml
Validate by startup on a safe host or staging instance
Impulse validates its config during startup. There is no standalone --validate flag.
Use one of these approaches:
- start Impulse against the candidate config on a non-production host and stop it after successful startup
- use the Control API staged flow:
POST /admin/runtime/validate,/preview, then/activate
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
impulse --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/impulse
# Now run as a regular user
impulse --config /etc/impulse/config.yaml
Binding port 443 as root with privilege drop
If Impulse starts as root and security.privileges.enabled=true, it drops to the configured user/group after binding the socket:
sudo impulse --config /etc/impulse/config.yaml
# Impulse binds port 443 as root, then drops to user 'impulse'
Run as a systemd Service
Create the system user and directories
sudo useradd --system --no-create-home --shell /usr/sbin/nologin impulse
sudo mkdir -p /etc/impulse/certs /var/log/impulse
sudo chown -R impulse:impulse /etc/impulse /var/log/impulse
Install the binary
sudo cp target/release/impulse /usr/bin/impulse
sudo chmod 755 /usr/bin/impulse
# Grant port 443 binding if not running as root
sudo setcap cap_net_bind_service=+ep /usr/bin/impulse
Copy your config and certificates
sudo cp /path/to/your/config.yaml /etc/impulse/config.yaml
sudo cp certs/fullchain.pem /etc/impulse/certs/fullchain.pem
sudo cp certs/privkey.pem /etc/impulse/certs/privkey.pem
sudo chown impulse:impulse /etc/impulse/certs/*
sudo chmod 640 /etc/impulse/certs/*
Create the systemd unit file
Create /etc/systemd/system/impulse.service:
[Unit]
Description=Impulse HTTP/3 Reverse Proxy
Documentation=https://github.com/Supernova-Labs-Org/impulse
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=impulse
Group=impulse
ExecStart=/usr/bin/impulse --config /etc/impulse/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
LimitNOFILE=65536
# Logging — journald captures stdout/stderr
StandardOutput=journal
StandardError=journal
SyslogIdentifier=impulse
# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/log/impulse
ReadOnlyPaths=/etc/impulse
[Install]
WantedBy=multi-user.target
If you run Impulse 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 theimpulseuser.
Enable and start
sudo systemctl daemon-reload
sudo systemctl enable impulse
sudo systemctl start impulse
# Check status
sudo systemctl status impulse
# Follow logs
sudo journalctl -u impulse -f
Graceful reload (after cert renewal)
# Prefer the staged Control API flow for runtime-managed config changes.
# Use a full restart only when the change touches restart-required settings.
sudo systemctl restart impulse
Run in Docker
Dockerfile
FROM debian:bookworm-slim
RUN useradd --system --no-create-home --shell /usr/sbin/nologin impulse
COPY target/release/impulse /usr/bin/impulse
RUN chmod 755 /usr/bin/impulse
RUN mkdir -p /etc/impulse/certs /var/log/impulse \
&& chown -R impulse:impulse /etc/impulse /var/log/impulse
USER impulse
EXPOSE 9889/udp 9889/tcp
ENTRYPOINT ["/usr/bin/impulse", "--config", "/etc/impulse/config.yaml"]
docker-compose.yml
services:
impulse:
build: .
ports:
- "9889:9889/udp"
- "9889:9889/tcp"
volumes:
- ./config/config.reverse.yaml:/etc/impulse/config.yaml:ro
- ./certs:/etc/impulse/certs:ro
- impulse-logs:/var/log/impulse
restart: unless-stopped
volumes:
impulse-logs:
docker compose up -d
docker compose logs -f impulse
Startup Sequence
When Impulse 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 -k --http1.1 https://127.0.0.1:9902/health
# Readiness — is Impulse ready to serve traffic?
curl -k --http1.1 https://127.0.0.1:9902/ready
# Runtime info (requires auth token)
curl -k --http1.1 -H "Authorization: Bearer <token>" https://127.0.0.1:9902/admin/runtime
Graceful Shutdown
Impulse 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 Impulse 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 impulse:impulse /etc/impulse/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 |