A beginner-friendly guide to reverse proxies, why we chose Caddy over Nginx, and how its automatic HTTPS and on-demand TLS let a small team handle infinite custom domains without a dedicated infrastructure engineer.

A Gentle Introduction for Junior Developers

When you first start building web applications, it is easy to believe that the world consists only of the code you write and the browser where it runs. You write a backend in Node.js, you build a frontend in React or Next.js, and you assume that traffic simply flows from the user to your application and back again like water through a clean pipe. In reality, the journey of a single HTTP request is far more complex. Before that request ever reaches your carefully crafted API endpoint, it must pass through a gatekeeper. That gatekeeper is called a reverse proxy, and the one we have chosen to stand at the gates of our infrastructure is a remarkable piece of software called Caddy.

This chapter is written for you, the junior developer who may have heard the word “proxy” whispered in passing but has never been given the chance to understand what it actually means or why it matters. We will walk through the problem that a reverse proxy solves, explain why we selected Caddy over its more famous competitors, and show you exactly how we have configured it to serve our platform. By the end, you will not only understand our setup; you will appreciate the elegance of a tool that makes the hard parts of web infrastructure feel almost effortless.


The Invisible Problem: What Happens Before a Request Reaches Your Code

Imagine a visitor types https://example.com into their browser. Their computer does not know where our servers live. It asks the Domain Name System, the great phone book of the internet, for the IP address associated with that name. Once it has the address, their browser opens a connection and sends an HTTP request across the network. But here is the subtle detail that many developers overlook: our infrastructure is not one application. It is a constellation of services. We have a database, a cache service, a backend API, and a frontend application. Each of these services runs inside its own container, isolated and specialized, speaking to one another across an internal network.

If we simply exposed our backend and frontend directly to the internet, we would face a host of problems. We would need to manage multiple public ports, configure SSL certificates manually for each service, and handle the routing logic that decides whether, for example, a request for /api/health should go to the backend or whether a request for the homepage should go to the frontend. Worse still, every service exposed to the public internet becomes a surface for attack. A reverse proxy solves all of this by becoming the single point of contact. It sits at the edge of our network, listens on the standard HTTP ports, and decides where each request should go. To the outside world, our platform looks like one cohesive server. Inside, the proxy quietly distributes traffic to the right destination.


Why Caddy, and Not Nginx or Apache

If you have spent any time reading about web servers, you have almost certainly encountered Nginx. It is powerful, battle-tested, and used by some of the largest websites on the planet. Apache, its older cousin, has been serving web pages since the 1990s. Both are excellent tools in the right hands. But excellence in production does not always mean ease in operation, and for a small team building a multi-tenant platform, ease matters enormously.

Nginx requires you to write configuration files that can feel more like programming in a domain-specific language than declaring intent. You must manually configure SSL certificate generation using external tools like Certbot, set up cron jobs for renewal, and manage certificate files on disk. If you support custom domains, as we do for our artist portfolios, the complexity multiplies. Each new domain requires its own certificate logic, its own server block, and careful orchestration to avoid downtime.

Caddy was built with a different philosophy. Its configuration is declarative and readable. More importantly, it handles Transport Layer Security automatically. When Caddy sees a new domain, it reaches out to Let’s Encrypt or ZeroSSL, obtains a certificate, and begins serving HTTPS within seconds. It renews certificates before they expire without any human intervention. For our platform, which allows artists to point their own custom domains to their portfolios, this is not merely convenient. It is transformative. We do not maintain a spreadsheet of expiration dates. We do not wake up at midnight to renew a certificate. Caddy simply handles it, as if HTTPS were the default state of the web, which, of course, it should be.


The Architecture of Our Stack

To understand how Caddy fits into our world, you must first see the whole picture. We run our entire platform inside Docker Compose. This means every service is defined as a container, and they communicate with one another over a private virtual network.

Notice something important about the backend and the web application. Neither of them exposes a port to the host machine. They listen on internal ports, but only inside the Docker network. They are invisible to the outside world. Caddy, by contrast, is the only service that maps ports 80 and 443 from the host into its container. Port 80 is the standard for HTTP, and port 443 is the standard for HTTPS. This means every single packet of traffic from the internet arrives first at Caddy. Nothing else is directly accessible. This is not merely a convenience; it is a security posture. By reducing our external surface area to a single hardened entry point, we make our platform significantly harder to attack.


Reading the Caddyfile: Configuration as Conversation

Caddy is configured through a file called, simply, the Caddyfile. It lives in our repository at, and it is mounted into the Caddy container when Docker Compose starts. If you open it, you will notice that it does not look like a traditional server configuration. There are no curly braces nested ten levels deep, no cryptic variable substitutions, and no boilerplate that exists only because the parser demands it. The Caddyfile reads almost like a conversation.

At the very top of the file, we define a global option block. Inside it, we configure on-demand TLS. This is the feature that makes custom domains possible. When a visitor arrives at a domain we have never seen before, Caddy pauses. Before it issues a certificate, it asks our backend whether that domain is legitimate through a validation endpoint. Only if our backend responds positively does Caddy proceed with certificate generation. This prevents abuse. Without this check, an attacker could point any domain at our server and force Caddy to issue certificates endlessly. The validation endpoint acts as a guardrail, ensuring we only serve domains that belong to real tenants in our system.

We also disable the admin API in production. Caddy exposes a local administrative interface that is useful for debugging, but in a production environment it is safer to turn it off. Finally, we provide an email address for the ACME protocol, which is the standard by which Caddy communicates with certificate authorities like Let’s Encrypt. This email receives notifications if something goes wrong with a certificate request.


How Traffic Flows Through Our Proxy

