Tools: Docker Compose Self-Hosted Services Guide (2026)

Tools: Docker Compose Self-Hosted Services Guide (2026)

The Foundations Before the Services

1. Portainer - Container Management

2. BookStack - Documentation and Knowledge Base

3. Vaultwarden - Password Management

4. Uptime Kuma - Monitoring

5. Gitea - Self-Hosted Git

6. Grafana with Prometheus - Metrics and Dashboards

7. Nextcloud - File Sync and Collaboration

8. Homepage - Service Dashboard

The Security Baseline That Actually Matters There is a certain satisfaction in running your own stack. Not because self-hosting is always the right choice, but because the discipline of deploying, securing, and maintaining your own services teaches you things that clicking through cloud consoles never does. You understand what a reverse proxy is doing when you have configured one. You understand secrets management when you have broken something by leaving credentials in a compose file. I run a self-hosted stack at home and have built similar setups for small IT teams. This guide covers eight services worth running yourself, with working Docker Compose configurations, security notes, and an honest view of where self-hosting earns its keep versus where managed services are the right call. Before you start, two things. First: Docker Compose is the right tool here. Not Kubernetes, not Nomad, not whatever the current trend is. For a small team or a serious home lab, Compose gives you readable declarative configuration, simple rollback, and low operational overhead. Second: put these services on a segmented network. Every service in this list shares a common infrastructure requirement: a reverse proxy. Running each service on a different host port (:8080, :8443, :3000) is fine for development, but it is a maintenance problem at any scale. You end up with port-mapping spreadsheets, inconsistent TLS handling, and no central place to manage access. Traefik solves this. It is a container-aware reverse proxy that integrates directly with Docker and manages TLS certificates automatically via Let's Encrypt. Create the proxy network once with docker network create proxy, then every service joins it and gets a Traefik label for routing. Portainer gives you a browser-based view of running containers, volumes, networks, and compose stacks. Security note: Restrict Portainer to your management VLAN or VPN. The admin account has root-equivalent access to everything Docker can reach. BookStack uses a Book/Chapter/Page hierarchy that maps well to how IT documentation actually works. Note the internal: true network for the database - the database container has no external access. Vaultwarden is a lightweight Bitwarden-compatible server. For a small team sharing infrastructure credentials, it is the right answer. SIGNUPS_ALLOWED=false is essential. After creating the accounts you need, disable open registration entirely. Uptime Kuma monitors URLs, TCP ports, Docker containers, and DNS entries, and sends alerts via Telegram, Slack, email, and a dozen other channels. Gitea is lightweight, fast, and has a GitHub-like interface. The whole thing runs on less than 256MB RAM. Security note: Disable public registration after creating your account. Grafana and Prometheus give you time-series metrics, customisable dashboards, and alerting based on thresholds rather than binary up/down status. Prometheus is on the internal network only. Only Grafana is exposed via Traefik. For a small IT team, the value is control: your files stay on your hardware, you set the retention policy, and you know exactly who has access to what. When you are running eight services, you want a single place to see them all. Homepage is a customisable application dashboard that integrates with Docker to pull running container status automatically. Read the full guide with complete compose configurations at danieljamesglover.com Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

