Caddy
Reverse proxy with automatic HTTPS for local development
Caddy is a modern web server with automatic HTTPS built in. Stackpanel uses Caddy as a reverse proxy so you can access your local services through clean URLs like https://myapp.local instead of http://localhost:4200.
Why a Reverse Proxy?
Running multiple services locally means juggling multiple ports: your web app on 4200, your API on 4201, Minio on 4260. A reverse proxy gives you:
- Readable URLs —
https://myapp.localinstead ofhttp://localhost:4200 - Automatic HTTPS — TLS certificates provisioned through Step CA, so your local dev environment behaves like production
- Single entry point — Route different paths or subdomains to different backend services
- CORS simplification — Everything goes through one origin, eliminating cross-origin headaches during development
Enable Caddy
stackpanel.caddy = {
enable = true;
};When enabled, Caddy is added to your devshell and managed as a service through process-compose. It starts alongside your other services when you run stackpanel services start.
Defining Routes
Routes map incoming requests to your local services. Define them under stackpanel.caddy.routes:
stackpanel.caddy = {
enable = true;
routes = {
web = {
from = "myapp.local";
to = "localhost:${toString config.stackpanel.ports.computed.web}";
};
api = {
from = "api.myapp.local";
to = "localhost:${toString config.stackpanel.ports.computed.api}";
};
};
};After starting services, https://myapp.local proxies to your web app and https://api.myapp.local proxies to your API server.
Routes use Nix expressions, so they automatically reference the correct deterministic ports. If your port assignments change, the Caddy config updates to match.
Managing Caddy
Use the Stackpanel CLI to control Caddy:
# Start Caddy (usually started with all services)
stackpanel caddy start
# Stop Caddy
stackpanel caddy stop
# Check status
stackpanel caddy status
# Link this project's generated sites into the shared proxy
stackpanel caddy add
# Unlink this project's sites again
stackpanel caddy removeRoutes themselves are declared in your config (for example via
stackpanel.apps.<app>.domain) and generated deterministically into
.stack/gen/caddy/ on devshell entry. stackpanel caddy add / remove only
link/unlink those generated files into the global ~/.config/caddy/sites.d/ —
they never generate or delete the files. You can also link a single site by
passing its domain (e.g. stackpanel caddy add web.myapp.localhost).
Or manage it through Studio, where routes are visible in the networking panel.
HTTPS and Certificates
For local HTTPS to work, Caddy needs a certificate authority (CA) that your browser trusts. Stackpanel integrates with Step CA to handle this:
- Step CA issues certificates for your local domains
- Caddy automatically requests and renews certificates from Step CA
- Your browser trusts the certificates because Step CA's root cert is installed in your system trust store
{
stackpanel.step-ca = {
enable = true;
ca-url = "https://ca.internal:443";
ca-fingerprint = "abc123...";
};
stackpanel.caddy = {
enable = true;
tls = {
ca = config.stackpanel.step-ca.ca-url;
ca-root = config.stackpanel.step-ca.root-cert;
};
};
}HTTPS requires Step CA to be configured. Without it, Caddy falls back to HTTP-only proxying. If you don't need HTTPS locally, you can skip the TLS configuration entirely.
Path-Based Routing
Instead of subdomains, you can route based on URL paths:
stackpanel.caddy = {
enable = true;
routes = {
web = {
from = "myapp.local";
to = "localhost:${toString config.stackpanel.ports.computed.web}";
};
api = {
from = "myapp.local/api/*";
to = "localhost:${toString config.stackpanel.ports.computed.api}";
strip_prefix = "/api";
};
minio = {
from = "myapp.local/storage/*";
to = "localhost:${toString config.stackpanel.ports.computed.minio}";
strip_prefix = "/storage";
};
};
};This routes myapp.local/api/users to your API's /users endpoint, and myapp.local/storage/ to Minio.
DNS Configuration
For .local domains to resolve to localhost, you need a local DNS resolver. Stackpanel can manage this through the DNS configuration, or you can add entries to /etc/hosts manually:
127.0.0.1 myapp.local
127.0.0.1 api.myapp.localOn macOS, you can use dscacheutil -flushcache after editing /etc/hosts to pick up
changes immediately.
Custom Caddyfile Snippets
For advanced use cases, you can inject raw Caddyfile directives:
stackpanel.caddy = {
enable = true;
extraConfig = ''
(cors) {
header Access-Control-Allow-Origin *
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization"
}
'';
routes.api = {
from = "api.myapp.local";
to = "localhost:${toString config.stackpanel.ports.computed.api}";
extraDirectives = ''
import cors
encode gzip
'';
};
};Combining with Other Services
A typical full-stack setup uses Caddy to front all your local services:
{
stackpanel.globalServices = {
enable = true;
postgres.enable = true;
redis.enable = true;
minio.enable = true;
};
stackpanel.caddy = {
enable = true;
routes = {
web = {
from = "myapp.local";
to = "localhost:${toString config.stackpanel.ports.computed.web}";
};
api = {
from = "api.myapp.local";
to = "localhost:${toString config.stackpanel.ports.computed.api}";
};
minio-console = {
from = "storage.myapp.local";
to = "localhost:${toString config.stackpanel.ports.computed.minio-console}";
};
};
};
}All services—including Caddy—are started together with stackpanel services start and stopped together with stackpanel services stop.
Troubleshooting
"address already in use" on port 80 or 443
Another web server (Apache, nginx, or another Caddy instance) is using the standard HTTP/HTTPS ports. Either stop the conflicting service or configure Caddy to use alternate ports:
stackpanel.caddy = {
enable = true;
http_port = 8080;
https_port = 8443;
};Browser shows certificate warning
This usually means the Step CA root certificate isn't trusted by your system. See Certificates → Trust Store for instructions on installing the root cert.
Domain not resolving
Make sure your .local domains resolve to 127.0.0.1. Check with:
dig myapp.local
# or
nslookup myapp.localIf they don't resolve, add entries to /etc/hosts or configure local DNS.
Caddy not starting
Check the logs for details:
stackpanel logs caddyCommon causes: port conflicts, invalid route configuration, or certificate provisioning failures.
Reference
- Options Reference → Caddy for all Caddy configuration options
- CLI → Caddy for CLI commands
- Certificates for HTTPS and Step CA configuration
- DNS for local domain resolution
- Caddy documentation for Caddy-specific configuration