Caddy vs Nginx Proxy Manager for a Homelab: When a GUI Slows the Next Compose Rewrite

Nadia Okoye

Nadia Okoye

September 18, 2026

Caddy vs Nginx Proxy Manager for a Homelab: When a GUI Slows the Next Compose Rewrite

I have rebuilt the same homelab compose file four times in two years: a used ThinkCentre, then a Mini PC, then TrueNAS SCALE apps I regretted, then Compose on Debian again. The services survived because they were YAML. Nginx Proxy Manager survived as a screenshot folder and a SQLite file I was never sure I had exported. Caddy survived as a Caddyfile in the same git repo as the stack. That is the review.

NPM is a good GUI. Caddy is a good file. The next rewrite does not care how pretty the proxy was on Tuesday. It cares whether you can grep the next hostname.

What I actually run at the edge

Caddy 2, official image, a mounted Caddyfile, ports 80 and 443, and the Cloudflare DNS plugin when I need a wildcard for *.lab.example.net without opening 80 on a CGNAT path. Certificates happen. I do not click Renew. I do not have an admin UI on 81 that I must remember to keep off the internet.

NPM, when I still used it, was jc21’s container, a MariaDB or SQLite, a UI on 81, and a pile of “Proxy Hosts” that looked like a control panel from a cheaper cPanel. Let’s Encrypt worked. Access lists worked. The Advanced tab held nginx snippets I would not have found in git because they never lived in git.

Both will terminate TLS for Immich, Vaultwarden, and a blog. Both will 502 when the upstream name in Compose changed and you did not. Only one of those 502s is a search-replace.

Paper notebook and ethernet cable on a wooden desk

The rewrite that made me retire the GUI

The stack moved from app_ghost_1 names to a project named prod with services called ghost and immich-server. Docker’s DNS names changed. In Caddy I changed seven reverse_proxy lines. In NPM I had nineteen proxy hosts, three of them with custom locations, two with forced websocket, one with a custom client_max_body_size I had typed in Advanced after an Immich upload failed. I clicked each host. I fat-fingered Vaultwarden and spent a morning on a redirect loop. I did not have a diff. The UI does not diff.

NPM can be backed up. People export the data folder. I have restored that folder onto a new VM and watched certificates sulk until I clicked around. Caddy’s data volume has certificates too, and I have restored that, but if I lose the volume Caddy just gets new certs from Let’s Encrypt or Cloudflare. The intent is the Caddyfile. NPM’s intent is a database. Databases are fine. I already have enough of them.

Compose rewrites also change networks. A new internal network and an edge network means the proxy must sit on both. In Compose that is a list under networks:. In NPM it is “I hope the container is on the same bridge as Immich,” plus a custom docker network you clicked in Portainer six months ago and did not write down. I have had NPM lose a host after I “cleaned up unused networks.”

Where NPM still wins

A relative who will add a new service while I am away. A small team that should not edit a Caddyfile on a Friday. Access lists with a basic auth user they can reset without learning caddy hash-password. The UI is the product, and it is a real product. I have put NPM in front of a friend’s *arr stack and told them not to ssh. That was the right call. They have not rewritten Compose. They have added a host twice a year.

Custom nginx is also easier to paste from a forum into NPM’s Advanced box than to translate into Caddy at 1 a.m. That ease is how the Advanced box becomes the only copy of a proxy_hide_header you need. I still do it for other people. I do not do it for myself anymore.

If you want nginx specifically — because you already know nginx, because you have a thirty-line location you will not rewrite — NPM or a raw nginx container beats Caddy. Caddy is not nginx with a friendlier file. It is a different server. handle_path is not location /foo/. I have lost time pretending they are the same.

Basement shelf with a small server and tidy ethernet

The upgrades nobody calendars

NPM’s own compose file is a product with versions. I have watched a “latest” pull refuse to start until the database migrated, and I have watched a plugin for the UI fail after that migration. During that hour every proxy host is still in the DB, and none of them are serving if the container is restarting. Caddy upgrades have been a new tag and a restart. I have had a Caddy plugin build break when I forgot to include xcaddy in the image — that is on me, and it is a file I can pin. NPM’s break was a GUI I could not pin because I had trusted latest.

Immich and Nextcloud both want a large upload body. In Caddy that is request_body / a limit I set once in a snippet. In NPM it is Advanced text on each host, or a custom nginx snippet in the UI’s “Custom locations.” I have fixed Immich on one hostname and forgotten Paperless on the other. A snippet in git is one place. Nineteen Advanced tabs are nineteen chances to be almost consistent.

Websockets are the other copy-paste. Home Assistant, Portainer, and some *arr UIs need them. Caddy’s reverse_proxy handles Upgrade by default in current versions. NPM has a checkbox. The checkbox is easy until you clone a host and do not notice it is unchecked. I cloned a host. I did not notice.

Caddy habits that survive a move

The Caddyfile lives next to compose.yaml. A site block per hostname, reverse_proxy immich-server:2283, encode gzip only when I mean it, and a snippet for the headers I actually want. I do not generate the file from a GUI. I have used caddy-docker-proxy (labels on each service) and I retired it for the same reason I retired Traefik labels: the next rewrite scatters intent across ten services. One file is greppable. Labels are fashionable until you rename a project.

DNS challenge: Cloudflare token in an env file, not in the Caddyfile. When I move hosts, the token comes with the env, the Caddyfile comes with git, the certs come back from Let’s Encrypt. NPM’s cert story after a move has been “wait” and “click renew” and once “I will just HTTP-01 again” on a box that no longer has port 80 from the world.

I still run a tiny NPM in a lab VM when I teach someone the idea of a reverse proxy. I do not promote it to the house edge anymore. The house edge gets rewritten whenever I buy a different used PC. Files like that. Clicking does not.

Security theater both of us do

Neither proxy is a WAF you should brag about. Both will expose Vaultwarden to the world if you point a name at it. I put the admin UIs — NPM’s :81, Portainer, the NAS — on Tailscale only. Caddy does not get a public site for those. NPM users often publish :81 “temporarily.” I have scanned that temporary port on other people’s labs. It stays.

Access lists in NPM are fine. Caddy’s basic_auth and forward_auth (Authelia, authentik) are fine. I prefer SSO in front of the apps that matter and nothing in front of a public Immich share I meant to be public. The proxy should not be the only password on a photo library.

The Traefik temptation

If you already write labels, Traefik will feel closer to Compose than either Caddy-as-a-file or NPM-as-a-GUI. I have retired two Traefik stacks after a rewrite because the labels were the config, and the config was wrong in four services. Caddyfile is the compromise I kept: still a file, still automatic HTTPS, still not a dashboard I must upgrade. If a paragraph in your head is already asking whether labels beat a Caddyfile, that is a different argument than NPM. NPM is not in that argument. NPM is a form.

What I tell people before they click

If you will not use git and you will add three hosts a year, NPM is kinder. Export the data directory on a calendar. Do not keep production logic only in Advanced.

If you rewrite Compose when you buy hardware, or when you finally name services like an adult, Caddy is kinder. Put the file in the repo. Do not invent a GUI so you can avoid learning twenty lines of reverse_proxy.

I like GUIs for applications. I like them on Immich. I like them on Home Assistant. I do not like them as the only record of how traffic enters the house. The next rewrite will not ask what I liked last winter. It will ask where the hostname went, and whether I can prove it from a repo. Caddy answers with a file. NPM answers with a login, a database, and a memory of which Advanced tab I meant.

More articles for you