---
title: "10 — Web Servers — Nginx, Apache, Caddy, and What They Actually Do"
uid: web-servers
tags: ["iis", "nginx", "roadmap:backend", "caddy", "apache", "web-server", "reverse-proxy"]
excerpt: "A web server is the front door — terminating connections, serving static files, reverse-proxying to your app. The app is a separate process with a separate job."
date: 2026-08-13T03:28:26+0000
source: https://www.aveshina.my.id/en/blog/web-servers
---

"The application itself" is what I used to call the web server, and the conflation cost me hours reading config files. The separation that fixed it: **a web server is the front door that terminates connections, serves static files, and reverse-proxies to my application; the application is the code that produces dynamic responses.** [1] They're different processes with different jobs, and conflating them blocks understanding both.

The frame that helped is what the web server does that my app doesn't want to. Terminating TLS, serving static files (images, CSS, JS) straight from disk, handling thousands of idle connections, load-balancing across multiple app instances — these are things a dedicated, battle-tested server process does better than my application code. The web server sits in front, handles the parts that are generic, and forwards the dynamic requests to my app over a local socket. This separation is the whole shape of a production deployment.

```figure
<svg viewBox="0 0 740 280" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="A web server box in the middle labelled 'listens on :80 and :443'. Clients arrive from the left as arrows. Inside the box are three lanes: 'terminate TLS' (yellow), 'serve static files' (green), 'reverse-proxy to app' (blue). The reverse-proxy lane forwards to an app process on the right labelled ':3000'. Four flavor pills (Nginx, Apache, Caddy, IIS) sit below the server box.">
  <defs>
    <marker id="wsarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- clients -->
    <text x="40" y="80" font-size="11" font-weight="700" fill="#64748b">clients</text>
    <line x1="50" y1="100" x2="180" y2="100" stroke="#64748b" stroke-width="2" marker-end="url(#wsarrow)"/>
    <line x1="50" y1="130" x2="180" y2="130" stroke="#64748b" stroke-width="2" marker-end="url(#wsarrow)"/>
    <line x1="50" y1="160" x2="180" y2="160" stroke="#64748b" stroke-width="2" marker-end="url(#wsarrow)"/>

    <!-- web server box -->
    <rect x="190" y="60" width="320" height="180" rx="12" fill="#e0e7ff" stroke="#6366f1" stroke-width="2"/>
    <text x="350" y="84" font-size="13" font-weight="700" fill="#1e1b4b" text-anchor="middle">Web server — listens :80 / :443</text>

    <!-- three lanes -->
    <rect x="210" y="100" width="130" height="36" rx="6" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="275" y="123" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">terminate TLS</text>
    <rect x="210" y="142" width="130" height="36" rx="6" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="275" y="165" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">serve static files</text>
    <rect x="210" y="184" width="130" height="36" rx="6" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="275" y="207" font-size="11" font-weight="700" fill="#500724" text-anchor="middle">reverse-proxy</text>

    <!-- forward to app -->
    <line x1="340" y1="202" x2="548" y2="202" stroke="#db2777" stroke-width="2" marker-end="url(#wsarrow)"/>
    <text x="445" y="195" font-size="10" fill="#500724" text-anchor="middle">dynamic requests</text>

    <!-- app -->
    <rect x="560" y="175" width="130" height="56" rx="10" fill="#dcfce7" stroke="#16a34a" stroke-width="2"/>
    <text x="625" y="198" font-size="13" font-weight="700" fill="#052e16" text-anchor="middle">your app</text>
    <text x="625" y="216" font-size="10" fill="#052e16" text-anchor="middle">process on :3000</text>

    <!-- flavors below -->
    <g>
      <rect x="190" y="255" width="70" height="20" rx="10" fill="#ffffff" stroke="#6366f1" stroke-width="1"/>
      <text x="225" y="269" font-size="10" font-weight="700" fill="#1e1b4b" text-anchor="middle">Nginx</text>
      <rect x="270" y="255" width="70" height="20" rx="10" fill="#ffffff" stroke="#6366f1" stroke-width="1"/>
      <text x="305" y="269" font-size="10" font-weight="700" fill="#1e1b4b" text-anchor="middle">Apache</text>
      <rect x="350" y="255" width="70" height="20" rx="10" fill="#ffffff" stroke="#6366f1" stroke-width="1"/>
      <text x="385" y="269" font-size="10" font-weight="700" fill="#1e1b4b" text-anchor="middle">Caddy</text>
      <rect x="430" y="255" width="70" height="20" rx="10" fill="#ffffff" stroke="#6366f1" stroke-width="1"/>
      <text x="465" y="269" font-size="10" font-weight="700" fill="#1e1b4b" text-anchor="middle">IIS</text>
    </g>
  </g>
</svg>
```

