pat@home-server:~$ nginx -t

Secure self-hosting

Getting a container to answer on localhost is the easy part. The useful work starts when it needs a stable name, trusted TLS, sensible access controls, IPv6 support, logs, and a repeatable deployment path.

case study

From a port number to a service

I use Nginx as the edge for many of my self-hosted applications. Services can stay focused on their own local port while Nginx owns names, HTTPS, WebSockets, and exposure policy.

That pattern means a new service becomes a small checklist rather than an improvisation: define DNS, obtain or reuse a certificate, add the Nginx server block, validate syntax, reload, then test the service from the network path that will actually use it.

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name service.example.com;

    ssl_certificate     /etc/letsencrypt/live/example/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:PORT;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Private when private is enough

Not every internal dashboard deserves to be on the public internet. My public homelab notes document a pattern using Cloudflare DNS, Let’s Encrypt DNS challenges, Nginx, and a private tailnet so a service can have a normal trusted HTTPS hostname without being generally reachable from the internet.

That’s a useful distinction: valid TLS and public exposure are separate decisions.

What changed when IPv6 entered the picture

IPv6 removed the familiar NAT boundary. Instead of forwarding one public IPv4 port to one internal host, globally routable addresses meant I had to think directly about inbound firewall policy, stable DNS, address lifetimes, and whether each service was listening on [::].

This is one reason I like running my own infrastructure: networking concepts stop being diagrams in a course and become the difference between “works locally” and “works from another network.”

Debugging order matters

When a hostname does not work, I try not to jump straight to DNS or Cloudflare. I work outward:

  1. Can the application answer on localhost?
  2. Is Nginx listening on the expected address family and port?
  3. Does the virtual host match the request?
  4. Are certificate and filesystem permissions correct?
  5. Does local firewall policy permit it?
  6. Does the router permit inbound IPv6?
  7. Does DNS resolve to the address I think it does?
  8. Finally, test from outside the LAN.

A repeatable workflow beats a clever config

The most valuable thing in my public homelab repository is not one Nginx block; it’s the repeatable workflow around it. Documentation turns a one-off success into something I can recreate months later or explain to someone else.

>
No matching command.