Home / Walkthroughs / Self-Hosted Stack
 Docker & Containers

Self-Hosted Services Stack
with Docker & Portainer

Deploy a full self-hosted service stack from scratch — Portainer for management, Nginx Proxy Manager for routing, AdGuard for DNS filtering, Plex/Emby for media, and Cloudflare for dynamic DNS and SSL certificates.

Beginner–Intermediate ~60 minutes Docker Compose
Docker Portainer Docker Compose AdGuard Plex Cloudflare DDNS
📖
Overview

Running everything in Docker containers lets you spin services up and down in seconds, keep configs in portable compose files, and isolate each service cleanly. This guide covers the core stack we use in the homelab — from DNS and proxy routing all the way through media management and remote access.

The architecture assumes a single Docker host (Unraid, Debian, or Ubuntu) with an NGINX Proxy Manager container handling all reverse proxy duties and a single wildcard SSL certificate covering every subdomain.

🐳
Install Docker & Docker Compose
  1. 1

    Install Docker Engine

    # Quick install (Debian/Ubuntu) curl -fsSL https://get.docker.com | sh usermod -aG docker $USER # add your user to docker group newgrp docker
  2. 2

    Install Docker Compose Plugin

    apt install docker-compose-plugin docker compose version # confirm: Docker Compose version v2.x
  3. 3

    Create appdata Directory

    Standardize your config storage location — everything goes under /opt/appdata/:

    mkdir -p /opt/appdata/{portainer,npm,adguard,plex,overseerr,ddns}
🖥️
Portainer CE

Portainer provides a GUI for Docker — manage stacks, containers, images, and volumes without the CLI. Deploy it first so you can manage everything else through the UI.

docker run -d \ --name=portainer \ --restart=unless-stopped \ -p 8000:8000 \ -p 9443:9443 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /opt/appdata/portainer:/data \ portainer/portainer-ce:latest

Access: https://<host-ip>:9443 — create admin account on first visit.

Portainer reads /var/run/docker.sock to control Docker. On Unraid, this is already available — add Portainer via Community Apps instead of running this command manually.
🔀
Nginx Proxy Manager

NPM routes external traffic to internal services by hostname. Pair with a Cloudflare wildcard SSL cert and you get HTTPS on every subdomain automatically.

version: '3.8' services: npm: image: jc21/nginx-proxy-manager:latest container_name: nginx-proxy-manager restart: unless-stopped ports: - "80:80" - "443:443" - "81:81" # Admin panel volumes: - /opt/appdata/npm/data:/data - /opt/appdata/npm/letsencrypt:/etc/letsencrypt

Admin panel: http://<host-ip>:81 — default login: admin@example.com / changeme

See the full NPM walkthrough for SSL and proxy host setup.

🛡️
AdGuard Home (DNS Filtering)

AdGuard Home acts as a local DNS server that blocks ads and tracking at the network level. All devices on the LAN point their DNS at this container.

version: '3.8' services: adguard: image: adguard/adguardhome:latest container_name: adguardhome restart: unless-stopped ports: - "53:53/tcp" # DNS - "53:53/udp" - "3000:3000" # Setup wizard - "8080:80" # Admin panel after setup volumes: - /opt/appdata/adguard/work:/opt/adguardhome/work - /opt/appdata/adguard/conf:/opt/adguardhome/conf
Port 53 may conflict with systemd-resolved on Ubuntu. Disable it first: systemctl stop systemd-resolved && systemctl disable systemd-resolved, then edit /etc/resolv.conf to point at your router.

After setup, configure your router's DHCP server to hand out the AdGuard host IP as the DNS server for all LAN clients.

☁️
Cloudflare DDNS

If your ISP assigns a dynamic public IP (most do), use a DDNS container to automatically update your Cloudflare A record whenever the IP changes.

version: '3.8' services: cloudflare-ddns: image: oznu/cloudflare-ddns:latest container_name: cloudflare-ddns restart: unless-stopped environment: - API_KEY=your_cloudflare_api_token - ZONE=yourdomain.com - SUBDOMAIN=@ # root domain - PROXIED=false # true to route through CF CDN

Use a scoped Cloudflare API token with Zone:DNS:Edit permissions — not the Global API Key.

🎬
Media Stack (Plex / Overseerr)
version: '3.8' services: plex: image: linuxserver/plex:latest container_name: plex restart: unless-stopped network_mode: host # required for Plex discovery environment: - PUID=1000 - PGID=1000 - VERSION=docker - PLEX_CLAIM=claim-XXXXXX # from plex.tv/claim volumes: - /opt/appdata/plex:/config - /mnt/media/movies:/movies:ro - /mnt/media/tv:/tv:ro overseerr: image: sctx/overseerr:latest container_name: overseerr restart: unless-stopped ports: - "5055:5055" volumes: - /opt/appdata/overseerr:/app/config

Overseerr connects to your Plex library and lets users request movies/shows — it integrates with Radarr and Sonarr to handle the download.