Docker Homelab
Three isometric shipping containers linked by blue cables to a small proxy box beside a padlock shield, showing Docker services behind an SSL proxy.
networking

Nginx Proxy Manager Docker Compose Tutorial with Free SSL

A copy-paste Nginx Proxy Manager docker-compose setup: the compose file, the network config that makes container names resolve, and free SSL certs.

By Docker Homelab Editorial · ·Updated August 18, 2026 · 4 min read

This nginx proxy manager docker compose tutorial gets you from “a pile of containers on random ports” to “clean hostnames with valid SSL” in about 20 minutes, using one compose file you can paste as-is. Nginx Proxy Manager (NPM) is the reverse proxy most homelabbers should start with: it is a pre-built Docker image with a web UI that handles proxy hosts, access lists, and free Let’s Encrypt certificates without you ever hand-editing an nginx config. If you have ever typed 192.168.1.50:8096 into your phone, this is the fix. That port belongs to Jellyfin, the service most people put behind a proxy first.

Who this is for (and who should skip it)

This is for you if you run a handful of services in Docker on a mini-PC or NAS and want jellyfin.yourdomain.com instead of IP-and-port roulette. You need a domain you own (any cheap one works, this can stay entirely internal) and basic docker-compose comfort.

Skip NPM if you are already deep into declarative config: Caddy or Traefik with labels will annoy you less long-term, because NPM’s state lives in a database you click at, not in a file you version-control. That is NPM’s real trade-off. The UI that makes it easy on day one makes it harder to rebuild from scratch on day 400, which is why the backup section below is not optional.

The stack

The official setup docs publish a minimal compose file. Here is a slightly hardened version of it, with a named network you will attach other containers to:

services:
  npm:
    image: 'docker.io/jc21/nginx-proxy-manager:2.15.1'
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      - '80:80'      # HTTP, needed for Let's Encrypt HTTP-01
      - '443:443'    # HTTPS, your actual traffic
      - '81:81'      # admin UI, LAN only, never port-forward this
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    networks:
      - proxy

networks:
  proxy:
    name: proxy

Notes on the choices:

  • The version is pinned to 2.15.1, the current release on the project’s GitHub. The docs use :latest; on a reverse proxy that everything else depends on, pin it and upgrade deliberately.
  • No database service. NPM defaults to SQLite in /data, and for a homelab that is the right answer. The docs cover MySQL/MariaDB and Postgres via DB_MYSQL_* / DB_POSTGRES_* environment variables, but a second container to manage (and back up) buys you nothing at this scale.
  • Bind mounts, not named volumes, so your config sits next to the compose file where your backup job can see it.

Bring it up with docker compose up -d (Compose v2 syntax, per the Docker Compose docs). First start takes a couple of minutes while it generates keys and initializes the database, so don’t panic-restart it. Then open http://<server-ip>:81. The default login is admin@example.com / changeme, and NPM forces you to change both immediately. The current official docs only say “a default admin user will be created,” so if a future version changes this, check docker compose logs npm.

Wiring up your first service

The part every NPM tutorial glosses over: how the proxy actually reaches your other containers. Put them on the same proxy network and Docker’s internal DNS does the rest. In your other service’s compose file:

services:
  jellyfin:
    image: 'jellyfin/jellyfin'
    restart: unless-stopped
    networks:
      - proxy
    # note: no ports section needed for proxied traffic

networks:
  proxy:
    external: true

Now in the NPM UI: Hosts → Proxy Hosts → Add Proxy Host. Domain name jellyfin.yourdomain.com, scheme http, forward hostname jellyfin (the container name resolves on the shared network), forward port 8096 (the container’s internal port, not a published one). Enable “Block Common Exploits” and “Websockets Support” while you are there; half of all “my app half-works behind the proxy” complaints on r/selfhosted are missing websockets.

The payoff of this pattern: services behind the proxy no longer need ports: entries at all. Fewer published ports means a smaller attack surface and no more port-number bookkeeping. The mechanics of why that works are in our Docker Compose networking guide.

SSL: HTTP-01 vs DNS-01, and why you probably want the wildcard

For a public-facing setup, request a cert in the proxy host’s SSL tab and NPM runs the standard HTTP-01 challenge, which per Let’s Encrypt’s challenge documentation requires port 80 reachable from the internet so the CA can fetch a token from your server.

For a lab that stays internal, or if you want one cert covering every service, use DNS-01 instead. NPM’s SSL Certificates screen has a “Use a DNS Challenge” toggle with built-in support for Cloudflare, deSEC, and dozens of other providers; you paste an API token and NPM proves domain ownership by publishing a TXT record. Two reasons this is the better default for homelabs: DNS-01 is the only challenge type that can issue wildcard certificates (*.yourdomain.com), and it works with port 80 closed, so nothing needs to be exposed to the internet at all. The one real cost, which Let’s Encrypt’s docs are candid about, is that a DNS API credential now lives on your server, so scope that token as narrowly as your provider allows.

Backups, or: your proxy config is a database now

Everything you click in the UI lands in ./data (SQLite database, generated nginx configs) and ./letsencrypt (your certs). Lose those directories and you are rebuilding every proxy host from memory. The backup is boringly simple, which is exactly why people skip it:

docker compose stop npm
tar czf npm-backup-$(date +%F).tar.gz data letsencrypt
docker compose start npm

Ship that tarball somewhere that is not the same disk, and actually run the restore drill once: fresh directory, untar, docker compose up -d, confirm the UI still knows your hosts. Ten minutes now versus an evening of reconstruction at month 9.

Two closing operational notes. First, never expose port 81 past your LAN; the admin UI plus a password is not something the open internet should get to guess at. If you want remote admin access, put it behind Tailscale or a VPN rather than a port-forward. Second, a reverse proxy is the front door to everything you host, so patch it like it matters. If you want label-driven config instead of a database you click at, compare it against Traefik. Watching the project’s GitHub releases page is the lightest-weight habit that actually catches proxy and TLS fixes.

Sources

  1. Nginx Proxy Manager — Full Setup Instructions (official docs)
  2. Nginx Proxy Manager — Project Guide (official docs)
  3. Let's Encrypt — Challenge Types
  4. Docker Compose documentation
  5. Nginx Proxy Manager — source repository
#nginx-proxy-manager #docker-compose #reverse-proxy#lets-encrypt #self-hosted

Related