## The job: terminate, serve, proxy

Three responsibilities define a web server in front of an application:

- **Terminate TLS.** The HTTPS connection from the client ends at the web server, which holds the TLS certificate, decrypts the request, and forwards plaintext to the app over the loopback interface. The app never has to know about certificates. This is the separation I flagged in the internet-fundamentals notes.
- **Serve static files.** Images, CSS, JS, fonts — files that don't change per request. The web server reads them straight from disk and streams them back, far faster than asking the app to do it. Some servers cache file metadata; all of them handle the OS-level I/O efficiently.
- **Reverse-proxy dynamic requests.** Anything that needs the application (an API call, a rendered page) gets forwarded to the app process — over HTTP, or over a faster binary protocol like FastCGI or a Unix domain socket. The server holds the connection from the client while the app produces the response, then streams the response back.

The win of putting a server in front is that the app process is freed from the generic work. The app handles business logic; the server handles connection management, TLS, static files, and often load-balancing across multiple app instances.

## Nginx: the event-driven default

**Nginx** is the high-performance, event-driven web server and reverse proxy [2]. Where older servers used a thread or process per connection (expensive at scale), Nginx uses an asynchronous, event-driven loop that handles thousands of concurrent connections per worker process with minimal memory. That architecture is why Nginx dominates as the "thing in front of your app" — it's extraordinarily efficient at holding many idle connections and proxying the busy ones.

Nginx is configured via a declarative config file (nginx.conf) where you declare server blocks, location blocks, upstream pools, and proxy rules. The learning curve is the config syntax; the payoff is a server that handles enormous load on modest hardware. Nginx is the default reverse proxy for modern deployments — the thing sitting in front of Node, Go, Python, Ruby apps, terminating TLS and load-balancing.

## Apache: the modular veteran

**Apache HTTP Server** is the older, module-based web server, the A in LAMP [3]. Apache dominated the early web and remains widely used, particularly in shared hosting and legacy setups. Its defining trait is a **modular architecture** — functionality (URL rewriting, auth, PHP execution) is added via loadable modules — and its process/thread-per-connection model (in the prefork/worker MPMs), which is simpler to reason about but more memory-hungry than Nginx's event loop at high concurrency.

Apache vs. Nginx is a classic comparison, and the practical summary: **Nginx excels at serving static content and reverse-proxying at scale; Apache excels at per-directory configuration (.htaccess) and module-based dynamic content.** In production they're often combined — Nginx in front for static + proxy, Apache behind for dynamic — though for greenfield work Nginx-alone-in-front-of-your-app is now the more common shape.

## Caddy: the zero-config modern option

**Caddy** is the modern, Go-based web server whose killer feature is **automatic HTTPS** — it provisions and renews Let's Encrypt TLS certificates with no configuration [4]. Caddy's pitch is that for small-to-medium projects, the TLS/cert choreography that Nginx requires (certbot, cron, reload) is built in and invisible. Its config is also dramatically simpler than Nginx's for the common case.

Caddy is the choice when "hassle-free setup" is the priority — a personal project, a small service, anything where I'd rather not write Nginx config and run cert management. It's less battle-tested at the extreme scale Nginx operates at, but for the vast majority of deployments it's more than sufficient, and the automatic HTTPS is genuinely a quality-of-life upgrade.

## MS IIS: the Windows enterprise option

