Does Anyone Still Run Their Homelab On Plain Linux Docker Compose
Introduction
The homelab has become a rite of passage for many DevOps engineers, sysadmins, and hobbyists who want to experiment with containerization, orchestration, and self‑hosted services without the overhead of a full‑blown cloud environment. A common question that surfaces on forums and Reddit threads is whether anyone still runs their homelab on a plain Linux install with Docker and a collection of docker‑compose.yml files.
This article directly addresses that query. We will explore the anatomy of a minimal Docker‑Compose‑only homelab, evaluate its relevance in 2025, and walk through a complete, production‑grade workflow that you can adopt today. By the end of this guide you will understand:
- Why a plain‑Linux + Docker‑Compose stack remains a viable option for experienced practitioners.
- How to design, install, configure, and maintain such an environment securely and efficiently.
- Which tools complement Docker‑Compose without adding unnecessary complexity.
- Real‑world troubleshooting patterns and best‑practice hardening techniques.
If you are an advanced sysadmin looking for a lean, scriptable, and fully version‑controlled setup, keep reading.
Understanding the Topic
What is “Plain Linux Docker Compose”?
A plain‑Linux homelab typically consists of:
- A lightweight distribution such as Ubuntu Server, Debian, or Rocky Linux.
- Docker Engine installed from the official repository.
- Docker Compose (v2) used to declare services in static YAML files stored in a version‑controlled directory.
Each service — whether it is a personal blog, a monitoring stack, or a private Git server — lives in its own docker-compose.yml. The entire stack is started with a single docker compose up -d command, and all containers are managed directly by Docker’s daemon.
Historical Context
Docker entered the scene in 2013, and Docker Compose followed shortly after as a means to define multi‑container applications declaratively. Early adopters of homelabs often used raw docker run commands, but as the number of services grew, the community converged on Compose files for reproducibility.
Throughout the 2010s, projects like OpenMediaVault, CasaOS, and later Unraid and TrueNAS introduced graphical interfaces that abstracted away the underlying Docker orchestration. While these platforms democratized homelab deployment, they also introduced additional layers of abstraction that can obscure low‑level debugging and customization.
Core Features
| Feature | Description |
|---|---|
| Declarative Configuration | Services, networks, volumes, and environment variables are defined in plain YAML, enabling Git‑based version control. |
| Portability | The same Compose file runs on any Linux host with Docker Engine, regardless of underlying hardware. |
| Isolation | Each container runs in its own namespace, providing process and filesystem isolation without the overhead of a full VM. |
| Extensibility | Custom healthchecks, restart policies, and resource limits can be fine‑tuned per service. |
| Community Ecosystem | Hundreds of ready‑made images exist on Docker Hub and GitHub Container Registry, covering databases, reverse proxies, monitoring, and more. |
Pros and Cons
Pros
- Full control over every aspect of the container lifecycle.
- Minimal external dependencies — only Docker Engine and Compose are required.
- Easy to script provisioning, backup, and migration tasks.
- Transparent resource accounting via
$CONTAINER_IDanddocker stats.
Cons
- Requires manual handling of networking, storage drivers, and security hardening.
- No built‑in UI for service discovery or configuration management.
- Scaling beyond a few dozen containers may necessitate external orchestration tools (e.g., Kubernetes).
Comparison to Alternatives
| Platform | Primary Advantage | Primary Drawback |
|---|---|---|
| TrueNAS SCALE | Integrated storage, UI, and plugins | Adds a separate management layer, less flexible for custom images |
| Unraid | Simple Docker management UI, easy GPU passthrough | Proprietary license, limited to specific hardware |
| Portainer | Web UI for Docker management | Still relies on Docker Engine; UI can become a single point of failure |
| Plain Linux + Docker Compose | Maximum transparency, scriptable, fully open‑source | Requires manual setup and maintenance of UI‑less services |
Current State and Future Trends
The industry is moving toward GitOps‑driven infrastructure, where declarative manifests (Helm charts, Kustomize overlays, or plain Compose files) are stored in a Git repository and reconciled by an operator. In this paradigm, a plain‑Linux Docker Compose homelab fits naturally as a lightweight GitOps target.
Emerging trends include:
- Edge‑oriented homelabs that run on low‑power hardware (e.g., Raspberry Pi 5) and use Compose for multi‑arch deployments.
- Hybrid orchestration where Compose files are extended with
docker composeextensions for secrets management viasopsor HashiCorp Vault. - Security‑first images that embed OpenSCAP scans into CI pipelines before pushing to private registries.
These trends reinforce the relevance of a minimal Docker‑Compose stack for engineers who value transparency and control.
Prerequisites
System Requirements
| Component | Minimum Specification | Recommended Specification |
|---|---|---|
| CPU | 2‑core x86_64 | 4‑core modern (Intel i5/i7 or AMD Ryzen) |
| RAM | 2 GB | 8 GB+ (depends on number of services) |
| Storage | 20 GB SSD | 256 GB SSD (for images, logs, backups) |
| Network | 1 Gbps Ethernet | 1 Gbps or higher, with optional VLAN support |
| OS | Ubuntu 22.04 LTS or Debian 12 | Latest LTS distribution with kernel ≥ 5.15 |
Required Software
- Docker Engine – version 24.x or later.
- Docker Compose Plugin –
docker compose(v2) must be installed and placed in$PATH. - Git – for version control of Compose directories.
- curl – used for fetching official installation scripts.
- jq – optional, for JSON parsing in scripts.
Network and Security Considerations
- Assign a dedicated static IP range for internal services (e.g.,
192.168.10.0/24). - Configure firewall rules (
ufworiptables) to expose only required ports to the LAN. - Use TLS certificates for reverse proxies (e.g., Caddy, Nginx) to terminate HTTPS internally.
- Enable user namespaces in Docker (
/etc/docker/daemon.jsonwith"userns-remap": "default").
User Permissions
- Add your regular user to the
dockergroup (sudo usermod -aG docker $USER). - Ensure the user has read/write access to the directory containing Compose files.
- For production‑grade hardening, consider using
sudo‑less Docker viasystemdsocket activation.
Installation & Setup
1. Install Docker Engine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Add Docker’s official GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Set up the stable repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \
https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
# Verify installation
docker version
Note: The above commands install Docker Community Edition (CE). For a homelab that may run privileged containers, ensure you understand the security implications.
2. Install Docker Compose Plugin
1
2
3
4
5
6
7
# Pull the latest v2 plugin
DOCKER_COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | jq -r .tag_name | sed 's/^v//')
sudo curl -SL "https://github.com/docker/compose/releases/download/v${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64" -o /usr/local/lib/docker/cli-plugins/docker-compose
sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
# Verify plugin installation
docker compose version
3. Directory Layout
Create a dedicated directory for your homelab configurations:
1
2
3
mkdir -p ~/homelab/compose
cd ~/homelab/compose
git init
Each service will occupy its own subdirectory:
1
2
3
4
5
6
7
mkdir -p \
monitoring \
blog \
gitlab \
nextcloud \
grafana \
prometheus
4. Example Service Directory Structure
1
2
3
4
# Directory: ~/homelab/compose/blog
├── docker-compose.yml
├── .env
└── Dockerfile # optional custom build
5. Sample docker-compose.yml (Blog Service)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
version: "3.9"
services:
blog:
image: nginx:alpine
container_name: $CONTAINER_NAMES_BLOG
restart: unless-stopped
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro
networks:
- internal
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 40s
end_check: false
networks:
internal:
driver: bridge
Explanation of placeholders
$CONTAINER_NAMES_BLOG– a variable you can export before runningdocker compose upto enforce naming conventions../html– a read‑only mount of static site files../nginx.conf– custom Nginx configuration.
6. Environment Variable Management
Create a .env file at the root of each service directory:
# Example: blog/.env
DB_HOST=db
DB_USER=blog_user
DB_PASSWORD=SuperSecret123
Docker Compose automatically loads .env files when they are present in the same directory as the Compose file.
7. Starting the Stack
1
2
3
4
cd ~/homelab/compose/blog
docker compose pull # Pull latest official images
docker compose up -d --remove-orphans
docker compose ps # Verify container status
8. Verification Steps
| Check | Command | Expected Outcome |
|---|---|---|
| Container is running | docker ps --filter "name=$CONTAINER_NAMES_BLOG" | Shows Up state with correct ports |
| Healthcheck passes | docker inspect --format='{{json .State.Health}}' $CONTAINER_ID | Status: "healthy" |
| Logs are accessible | docker logs $CONTAINER_ID | No immediate errors |
| Network connectivity | curl http://localhost:8080 | Returns the blog index page |
9. Common Installation Pitfalls
| Issue | Root Cause | Fix |
|---|---|---|
| Port conflict | Another service already bound to the same host port | Use a different host port or adjust ports mapping |
| Permission denied on volume mount | Host directory owned by root | chown -R 1000:1000 ./html and add user: "1000" to service definition |
| Image pull failure behind proxy | Docker daemon lacks proxy configuration | Add proxy settings to /etc/systemd/system/docker.service.d/http-proxy.conf |
| Container restarts endlessly | Healthcheck fails repeatedly | Adjust start_period or fix the underlying application logic |
Configuration & Optimization
1. Security Hardening
a. Run Containers as Non‑Root
1
2
3
4
5
services:
blog:
image: nginx:alpine
user: "1000:1000" # maps to UID/GID 1000 on the host
# rest of the config unchanged
b. Enable Docker Content Trust
1
2
export DOCKER_CONTENT_TRUST=1
docker compose pull
c. Use Seccomp Profiles
Create a custom seccomp JSON file (seccomp.json) and reference it:
1
2
3
4
services:
blog:
security_opt:
- seccomp=./seccomp.json
d. Limit Kernel Capabilities
1
2
3
4
cap_add:
- NET_ADMIN
cap_drop:
- ALL
e. Secret Management
Store secrets outside the repository and inject them via Docker Swarm‑style secret files or environment variables sourced from a protected location:
1
2
# Example: reading a secret file
export DB_PASSWORD=$(cat /etc/docker/secrets/db_password)
2. Performance Optimization
| Setting | Impact | Recommended Value |
|---|---|---|
mem_limit | Prevents a single container from exhausting host RAM | 512m for lightweight services |
cpus | Caps CPU usage | 0.5 for background jobs |
io_rate | Controls disk I/O bandwidth | 1073741824 (1 GiB) for database containers |
log_driver | Reduces disk I/O for logs | json-file with max-size: "10m" and max-file: "3" |
Example snippet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
services:
postgres:
image: postgres:15-alpine
deploy:
resources:
limits:
memory: 1g
cpus: "1.0"
reservations:
memory: 512m
cpus: "0.5"
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
3. Integration with Reverse Proxies
A common pattern is to place Caddy or Nginx as a front‑end reverse proxy that terminates TLS and routes traffic to internal services. Example Caddyfile:
blog.localhost {
reverse_proxy blog:80
encode gzip
}
Add the reverse proxy service to your Compose file and expose only port 443 on the host.
4. Customization for Different Use Cases
| Use Case | Recommended Base Image | Additional Tuning |
|---|---|---|
| Static Site | nginx:alpine | Enable HTTP/2, add add_header Strict-Transport-Security |
| Database | postgres:15-alpine | Use max_connections=200, enable pg_stat_statements |
| Monitoring | prom/prometheus | Scrape metrics from all services via Docker socket (/var/run/docker.sock) |
| **CI/CD |