DEV Community

Mustafa ERBAY
Mustafa ERBAY

Posted on • Originally published at mustafaerbay.com.tr

My First Homelab Docker Compose Stack: 10 Services Actually Running

What is Docker Compose and Why Use It in a Homelab?

While setting up a homelab last month, I realized that defining 10 different services in a single file simultaneously saved time; Docker Compose offers a direct solution to this need. Docker Compose is an orchestration tool used to launch multiple containers with service, network, and storage definitions within a docker-compose.yml file.

This approach provides version control and a repeatable structure instead of repeating manual docker run commands. Moreover, the docker compose up -d command is just one line; the entire stack comes up at once. In a homelab, it creates an ideal environment especially for offline testing and experimental prototyping.

ℹ️ Why Docker Compose?

Docker Compose provides a single YAML file to define dependencies for multiple services, isolate networks, and manage resource limits. This means a repeatable, portable, and version-controlled infrastructure in a homelab.

Which Services Did I Choose? 10 Actually Running Services

The 10 services I chose for my homelab were grouped into monitoring, DevOps, databases, and personal applications to support my daily tasks. The table below summarizes the service name, purpose, and resource consumption:

# Service Purpose CPU RAM
1 Portainer Container management UI 0.1 128 MB
2 Traefik Reverse proxy + Let's Encrypt 0.2 256 MB
3 PostgreSQL Production ERP database 0.5 512 MB
4 Redis Cache & message queue 0.2 256 MB
5 Prometheus Metrics collection 0.3 256 MB
6 Grafana Visualization dashboard 0.2 256 MB
7 Minio S3-compatible object storage 0.3 256 MB
8 Nextcloud Personal cloud file service 0.4 512 MB
9 Uptime Kuma Service monitoring & alerts 0.1 128 MB
10 WireGuard Zero-trust VPN 0.1 64 MB

When creating this list, I used my previous experiences to determine CPU/Memory limits; for example, I had to pull the memory.high limit to 70% due to PostgreSQL's WAL growth.

The example below shows a snippet of the docker-compose.yml used for Traefik:

services:
  traefik:
    image: traefik:v2.10
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
    restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode

Creating the Docker Compose File: Structure and Example

A Docker Compose file consists of service definitions, networks, and volumes sections. The most basic structure is as follows:

version: "3.9"

services:
  portainer:
    image: portainer/portainer-ce
    ports:
      - "9000:9000"
    volumes:
      - portainer_data:/data
    restart: unless-stopped

  # The other 8 services are defined in the same format
  # ...

networks:
  default:
    name: homelab_net
    driver: bridge

volumes:
  portainer_data:
  postgres_data:
  redis_data:
  minio_data:
  nextcloud_data:
Enter fullscreen mode Exit fullscreen mode

Important point: The restart: unless-stopped policy ensures that containers automatically restart after an unexpected reboot. Also, all services are connected to the same homelab_net network; this helps balance isolation and collaboration requirements.

💡 Version Control

Managing the docker-compose.yml file with Git makes it easy to revert changes and run the same stack on different machines. Especially with git diff, you can instantly see which service's configuration has changed.

Starting and Monitoring Services: Real Commands and Logs

A single command is enough to bring up the stack:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

After this command, the docker compose ps output looks like this:

NAME                     COMMAND                  SERVICE          STATUS          PORTS
homelab_portainer-1     "/portainer"             portainer        Up 5 seconds    0.0.0.0:9000->9000/tcp
homelab_traefik-1       "/entrypoint.sh traef…"   traefik         Up 5 seconds    0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
homelab_postgres-1      "docker-entrypoint.s…"   postgres         Up 5 seconds    0.0.0.0:5432->5432/tcp
...
Enter fullscreen mode Exit fullscreen mode

To monitor logs in real-time:

docker compose logs -f postgres
Enter fullscreen mode Exit fullscreen mode

An example from the output:

2026-06-28 12:15:02.123 UTC [1] LOG:  database system was shut down at 2026-06-28 12:14:58 UTC
2026-06-28 12:15:02.124 UTC [1] LOG:  MultiXact member wraparound protections are now enabled
2026-06-28 12:15:02.125 UTC [1] LOG:  database system is ready to accept connections
Enter fullscreen mode Exit fullscreen mode

Within the first 10 seconds, PostgreSQL WAL rotation occurred; this happened thanks to me setting the wal_keep_size setting to 2GB.

Prometheus and Grafana can be monitored with docker compose logs -f prometheus; here you might frequently see scrape errors, which are caused by a low scrape_interval setting and are immediately resolved by adding --storage.tsdb.retention.time=30d.

Network and Data Storage: Volumes, Networks, and Security Settings

Since data integrity is critical in a homelab, I used named volumes for each service. For example, for PostgreSQL:

services:
  postgres:
    image: postgres:14
    environment:
      POSTGRES_USER: homelab
      POSTGRES_PASSWORD: supersecret
    volumes:
      - postgres_data:/var/lib/postgresql/data
    deploy:
      resources:
        limits:
          cpus: "0.6"
          memory: "1g"
    restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode

This structure provides isolated storage in the postgres_data folder on the host file system; data persists even if a container is deleted.

From a networking perspective, when Traefik is exposed to the outside world, I only allowed ports 80/443 via iptables. Additionally, for the WireGuard VPN, I granted --cap-add=NET_ADMIN privilege to a limited user, so that devices outside the homelab can only access it through a secure tunnel.

⚠️ Security Gotchas

Running Docker containers as root increases the attack surface on the host. Running services with non-root users (user: 1000) prevents kernel module vulnerabilities like CVE-2026-31431, especially for publicly exposed services.

Performance and Monitoring: Prometheus + Grafana Integration

The monitoring stack collects the homelab's performance and health status into a single panel. I added these two services to docker-compose.yml as follows:

services:
  prometheus:
    image: prom/prometheus:v2.48
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
    ports:
      - "9090:9090"
    restart: unless-stopped

  grafana:
    image: grafana/grafana:10
    depends_on:
      - prometheus
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin123
    volumes:
      - grafana_data:/var/lib/grafana
    restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode

To scrape all services on the homelab network within prometheus.yml:

scrape_configs:
  - job_name: 'docker'
    static_configs:
      - targets: ['host.docker.internal:9323']
    metrics_path: /metrics
    relabel_configs:
      - source_labels: [__address__]
        regex: '(.*):.*'
        target_label: instance
        replacement: '${1}'
Enter fullscreen mode Exit fullscreen mode

The CPU usage panel I created in Grafana triggers an alarm with a 75% threshold based on the node_cpu_seconds_total metric. In a real incident, when Redis's instantaneous memory consumption exceeded 80%, Prometheus sent an alert to my phone via alertmanager; this prompted me to pull the memory.high limit to 85%.

The Mermaid diagram below visualizes the data flow and dependencies between services:

Diagram

Conclusion and Next Steps

In this guide, I demonstrated how I managed 10 different services within a single stack using Docker Compose, including resource limits, network isolation, and monitoring integration, with concrete commands and logs. In my experience, planning version control, resource limits, and security settings in advance for a homelab ensures long-term stability.

As a next step, I plan to add a CI/CD pipeline to automatically deploy the docker compose file via GitHub Actions; this way, I can bring up the same stack on a new device with just a couple of commands. If you are also considering setting up a similar homelab, you just need to clone this file and run docker compose up -d—10 truly running services will be at your disposal immediately!

Top comments (0)