How · 03
How a domain and HTTPS actually work, and the one shared setup every tool guide on this site quietly depends on: a single reverse proxy in front of everything.
Reading this with AI
Don't want to read this top to bottom? Paste this page's URL into ChatGPT or Claude and ask it to walk you through it instead, your AI will know what to do with it better than most guides could predict. A good opening message:
Your server already has an address: its IP address, the string of numbers from the server basics deep dive. A domain doesn't replace that number, it just gives people (and apps) an easier way to reach it. You buy a domain from a registrar, then add a DNS record, usually called an A record, that points it at your server's IP address.
Most of this site's guides use a subdomain per app rather than a separate domain for each one: vault.yourdomain.com, photos.yourdomain.com, and so on, all pointing at the same server. One domain, as many subdomains as you want, each one just another DNS record.
HTTPS encrypts what travels between your device and your server. Without it, anyone on the same network, café wifi, your internet provider, a compromised router, can read or quietly alter what you send, including passwords. Some apps enforce this directly: Bitwarden's clients simply refuse to connect to a server over plain HTTP.
Getting a certificate used to mean paying a certificate authority and manually renewing it every year, a real barrier for a hobby project. That changed with Let's Encrypt, which issues certificates for free and automatically. It's a big part of why self-hosting a real, secure address is practical today in a way it wasn't a decade ago.
Every app in this series listens on its own port inside your server, Immich on 2283, Vikunja on 3456, and so on. You could open each of those ports directly to the internet, but then you'd be requesting, installing, and renewing a separate certificate for every single app, and exposing more of your server's internals than you need to.
A reverse proxy solves this by being the only thing actually reachable from outside. It listens on the standard web ports, 80 and 443, looks at which domain a request came in on, and quietly forwards it to the right app running inside your server. The apps themselves never need to face the internet directly.
This site uses Caddy as that reverse proxy, because its automatic HTTPS needs almost no configuration: point a few lines of config at a domain and a container name, and Caddy requests, installs, and renews the certificate itself.
Here's the part that's easy to get wrong if you set up more than one app from this site: you only want one Caddy, for your whole server. If every app's guide started its own separate Caddy container, the second one would fail, ports 80 and 443 can each only be claimed once. Set up a single shared proxy instead, and have every app plug into it.
Start by creating a Docker network that isn't tied to any one app's compose file, so independent projects can still find each other by name:
Create the network once, before your first app:
docker network create webThen create a folder for the proxy itself, separate from any single app, with its own docker-compose.yml:
services:
caddy:
image: caddy:2
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- web
networks:
web:
external: true
volumes:
caddy_data:
caddy_config:And a Caddyfile next to it. It can start empty, or with your first app already in it:
vault.yourdomain.com {
reverse_proxy vaultwarden:80
}Start it with docker compose up -d. This container is the only thing you'll expose to the internet from now on.
Every tool guide on this site that needs HTTPS follows the same three changes to its own compose file: join the web network, drop its ports: mapping since Caddy now reaches it internally, and add a block to the shared Caddyfile.
services:
your-app:
image: some/app:latest
restart: unless-stopped
networks:
- web
networks:
web:
external: trueThen add a matching block to the proxy's Caddyfile and restart it:
app.yourdomain.com {
reverse_proxy your-app:PORT
}docker compose restart caddyThat's the whole pattern. Every guide on this site that mentions "put it behind HTTPS" means exactly this: join web, drop the exposed port, add a Caddyfile block.
To be unambiguous about the one detail that's easy to get wrong: there is exactly one Caddyfile for your whole server, and it grows one block per app. After setting up Vaultwarden and Immich, for example, that single file looks like this, not two separate files and not two separate Caddy containers:
vault.yourdomain.com {
reverse_proxy vaultwarden:80
}
photos.yourdomain.com {
reverse_proxy immich-server:2283
}Every time you add another tool from this site, you're adding one more block to this same file and restarting the one Caddy container, never creating a second one.
Every tool guide states these facts individually. Gathered here, for whenever you need to check one without re-reading five pages, especially useful if you're running more than one of these on the same server and need to know what's using which port:
| Tool | Internal port | Version pinning | Typical resource use |
|---|---|---|---|
| Vaultwarden | 80 | Floats on :latest, by the project's own convention | Light |
| Immich | 2283 | Pinned to a specific release (e.g. v2.1.0), per Immich's own recommendation | Heaviest tool here: 6 GB RAM / 2 CPU cores minimum, stated by Immich's own docs |
| Vikunja | 3456 | App image floats on :latest; its database is pinned to postgres:18 | Light |
| BookStack | 80 | Floats on :latest | Light |
| Readeck | 8000 | Floats on :latest | Light |
The pinning is genuinely inconsistent, and that's intentional rather than an oversight: Immich's own docs specifically recommend pinning because its mobile app and server need to stay in step, so an unpinned upgrade can break the connection between them. The others don't carry that same constraint, so this series leaves them on :latest, on the assumption you're following the update habit from the backups deep dive rather than letting any image sit untouched for years. If you'd rather have predictable upgrades on any of them, pin the tag yourself, it's a one-line change.
One exception not in the table: Nextcloud's All-in-One installer manages its own port 443 directly and isn't designed to sit behind this shared proxy out of the box. Running AIO alongside other tools from this site on the same server means two things competing for port 443. AIO does support running behind another reverse proxy through its own advanced settings, but that's beyond what this series covers, if you want both, the simplest path is a separate server or a separate public IP for AIO.
One certificate setup covers every app you add from here on, renewed automatically, with no manual steps. Adding a new app just means a new block in one file and a restart, not a new proxy.
What's still on you: picking sensible subdomains, keeping the Caddy image itself updated like anything else, and remembering that this proxy is now a single point of failure for every app behind it. If it goes down, so does access to all of them, one more reason the monitoring habit from the backups deep dive is worth setting up.
What's next
Every tool guide on this site uses exactly this pattern for HTTPS.