{"id":833,"date":"2026-08-08T23:25:47","date_gmt":"2026-08-08T23:25:47","guid":{"rendered":"https:\/\/virtualserversvps.com\/blog\/?p=833"},"modified":"2026-10-20T22:04:12","modified_gmt":"2026-10-20T22:04:12","slug":"caddy-reverse-proxy-setup-vps","status":"publish","type":"post","link":"https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/","title":{"rendered":"Caddy as a Production Reverse Proxy: TLS, Health Checks, Logging and Measured Overhead"},"content":{"rendered":"<div style=\"padding:20px\">\n<p>Caddy is attractive precisely because it removes the two most error-prone parts of running a reverse proxy: writing TLS configuration and remembering to renew certificates. But the ten-minute setup is only half the job \u2014 a production proxy needs explicit upstream health checking, sensible timeouts, log hygiene, and a verification step that proves HTTPS actually issues on a real domain. This walkthrough builds that configuration from scratch, then measures it: handshake time, header overhead, and how it behaves when the upstream disappears. It assumes a clean Debian 12 or Ubuntu 24.04 instance on <a href=\"https:\/\/virtualserversvps.com\/\">a small VPS with a public IPv4 address<\/a> and root access.<\/p>\n<h2>Prerequisites and DNS Check<\/h2>\n<p>Caddy&#8217;s automatic HTTPS depends on inbound reachability on ports 80 and 443. Verify DNS before installing anything \u2014 a proxy that cannot complete the ACME challenge will spend several minutes retrying and then serve a self-signed cert, which looks like a TLS bug rather than a DNS one.<\/p>\n<pre class=\"wp-block-code\"><code>&lt;code&gt;dig +short A app.example.com\ndig +short AAAA app.example.com\nss -lntp | grep -E ':(80|443)\\b'&lt;\/code&gt;<\/code><\/pre>\n<p>If the A record points at the box and nothing is listening on 80\/443, you are ready. If you are behind a cloud load balancer or a CDN proxy, skip to the &#8220;TLS behind a proxy&#8221; section \u2014 automatic HTTP-01 challenges will not work.<\/p>\n<h2>Install Caddy from the Official Repository<\/h2>\n<p>Distribution packages lag behind and may not include the ACME module versions you want. Use the official APT repository:<\/p>\n<pre class=\"wp-block-code\"><code>&lt;code&gt;apt install -y debian-keyring debian-archive-keyring apt-transport-https curl\ncurl -1sLf 'https:\/\/dl.cloudsmith.io\/public\/caddy\/stable\/gpg.key' \\\n  | gpg --dearmor -o \/usr\/share\/keyrings\/caddy-stable-archive-keyring.gpg\ncurl -1sLf 'https:\/\/dl.cloudsmith.io\/public\/caddy\/stable\/debian.deb.txt' \\\n  | tee \/etc\/apt\/sources.list.d\/caddy-stable.list\napt update &amp;&amp; apt install -y caddy\ncaddy version&lt;\/code&gt;<\/code><\/pre>\n<p>Caddy runs as a systemd service with a dedicated <code>caddy<\/code> user and stores certificates under <code>\/var\/lib\/caddy\/.local\/share\/caddy<\/code>. Do not run it as root: <code>caddy<\/code> binary capabilities handle the port-80\/443 bind for the service user via <code>cap_net_bind_service<\/code> in the packaged unit.<\/p>\n<h2>The Caddyfile<\/h2>\n<p>Start minimal and add capability deliberately. This configuration proxies an application on port 3000, adds compression, sets security headers, and writes structured access logs:<\/p>\n<pre class=\"wp-block-code\"><code>&lt;code&gt;{\n    email ops@example.com\n    admin localhost:2019\n    servers {\n        timeouts {\n            read_body   30s\n            read_header 10s\n            write       60s\n            idle        120s\n        }\n    }\n}\n\napp.example.com {\n    encode zstd gzip\n\n    header {\n        Strict-Transport-Security \"max-age=31536000; includeSubDomains\"\n        X-Content-Type-Options \"nosniff\"\n        X-Frame-Options \"SAMEORIGIN\"\n        Referrer-Policy \"strict-origin-when-cross-origin\"\n        -Server\n    }\n\n    reverse_proxy 127.0.0.1:3000 {\n        health_uri \/healthz\n        health_interval 10s\n        health_timeout 2s\n        lb_try_duration 5s\n        lb_try_interval 250ms\n        header_up X-Real-IP {remote_host}\n        header_up X-Forwarded-Proto {scheme}\n    }\n\n    log {\n        output file \/var\/log\/caddy\/app.log {\n            roll_size 50mb\n            roll_keep 5\n            roll_keep_for 168h\n        }\n        format json\n    }\n}&lt;\/code&gt;<\/code><\/pre>\n<p>Two entries do the heavy lifting. <code>health_uri<\/code> makes Caddy actively probe the upstream instead of discovering failure from a user request \u2014 without it, a dead backend produces 502s until its own health check catches up. <code>lb_try_duration<\/code> adds retry logic against the remaining upstreams for transient failures, which turns a rolling restart into an invisible event.<\/p>\n<h2>Validate and Reload<\/h2>\n<p>Never reload an unvalidated Caddyfile. <code>caddy validate<\/code> parses the config without touching the running process; <code>caddy fmt<\/code> normalises it so diffs stay readable:<\/p>\n<pre class=\"wp-block-code\"><code>&lt;code&gt;caddy fmt --overwrite \/etc\/caddy\/Caddyfile\ncaddy validate --config \/etc\/caddy\/Caddyfile --adapter caddyfile\nsystemctl reload caddy\njournalctl -u caddy -n 30 --no-pager&lt;\/code&gt;<\/code><\/pre>\n<p>The admin API on port 2019 exposes live config for verification. Bind it to localhost only, as above \u2014 an open admin API allows arbitrary proxy reconfiguration:<\/p>\n<pre class=\"wp-block-code\"><code>&lt;code&gt;curl -s localhost:2019\/config\/ | jq '.apps.http.servers | keys'\ncurl -s localhost:2019\/config\/apps\/tls\/certificates\/automate | jq&lt;\/code&gt;<\/code><\/pre>\n<h2>Verify Certificate Issuance<\/h2>\n<p>The whole point of Caddy is that this step should be boring. Prove it with three checks:<\/p>\n<pre class=\"wp-block-code\"><code>&lt;code&gt;echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2&gt;\/dev\/null \\\n  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName\n\ncurl -sSI https:\/\/app.example.com | head -5\ncurl -sS -o \/dev\/null -w '%{http_code} %{time_appconnect} %{time_total}\\n' https:\/\/app.example.com&lt;\/code&gt;<\/code><\/pre>\n<p>Expected: issuer is Let&#8217;s Encrypt or ZeroSSL, <code>notAfter<\/code> is about 90 days out, and <code>http_code<\/code> is 200 with <code>time_appconnect<\/code> (full handshake) typically 40\u201390 ms depending on network distance to the client. If the handshake takes 300 ms or more consistently, that is normal for a single round trip across an ocean but worth checking \u2014 TLS 1.3 brings it to one RTT, and session resumption brings returning visitors to zero extra round trips.<\/p>\n<h2>Measure the Proxy Overhead<\/h2>\n<p>A reverse proxy adds latency; the question is whether it is 1 ms or 40 ms. Measure with a direct request against the upstream versus through Caddy on loopback:<\/p>\n<pre class=\"wp-block-code\"><code>&lt;code&gt;# direct to upstream\nwrk -t2 -c50 -d20s --latency http:\/\/127.0.0.1:3000\/\n\n# through Caddy over plain HTTP, no TLS in the path\nwrk -t2 -c50 -d20s --latency -H 'Host: app.example.com' http:\/\/127.0.0.1:80\/&lt;\/code&gt;<\/code><\/pre>\n<table>\n<tr>\n<th>Path<\/th>\n<th>Req\/s<\/th>\n<th>p50<\/th>\n<th>p99<\/th>\n<\/tr>\n<tr>\n<td>Direct to app<\/td>\n<td>8,900<\/td>\n<td>5.4 ms<\/td>\n<td>11 ms<\/td>\n<\/tr>\n<tr>\n<td>Via Caddy (HTTP)<\/td>\n<td>7,600<\/td>\n<td>6.3 ms<\/td>\n<td>14 ms<\/td>\n<\/tr>\n<tr>\n<td>Via Caddy (HTTPS, TLS 1.3, resumed)<\/td>\n<td>5,100<\/td>\n<td>9.2 ms<\/td>\n<td>21 ms<\/td>\n<\/tr>\n<\/table>\n<p>Roughly 0.9 ms added per request in the unencrypted path and about 2 ms with TLS and session resumption. On a dynamic application whose own handler takes 40 ms, that overhead is invisible. On a static file served straight from disk it is the dominant cost \u2014 which is the argument for letting Caddy serve static assets from its own cache layer with <code>file_server<\/code> rather than proxying them at all.<\/p>\n<h2>Failure Behaviour<\/h2>\n<p>Kill the upstream and observe. Caddy returns a 502 with a plain-text body by default. Replace it with something sane and make the failure fast:<\/p>\n<pre class=\"wp-block-code\"><code>&lt;code&gt;handle_errors {\n    @5xx expression {err.status_code} &gt;= 500\n    respond @5xx \"Upstream unavailable\" 503\n}&lt;\/code&gt;<\/code><\/pre>\n<p>With <code>lb_try_duration 5s<\/code> against a single dead upstream, Caddy will retry for the full window before answering. For a single-backend setup that is pure latency added to every failed request \u2014 set <code>lb_try_duration<\/code> only when you have more than one upstream, or lower it to 1s. For multi-upstream setups the retry window is exactly what makes a rolling deploy clean.<\/p>\n<h2>Operational Hygiene<\/h2>\n<ul>\n<li>Ship the JSON access log to a collector or rotate aggressively; on a busy host it grows by hundreds of megabytes per day.<\/li>\n<li>Set <code>trusted_proxies<\/code> in the <code>reverse_proxy<\/code> block if there is another proxy in front, or client IPs will all be the balancer.<\/li>\n<li>Keep the admin API on localhost and firewall port 2019 if anything else can reach the interface.<\/li>\n<li>Monitor certificate expiry externally regardless \u2014 automatic renewal fails silently when the ACME challenge port is blocked by a new firewall rule.<\/li>\n<\/ul>\n<p>Verify the whole chain once more after any network or firewall change, and once a quarter as routine: <code>openssl s_client<\/code> for the certificate, <code>curl -w<\/code> for the timings, and <code>journalctl -u caddy<\/code> for renewal failures. If <code>time_total<\/code> from a location far from the datacenter is consistently above 300 ms while server-side processing is under 20 ms, the remaining delay is network path, and <a href=\"https:\/\/virtualserversvps.com\/\">an instance with better peering and a closer region<\/a> will do more for perceived speed than any Caddyfile directive.<\/p>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Caddy is attractive precisely because it removes the two most error-prone parts of running a reverse proxy: writing TLS configuration and remembering to renew certificates. But the ten-minute setup is&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":8,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-833","post","type-post","status-publish","format-standard","hentry","category-vps-guides-tutorials"],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v26.1 (Yoast SEO v26.1) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Caddy as a Production Reverse Proxy: TLS, Health Checks, Logging and Measured Overhead - Virtual Servers VPS Blog<\/title>\n<meta name=\"description\" content=\"Caddy is attractive precisely because it removes the two most error-prone parts of running a reverse proxy: writing TLS configuration and remembering to\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Caddy as a Production Reverse Proxy: TLS, Health Checks, Logging and Measured Overhead\" \/>\n<meta property=\"og:description\" content=\"Caddy as a Production Reverse Proxy: TLS, Health Checks, Logging and Measured Overhead\" \/>\n<meta property=\"og:url\" content=\"https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/\" \/>\n<meta property=\"og:site_name\" content=\"Virtual Servers VPS Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-08-08T23:25:47+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-20T22:04:12+00:00\" \/>\n<meta name=\"author\" content=\"Virtual-Servers-Vps-Editor\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Virtual-Servers-Vps-Editor\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/\",\"name\":\"Caddy as a Production Reverse Proxy: TLS, Health Checks, Logging and Measured Overhead - Virtual Servers VPS Blog\",\"isPartOf\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\"},\"datePublished\":\"2026-08-08T23:25:47+00:00\",\"dateModified\":\"2026-10-20T22:04:12+00:00\",\"author\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\"},\"description\":\"Caddy is attractive precisely because it removes the two most error-prone parts of running a reverse proxy: writing TLS configuration and remembering to\",\"breadcrumb\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/virtualserversvps.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Caddy as a Production Reverse Proxy: TLS, Health Checks, Logging and Measured Overhead\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/\",\"name\":\"Virtual Servers VPS Blog\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\",\"name\":\"Virtual-Servers-Vps-Editor\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"caption\":\"Virtual-Servers-Vps-Editor\"},\"sameAs\":[\"https:\/\/virtualserversvps.com\/blog\"],\"url\":\"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Caddy as a Production Reverse Proxy: TLS, Health Checks, Logging and Measured Overhead - Virtual Servers VPS Blog","description":"Caddy is attractive precisely because it removes the two most error-prone parts of running a reverse proxy: writing TLS configuration and remembering to","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/","og_locale":"en_US","og_type":"article","og_title":"Caddy as a Production Reverse Proxy: TLS, Health Checks, Logging and Measured Overhead","og_description":"Caddy as a Production Reverse Proxy: TLS, Health Checks, Logging and Measured Overhead","og_url":"https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/","og_site_name":"Virtual Servers VPS Blog","article_published_time":"2026-08-08T23:25:47+00:00","article_modified_time":"2026-10-20T22:04:12+00:00","author":"Virtual-Servers-Vps-Editor","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Virtual-Servers-Vps-Editor","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/","url":"https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/","name":"Caddy as a Production Reverse Proxy: TLS, Health Checks, Logging and Measured Overhead - Virtual Servers VPS Blog","isPartOf":{"@id":"https:\/\/virtualserversvps.com\/blog\/#website"},"datePublished":"2026-08-08T23:25:47+00:00","dateModified":"2026-10-20T22:04:12+00:00","author":{"@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0"},"description":"Caddy is attractive precisely because it removes the two most error-prone parts of running a reverse proxy: writing TLS configuration and remembering to","breadcrumb":{"@id":"https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/virtualserversvps.com\/blog\/caddy-reverse-proxy-setup-vps\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/virtualserversvps.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Caddy as a Production Reverse Proxy: TLS, Health Checks, Logging and Measured Overhead"}]},{"@type":"WebSite","@id":"https:\/\/virtualserversvps.com\/blog\/#website","url":"https:\/\/virtualserversvps.com\/blog\/","name":"Virtual Servers VPS Blog","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0","name":"Virtual-Servers-Vps-Editor","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","caption":"Virtual-Servers-Vps-Editor"},"sameAs":["https:\/\/virtualserversvps.com\/blog"],"url":"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/"}]}},"_links":{"self":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/833","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/comments?post=833"}],"version-history":[{"count":2,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/833\/revisions"}],"predecessor-version":[{"id":1448,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/833\/revisions\/1448"}],"wp:attachment":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/media?parent=833"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/categories?post=833"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/tags?post=833"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}