The heart of the Caddyfile is the site block that begins with :80, :443. This tells Caddy to listen on both the HTTP and HTTPS ports for every domain that reaches the server. Inside this block, we define a series of route handlers using the handle directive. Caddy evaluates these in the order they appear, and the first matching handler wins.

The first handler is our health check. When a monitoring service or a load balancer wants to know if Caddy itself is alive, it requests a health endpoint, and Caddy responds with a plain HTTP 200. This is a small detail, but it is critical for automated deployments. Our deployment scripts use this endpoint to confirm that Caddy has started successfully before proceeding.

Next come the routes that require special treatment. We have a handful of authentication endpoints that must be proxied directly to our backend. The reason is subtle. These endpoints handle sensitive operations that our web application’s own API layer does not manage. By routing them directly to the backend, we bypass the web server and reduce the attack surface for session-related logic. Similarly, our admin endpoints and tenant creation routes go straight to the web application, because those are implemented as server-side API routes rather than backend services.

After the special cases, we have a catch-all for everything under /api. Any request that begins with /api and has not already been handled by a more specific rule is forwarded to the backend. This is where the majority of our platform logic lives.

Then we handle static uploads. When users upload files, our backend stores them and serves them from a dedicated path. Caddy forwards these requests to the backend as well, ensuring that media is served through the same trusted pipeline.

Finally, we reach the default handler. If a request has not matched any of the previous rules, it falls through to this catch-all, which proxies everything to the Next.js web application. This is where the artist portfolios, the studio dashboard, and the landing pages are rendered. Because this handler has no path prefix, it acts as the ultimate fallback. Every human visitor browsing a portfolio will have their request handled here.


On-Demand TLS and the Dream of Infinite Custom Domains

One of the defining features of our platform is that every artist can have their own subdomain, and eventually their own custom domain, pointing to their portfolio. In a traditional setup, supporting thousands of custom domains would be a nightmare. You would need to generate certificates in advance, store them on disk, monitor expiration dates, and handle renewal failures. With Caddy’s on-demand TLS, this nightmare vanishes.

When a visitor types a custom subdomain into their browser, the DNS record points to our server. Caddy receives the connection, notices it does not yet have a certificate for that domain, and triggers the on-demand logic. It consults our backend validation endpoint. Our backend checks whether the domain belongs to a registered tenant. If yes, Caddy requests a certificate from Let’s Encrypt, installs it, and serves the site over HTTPS. The entire process takes seconds, and the visitor sees nothing but a secure lock icon in their browser. If the user later points their own custom domain to our server, the exact same process repeats. We do not edit the Caddyfile. We do not restart the server. The infrastructure scales to an arbitrary number of domains without human intervention.

This is the kind of operational simplicity that allows a small engineering team to behave like a much larger one. We do not need a dedicated infrastructure engineer to manage certificates. We do not maintain runbooks for certificate renewal. Caddy absorbs that complexity and presents us with a clean, reliable abstraction.


Security at the Edge

A reverse proxy is not merely a traffic cop. It is also a security guard. Every request that passes through Caddy can be inspected, filtered, and hardened before it ever reaches our application code. In our Caddyfile, we attach a set of security headers to every response. The X-Frame-Options header set to SAMEORIGIN prevents other websites from embedding our pages in invisible iframes, a technique commonly used in clickjacking attacks. The X-Content-Type-Options header set to nosniff stops browsers from guessing the content type of a response, which prevents certain classes of cross-site scripting attacks. The Referrer-Policy header ensures that when a visitor clicks a link away from our site, the full URL of the originating page is not leaked to the destination.

These headers are small details, but security is composed of small details. By centralizing them in the reverse proxy, we guarantee that every service behind Caddy benefits from the same protections. We do not need to remember to set these headers in our Next.js application, in our backend API, and in any future service we might add. Caddy applies them universally.


The Simplicity of Operation

Perhaps the greatest testament to Caddy’s design is how little we think about it in our daily work. Our deployment script does not contain a single line of code for certificate management. Our production deployment guide does not include a section titled “How to Renew SSL Certificates.” When we add a new tenant with a custom domain, we do not open an SSH session to the server and edit configuration files. We simply mark the domain as valid in our database, and Caddy handles the rest.

This invisibility is the highest compliment you can pay to infrastructure software. It means the tool has succeeded so thoroughly at its job that it fades into the background, leaving you free to focus on the product you are building. For a junior developer joining our team, this is especially valuable. You do not need to spend your first week learning the arcane syntax of Nginx configuration or the ritual dance of Certbot renewal. You can read our Caddyfile in five minutes, understand exactly how traffic flows through our system, and move on to the far more interesting work of building features for artists.


A Closing Thought

The tools we choose shape the way we think about problems. When a reverse proxy requires constant manual attention, infrastructure feels like a burden. When it disappears into the background and simply works, infrastructure becomes a foundation upon which you can build with confidence. Caddy is not merely a web server. It is a statement of philosophy: that security should be automatic, that configuration should be readable, and that the complex machinery of the modern web should not be dumped onto the shoulders of the developers who use it.

We use Caddy because it allows us to offer every artist on our platform a secure, custom domain without maintaining a certificate spreadsheet. We use it because its configuration reads like intent rather than code. We use it because, on the day when traffic to our platform unexpectedly doubles, we want to be thinking about how to scale our database, not how to renew an SSL certificate that expired at midnight. And if you are a junior developer reading this, we hope you will carry this lesson with you: the best tools are not always the most famous ones. They are the ones that remove obstacles from your path so that you can focus on what truly matters, which is building something wonderful.

Welcome to The infinite monkey theorem

Somewhere a monkey just typed Shakespeare in TypeScript. Be the first to read the masterpieces (and the hilarious misfires) landing on the blog.

Subscribe to The infinite monkey theorem

We fling fresh posts—no banana peels attached—straight to your inbox.