# traefik/-weight: 500;">docker-compose.yml services: traefik: image: traefik:v3.0 container_name: traefik -weight: 500;">restart: unless-stopped command: - "--api.insecure=false" - "--providers.-weight: 500;">docker=true" - "--providers.-weight: 500;">docker.exposedbydefault=false" - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" - "--certificatesresolvers[email protected]" - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" ports: - "80:80" - "443:443" volumes: - /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock:ro - ./letsencrypt:/letsencrypt networks: - proxy networks: proxy: external: true # traefik/-weight: 500;">docker-compose.yml services: traefik: image: traefik:v3.0 container_name: traefik -weight: 500;">restart: unless-stopped command: - "--api.insecure=false" - "--providers.-weight: 500;">docker=true" - "--providers.-weight: 500;">docker.exposedbydefault=false" - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" - "--certificatesresolvers[email protected]" - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" ports: - "80:80" - "443:443" volumes: - /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock:ro - ./letsencrypt:/letsencrypt networks: - proxy networks: proxy: external: true # traefik/-weight: 500;">docker-compose.yml services: traefik: image: traefik:v3.0 container_name: traefik -weight: 500;">restart: unless-stopped command: - "--api.insecure=false" - "--providers.-weight: 500;">docker=true" - "--providers.-weight: 500;">docker.exposedbydefault=false" - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" - "--certificatesresolvers[email protected]" - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" ports: - "80:80" - "443:443" volumes: - /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock:ro - ./letsencrypt:/letsencrypt networks: - proxy networks: proxy: external: true services: portainer: image: portainer/portainer-ce:latest container_name: portainer -weight: 500;">restart: unless-stopped volumes: - /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock:ro - portainer_data:/data labels: - "traefik.-weight: 500;">enable=true" - "traefik.http.routers.portainer.rule=Host(`portainer.yourdomain.com`)" - "traefik.http.routers.portainer.entrypoints=websecure" - "traefik.http.routers.portainer.tls.certresolver=letsencrypt" networks: - proxy services: portainer: image: portainer/portainer-ce:latest container_name: portainer -weight: 500;">restart: unless-stopped volumes: - /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock:ro - portainer_data:/data labels: - "traefik.-weight: 500;">enable=true" - "traefik.http.routers.portainer.rule=Host(`portainer.yourdomain.com`)" - "traefik.http.routers.portainer.entrypoints=websecure" - "traefik.http.routers.portainer.tls.certresolver=letsencrypt" networks: - proxy services: portainer: image: portainer/portainer-ce:latest container_name: portainer -weight: 500;">restart: unless-stopped volumes: - /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock:ro - portainer_data:/data labels: - "traefik.-weight: 500;">enable=true" - "traefik.http.routers.portainer.rule=Host(`portainer.yourdomain.com`)" - "traefik.http.routers.portainer.entrypoints=websecure" - "traefik.http.routers.portainer.tls.certresolver=letsencrypt" networks: - proxy services: bookstack: image: lscr.io/linuxserver/bookstack:latest container_name: bookstack -weight: 500;">restart: unless-stopped environment: - APP_URL=https://docs.yourdomain.com - DB_HOST=bookstack-db - DB_PASS=${DB_PASS} volumes: - ./config:/config networks: - proxy - internal bookstack-db: image: mariadb:10.11 networks: - internal services: bookstack: image: lscr.io/linuxserver/bookstack:latest container_name: bookstack -weight: 500;">restart: unless-stopped environment: - APP_URL=https://docs.yourdomain.com - DB_HOST=bookstack-db - DB_PASS=${DB_PASS} volumes: - ./config:/config networks: - proxy - internal bookstack-db: image: mariadb:10.11 networks: - internal services: bookstack: image: lscr.io/linuxserver/bookstack:latest container_name: bookstack -weight: 500;">restart: unless-stopped environment: - APP_URL=https://docs.yourdomain.com - DB_HOST=bookstack-db - DB_PASS=${DB_PASS} volumes: - ./config:/config networks: - proxy - internal bookstack-db: image: mariadb:10.11 networks: - internal services: vaultwarden: image: vaultwarden/server:latest environment: - DOMAIN=https://vault.yourdomain.com - SIGNUPS_ALLOWED=false - ADMIN_TOKEN=${ADMIN_TOKEN} volumes: - ./vw-data:/data services: vaultwarden: image: vaultwarden/server:latest environment: - DOMAIN=https://vault.yourdomain.com - SIGNUPS_ALLOWED=false - ADMIN_TOKEN=${ADMIN_TOKEN} volumes: - ./vw-data:/data services: vaultwarden: image: vaultwarden/server:latest environment: - DOMAIN=https://vault.yourdomain.com - SIGNUPS_ALLOWED=false - ADMIN_TOKEN=${ADMIN_TOKEN} volumes: - ./vw-data:/data services: uptime-kuma: image: louislam/uptime-kuma:1 volumes: - uptime-kuma:/app/data labels: - "traefik.-weight: 500;">enable=true" - "traefik.http.routers.uptime-kuma.rule=Host(`-weight: 500;">status.yourdomain.com`)" services: uptime-kuma: image: louislam/uptime-kuma:1 volumes: - uptime-kuma:/app/data labels: - "traefik.-weight: 500;">enable=true" - "traefik.http.routers.uptime-kuma.rule=Host(`-weight: 500;">status.yourdomain.com`)" services: uptime-kuma: image: louislam/uptime-kuma:1 volumes: - uptime-kuma:/app/data labels: - "traefik.-weight: 500;">enable=true" - "traefik.http.routers.uptime-kuma.rule=Host(`-weight: 500;">status.yourdomain.com`)" services: gitea: image: gitea/gitea:latest environment: - GITEA__database__DB_TYPE=postgres - GITEA__database__HOST=gitea-db:5432 networks: - proxy - internal gitea-db: image: postgres:15 networks: - internal services: gitea: image: gitea/gitea:latest environment: - GITEA__database__DB_TYPE=postgres - GITEA__database__HOST=gitea-db:5432 networks: - proxy - internal gitea-db: image: postgres:15 networks: - internal services: gitea: image: gitea/gitea:latest environment: - GITEA__database__DB_TYPE=postgres - GITEA__database__HOST=gitea-db:5432 networks: - proxy - internal gitea-db: image: postgres:15 networks: - internal services: prometheus: image: prom/prometheus:latest networks: - internal grafana: image: grafana/grafana:latest environment: - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASS} - GF_USERS_ALLOW_SIGN_UP=false networks: - proxy - internal services: prometheus: image: prom/prometheus:latest networks: - internal grafana: image: grafana/grafana:latest environment: - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASS} - GF_USERS_ALLOW_SIGN_UP=false networks: - proxy - internal services: prometheus: image: prom/prometheus:latest networks: - internal grafana: image: grafana/grafana:latest environment: - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASS} - GF_USERS_ALLOW_SIGN_UP=false networks: - proxy - internal - Secrets stay out of compose files. Use a .env file. Add .env to .gitignore immediately. - Separate networks by trust level. Public-facing services on the proxy network. Database containers on internal-only networks. - Run scheduled security scanning. Trivy and Docker Bench for Security are worth running regularly. - Back up volumes, not just images. The image is replaceable. Your BookStack content and Vaultwarden data are not. - Pin image versions in production. latest can introduce breaking changes on pull.