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
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:
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.ymlfile with Git makes it easy to revert changes and run the same stack on different machines. Especially withgit 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
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
...
To monitor logs in real-time:
docker compose logs -f postgres
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
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
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
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}'
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:
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)