**Microsoft IIS** (Internet Information Services) is the web server bundled with Windows Server, the choice for ASP.NET and Windows-centric enterprise deployments [5]. IIS integrates tightly with Windows authentication, SSL/TLS, and the .NET runtime. If the stack is Microsoft-first, IIS is the natural — often mandatory — server; outside the Windows world it's rarely chosen.

## Servers vs. application frameworks

One confusion worth heading off: many application frameworks ship with a "development server" (Node's http, Django's runserver, Flask's built-in). Those are **not** production web servers — they're single-process, not hardened, and exist only for local development. In production, the app runs behind a real web server (or a PaaS that provides one). The portfolio's Next.js app runs behind Vercel's edge network, which is effectively a managed reverse proxy doing the terminate-serve-proxy job at scale — the same three responsibilities, just packaged.

## How I use this

The practical decision is which server to put in front of the app, and the answer is almost always Nginx or Caddy:

- **Default, scale, control → Nginx.** The most documented, most proven, most efficient reverse proxy. The config is verbose but I can make it do anything.
- **Small project, hate writing config → Caddy.** Automatic HTTPS, two-line config, "just works."
- **Windows / ASP.NET shop → IIS.** Not a choice, a constraint.
- **PaaS (Vercel, Render, Fly) → managed.** The server is provided; I never touch it.

The habit that matters: I never expose my application process directly to the internet. There's always a web server (or managed equivalent) in front, terminating TLS and absorbing the connection-management work. My app listens on a local port and answers proxied requests. That separation is the single most important deployment discipline — it lets the app and the server each do what they're good at.

## References

[1] Mozilla, "What is a web server?," MDN Web Docs. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_is_a_web_server](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_is_a_web_server)

[2] "Nginx." [Online]. Available: [https://nginx.org/](https://nginx.org/)

[3] "Apache HTTP Server." [Online]. Available: [https://httpd.apache.org/](https://httpd.apache.org/)

[4] "Caddy Server." [Online]. Available: [https://caddyserver.com/](https://caddyserver.com/)

[5] Microsoft, "Internet Information Services (IIS)." [Online]. Available: [https://www.iis.net/](https://www.iis.net/)

```quiz
Q: What are the three responsibilities a web server typically handles in front of an application?
- Terminate TLS, serve static files, reverse-proxy dynamic requests to the app
- Render React components, run SQL queries, send emails
correct: 0
explain: A web server terminates the HTTPS connection (holding the cert), serves static files straight from disk, and reverse-proxies dynamic requests to the application process. The app is freed from generic connection/TLS/static work.

Q: Why is Nginx's event-driven architecture an advantage at scale?
- It handles thousands of concurrent connections per worker with minimal memory, vs. a thread/process per connection
- It compiles to a single binary
correct: 0
explain: Nginx uses an asynchronous event loop instead of spawning a thread or process per connection. This makes it dramatically more memory-efficient when holding many idle connections, which is why it dominates as a reverse proxy.

Q: What is Caddy's distinguishing feature, and when is it the natural choice?
- Automatic HTTPS (provisions and renews Let's Encrypt certs with no config); great for small-to-medium projects where setup simplicity matters
- The fastest raw throughput of any web server
correct: 0
explain: Caddy handles certificate provisioning and renewal automatically. For small projects or anyone who'd rather not write Nginx config and run certbot, Caddy's zero-config HTTPS is a genuine quality-of-life win. It's less common at extreme scale.

Q: A framework's built-in development server (Django runserver, Node http) is suitable for production. True or false, and why?
- False — development servers are single-process, not hardened, and exist only for local development
- True — they are production-grade web servers
correct: 0
explain: Dev servers are not production web servers. In production the app runs behind a real server (Nginx, Caddy, IIS) or a managed PaaS that provides one. Never expose a dev server directly to the internet.

Q: Why is the application process kept behind a web server rather than exposed directly?
- The server absorbs TLS termination, static file serving, and connection management, letting each do what it's good at; the app stays focused on business logic
- There is no benefit; direct exposure is preferred for performance
correct: 0
explain: Separation lets the dedicated server handle generic, high-concurrency work (TLS, static, connections, load-balancing) efficiently, while the app focuses on dynamic responses. It's the most important deployment discipline — never expose the app process directly.
```
