Caddy Reverse Proxy
Overview
ASD uses Caddy as the local reverse proxy between your services and the outside world. Every request from a tunnel or local hostname passes through Caddy before reaching your application.
Internet --> ASD Cloud --> SSH Tunnel --> Caddy (local) --> Your Service
|
Routing, TLS,
auth, headers Caddy is configured declaratively through your asd.yaml. You never write a Caddyfile by hand. ASD generates the Caddy JSON configuration from your service definitions and applies it via the Caddy Admin API.
Commands
asd caddy start # Start the Caddy reverse proxy
asd caddy stop # Stop Caddy
asd caddy restart # Restart with updated configuration
asd caddy config # Show the current generated Caddy configuration Caddy starts automatically when you run asd net apply --caddy or set auto_start_caddy: true in your features block. By default, Caddy listens on port 80 (HTTP) and port 443 (HTTPS when TLS is enabled). The admin API listens on localhost:2019.
How Caddy Fits In
ASD provides three access patterns for every service. Caddy controls two of them:
| Access Pattern | Goes Through Caddy | Description |
|---|---|---|
| Local Direct | No | http://localhost:3000 — raw connection |
| Caddy Route | Yes | http://myapp.localhost — local routing |
| Tunnel Remote | Yes | https://myapp-abc123.eu1.tn.asd.engineer — public access |
When Caddy is not running, only local direct access works. Tunnel traffic always flows through Caddy because the SSH tunnel terminates at the Caddy listener.
Routing Modes
Caddy supports two routing strategies. You can use both simultaneously for the same service.
Host-Based Routing
Each service gets its own hostname:
network:
services:
frontend:
dial: "127.0.0.1:5173"
host: "app.localhost" Access: http://app.localhost/
Path-Based Routing
Multiple services share a single hostname, distinguished by URL path:
network:
services:
api:
dial: "127.0.0.1:8080"
paths: ["/api"]
stripPrefix: true Access: http://asd.localhost/api/
With stripPrefix: true, Caddy removes /api before forwarding to the upstream. Your API receives requests at / instead of /api/.
Route Priority
When multiple services have overlapping paths, use priority to control matching order. Higher values match first:
network:
services:
api-v2:
dial: "127.0.0.1:8081"
paths: ["/api/v2"]
priority: 60
api:
dial: "127.0.0.1:8080"
paths: ["/api"]
priority: 50 Without priority, /api/v2/users might match the broader /api rule first.
Publish Preference
The publishPreferred property controls which routing strategy is used for tunnel URLs:
network:
services:
myapp:
dial: "127.0.0.1:3000"
host: "app.localhost"
paths: ["/"]
publishPreferred: "host" # "host", "path", or "both" TLS Configuration
Caddy handles TLS certificates for local HTTPS:
network:
caddy:
tls:
enabled: true
auto: true # Self-signed certificates for *.localhost For custom certificates:
network:
caddy:
tls:
enabled: true
cert_file: "/path/to/cert.pem"
key_file: "/path/to/key.pem" For automatic certificates from a public certificate authority (ACME protocol, used by Let’s Encrypt) with DNS challenge:
network:
caddy:
tls:
enabled: true
acme:
email: "admin@example.com"
dns_provider: "cloudflare" Tunnel traffic already uses HTTPS via the ASD cloud. Local TLS is optional and primarily useful for testing HTTPS-only features like Secure cookies or HSTS.
Basic Authentication
HTTP Basic Auth adds a browser login prompt to services accessed via Caddy or tunnel.
Project-Wide
Protect all services:
network:
caddy:
basic_auth:
enabled: true
realm: "My Project"
routes: ["host", "path", "tunnel"] Credentials in .env:
ASD_BASIC_AUTH_USERNAME=admin
ASD_BASIC_AUTH_PASSWORD=your-secure-password ASD hashes passwords with bcrypt before sending them to Caddy. Plaintext credentials never leave your machine.
Per-Service Override
Disable auth for a public API while keeping everything else protected:
network:
caddy:
basic_auth:
enabled: true
services:
public-api:
dial: "127.0.0.1:3000"
basic_auth:
enabled: false
admin-panel:
dial: "127.0.0.1:8080"
basic_auth:
enabled: true
realm: "Admin Only"
routes: ["host"] Route Type Filtering
The routes property controls which access patterns require authentication:
| Route Type | Meaning |
|---|---|
host | Host-based routes (http://api.localhost) |
path | Path-based routes (http://asd.localhost/api) |
tunnel | Tunnel routes (https://myapp-xxx.eu1.tn.asd.engineer) |
When routes is not specified, authentication applies to all types.
Security-Sensitive Services
Built-in services that provide shell or code access are flagged as security-sensitive:
| Service | Why | Behavior |
|---|---|---|
| ttyd (Web Terminal) | Full shell access | Always requires TTYD_USERNAME and TTYD_PASSWORD |
| code-server (VS Code) | Code editing + terminal | Recommends ASD_CODESERVER_AUTH=password |
You can mark your own services as security-sensitive:
network:
services:
admin-tool:
dial: "127.0.0.1:9000"
securitySensitive: true Security Headers
Add HTTP security headers per service:
network:
services:
production-api:
dial: "127.0.0.1:3000"
securityHeaders:
enableHsts: true
hstsMaxAge: 31536000
frameOptions: "DENY"
enableCompression: true | Property | Type | Description |
|---|---|---|
enableHsts | boolean | Add Strict-Transport-Security header |
hstsMaxAge | number | HSTS max-age in seconds (default: 1 year) |
frameOptions | "DENY" or "SAMEORIGIN" | X-Frame-Options header value |
enableCompression | boolean | Enable gzip/zstd response compression |
Iframe Embedding
Allow a service to be embedded in an iframe from a specific origin:
network:
services:
dashboard:
dial: "127.0.0.1:3000"
iframeOrigin: "https://dashboard.asd.engineer"
deleteResponseHeaders:
- "Content-Security-Policy"
- "X-Frame-Options" deleteResponseHeaders strips headers from the upstream response before Caddy adds its own. This is needed when the upstream sets restrictive headers that conflict with your embedding requirements.
Set iframeOrigin: null to explicitly block iframe embedding.
Ingress Tagging
Tag routes with identifiers for monitoring:
network:
services:
api:
dial: "127.0.0.1:8080"
ingressTag: "production-api" Caddy adds the tag as an X-ASD-Ingress response header. The default value comes from the ASD_INGRESS_TAG environment variable, or "local-caddy" if unset. Set ingressTag: null to disable the header.
Macro-Driven Configuration
Service definitions support ${{ }} template expressions that resolve at configuration time.
Environment Variables
network:
services:
app:
dial: "127.0.0.1:${{ env.APP_PORT }}"
host: "${{ env.APP_HOST }}" Tunnel Credential Macros
These macros read from the credential registry and work across all authentication types (SSH keys, tokens, ephemeral):
| Macro | Returns | Example |
|---|---|---|
${{ macro.tunnelHost("app") }} | Full tunnel hostname | app-fkmc.eu1.tn.asd.engineer |
${{ macro.tunnelClientId() }} | Short client ID | fkmc |
${{ macro.tunnelEndpoint() }} | Server FQDN | eu1.tn.asd.engineer |
${{ macro.exposedOrigin("app") }} | Full HTTPS origin URL | https://app-fkmc.eu1.tn.asd.engineer |
${{ macro.exposedOriginWithAuth("app") }} | Origin with embedded credentials | https://user:pass@app-fkmc.eu1.tn.asd.engineer |
When no tunnel credentials exist (fresh install), these macros resolve to empty strings. Empty hosts are filtered out, so routes fall back to localhost only.
Utility Macros
| Macro | Description |
|---|---|
${{ macro.getRandomPort() }} | Allocate a free port |
${{ macro.getRandomString(length=32) }} | Random alphanumeric string |
${{ macro.bcrypt(password="...", cost=12) }} | Generate a bcrypt hash |
${{ core.isDockerAvailable() }} | Returns true if Docker is running |
Bcrypt in Auth Config
Use the bcrypt macro to hash passwords in your asd.yaml:
network:
caddy:
basic_auth:
enabled: true
password: "${{ macro.bcrypt(password='my-secret', cost=12) }}" Built-in Service Paths
ASD’s built-in services use the /asde/ prefix to avoid collisions with your routes:
| Service | Caddy Path |
|---|---|
| Web Terminal (ttyd) | /asde/ttyd/ |
| VS Code Server | /asde/codeserver/ |
| Database UI (DbGate) | /asde/dbgate/ |
| Network Inspector | /asde/mitmproxy/ |
These paths are registered automatically when you start the respective service.
Health Checks
ASD runs a multi-level health check cascade for each service. The asd net TUI shows green/red indicators based on these results. Checks run in order and stop at the first failure:
- Tunnel check — sends an HTTP request to the public tunnel URL. The
X-Asd-Tunnelresponse header distinguishes “server alive, no client” (not-found) from “request forwarded” (forwarded). - HTTP check — sends an HTTP request to the Caddy route (e.g.,
http://myapp.localhost). A 200 or 401 response means the service is reachable. - TCP check — attempts a TCP connection to the service port. Confirms the process is listening.
- Process check — verifies the background process is still running (PID alive).
Press Ctrl+R in the asd net TUI to re-run all health checks, or use asd net refresh from the command line.
Related Guides
- Configuration Reference (asd.yaml) — full service configuration
- Security and Authentication — all security levels
- Access Patterns — three ways to reach your services