1. The problem I’m having:
I have partners sending API calls to our API.
Sometimes we want to debug these calls on our local running computer, but we would like the system to be flexible.
The solution we imagine is to have the partner continue calling our back-end API URL but doing some reverse proxy stuff.
We’re using ngrok to open tunnels between our machines to a specific public static URL (NGROK_URL).
We discovered that when no tunnel are connected to ngrok, the NGROK_URL returns a 404 with a specific header Ngrok-Error-Code: ERR_NGROK_3200
Our caddy webserver is in a container, currently redirecting all the requests to an other container (called backend).
What we had in mind:
instead of redirecting everything to backed, try redirecting to NGROK_URL. If we receive an error with the above header, then we redirect the request to backend.
Ideally, we don’t want to redirect ALL endpoints, so there is a matching condition on this specific reverse_proxy.
The problem, it does not redirect to backend on NGROK error. The handle errors seems non functioning
I could try following this path, but it seems a bit over complicated: Conditional redirect with a reverse proxy
2. Error messages and/or full log output:
No error in itself, but here is an entry of access.log
{"level":"debug","ts":1729114419.3303804,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"NGROK_URL:443","duration":0.043435274,"request":{"remote_ip":"xxx.xxx.xxx.xxx","remote_port":"36458","client_ip":"xxx.xxx.xxx.xxx","proto":"HTTP/2.0","method":"GET","host":"NGROK_URL","uri":"/docs","headers":{"Upgrade-Insecure-Requests":["1"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Sec-Ch-Ua-Mobile":["?0"],"Priority":["u=0, i"],"Sec-Fetch-Site":["none"],"Cookie":["REDACTED"],"Cache-Control":["max-age=0"],"Sec-Ch-Ua-Platform":["\"Linux\""],"Sec-Fetch-Dest":["document"],"Sec-Ch-Ua":["\"Chromium\";v=\"129\", \"Not=A?Brand\";v=\"8\""],"User-Agent":["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"],"X-Forwarded-For":["109.190.181.174"],"Ngrok-Skip-Browser-Warning":["true"],"Accept-Encoding":["gzip, deflate, br, zstd"],"X-Forwarded-Proto":["https"],"Sec-Fetch-Mode":["navigate"],"Accept-Language":["fr,en;q=0.9"],"Sec-Fetch-User":["?1"],"X-Forwarded-Host":["PUBLIC_BACK_END_FQDN"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"PUBLIC_BACK_END_FQDN"}},"headers":{"Date":["Wed, 16 Oct 2024 21:33:39 GMT"],"Content-Type":["text/html"],"Ngrok-Error-Code":["ERR_NGROK_3200"],"Referrer-Policy":["no-referrer"]},"status":404}
3. Caddy version:
latest caddy docker image
4. How I installed and ran Caddy:
a. System environment:
docker
b. Command:
docker compose up
c. Service/unit/compose file:
services:
caddy:
image: caddy:2-alpine
container_name: caddy
restart: always
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy_data:/data
- ./caddy_config:/config
- ./caddy_logs:/var/log/caddy
ports:
- "80:80"
- "443:443"
- "443:443/udp"
backend:
build:
context: .
target: 'app_image'
env_file: app_config
container_name: backend
logging:
driver: local
options:
max-size: "100m"
max-file: "10"
restart: always
volumes:
- './app/:/app'
environment:
- LOG_LEVEL=info
d. My complete Caddy config:
{
admin off
email contact@customsbridge.fr
log {
output file /var/log/caddy/access.log {
roll_size 200mb
roll_keep 10
roll_keep_for 365d
}
level DEBUG # Set the log level to DEBUG for more verbose output
}
}
# The caddy webserver will be listening on urls defined in env variable $SITE_ADDRESS
# This env variable is defined in the dockerfile at the root of the front code project, or defined in the init.sh script in deploy folder
{PUBLIC_BACK_END_FQDN} {
# Match specific paths and proxy them to an external service
@external_paths {
path /docs* /redoc* /other_endpoint*
}
# Reverse proxy to external service and set necessary headers
reverse_proxy @external_paths https://NGROK_URL {
header_up Host NGROK_URL # Rewrite the Host header for the upstream
header_up ngrok-skip-browser-warning true # Add custom header to skip ngrok browser warning
}
handle_errors {
@ngrok_error {
header Ngrok-Error-Code ERR_NGROK_3200 # Match Ngrok error in response header
}
reverse_proxy backend:80 #redirecting to backend in case no local dev computer is connected to Ngrok tunnel
}
# Default reverse proxy to backend for other routes
reverse_proxy backend:80
tls {
protocols tls1.2 tls1.3
ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
}
encode zstd gzip
header {
Strict-Transport-Security max-age=63072000;
X-Content-Type-Options nosniff
X-Frame-Options DENY
Content-Security-Policy "default-src 'self';
script-src 'report-sample' 'unsafe-inline' 'self';
style-src 'report-sample' 'unsafe-inline' 'self';
object-src 'none';
base-uri 'self';
connect-src 'self';
font-src 'self';
frame-src 'self';
img-src 'self' data:;
manifest-src 'self';
media-src 'self';
worker-src blob:;"
X-XSS-Protection "1; mode=block"
Permissions-Policy "accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), xr-spatial-tracking=()"
Referrer-Policy no-referrer
-Server
}
}