Post

Does Anyone Still Run Their Homelab On Plain Linux Docker Compose

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:

  1. A lightweight distribution such as Ubuntu Server, Debian, or Rocky Linux.
  2. Docker Engine installed from the official repository.
  3. 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

FeatureDescription
Declarative ConfigurationServices, networks, volumes, and environment variables are defined in plain YAML, enabling Git‑based version control.
PortabilityThe same Compose file runs on any Linux host with Docker Engine, regardless of underlying hardware.
IsolationEach container runs in its own namespace, providing process and filesystem isolation without the overhead of a full VM.
ExtensibilityCustom healthchecks, restart policies, and resource limits can be fine‑tuned per service.
Community EcosystemHundreds 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_ID and docker 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

PlatformPrimary AdvantagePrimary Drawback
TrueNAS SCALEIntegrated storage, UI, and pluginsAdds a separate management layer, less flexible for custom images
UnraidSimple Docker management UI, easy GPU passthroughProprietary license, limited to specific hardware
PortainerWeb UI for Docker managementStill relies on Docker Engine; UI can become a single point of failure
Plain Linux + Docker ComposeMaximum transparency, scriptable, fully open‑sourceRequires manual setup and maintenance of UI‑less services

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 compose extensions for secrets management via sops or 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

ComponentMinimum SpecificationRecommended Specification
CPU2‑core x86_644‑core modern (Intel i5/i7 or AMD Ryzen)
RAM2 GB8 GB+ (depends on number of services)
Storage20 GB SSD256 GB SSD (for images, logs, backups)
Network1 Gbps Ethernet1 Gbps or higher, with optional VLAN support
OSUbuntu 22.04 LTS or Debian 12Latest LTS distribution with kernel ≥ 5.15

Required Software

  1. Docker Engine – version 24.x or later.
  2. Docker Compose Plugindocker compose (v2) must be installed and placed in $PATH.
  3. Git – for version control of Compose directories.
  4. curl – used for fetching official installation scripts.
  5. 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 (ufw or iptables) 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.json with "userns-remap": "default").

User Permissions

  • Add your regular user to the docker group (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 via systemd socket 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 running docker compose up to 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

CheckCommandExpected Outcome
Container is runningdocker ps --filter "name=$CONTAINER_NAMES_BLOG"Shows Up state with correct ports
Healthcheck passesdocker inspect --format='{{json .State.Health}}' $CONTAINER_IDStatus: "healthy"
Logs are accessibledocker logs $CONTAINER_IDNo immediate errors
Network connectivitycurl http://localhost:8080Returns the blog index page

9. Common Installation Pitfalls

IssueRoot CauseFix
Port conflictAnother service already bound to the same host portUse a different host port or adjust ports mapping
Permission denied on volume mountHost directory owned by rootchown -R 1000:1000 ./html and add user: "1000" to service definition
Image pull failure behind proxyDocker daemon lacks proxy configurationAdd proxy settings to /etc/systemd/system/docker.service.d/http-proxy.conf
Container restarts endlesslyHealthcheck fails repeatedlyAdjust 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

SettingImpactRecommended Value
mem_limitPrevents a single container from exhausting host RAM512m for lightweight services
cpusCaps CPU usage0.5 for background jobs
io_rateControls disk I/O bandwidth1073741824 (1 GiB) for database containers
log_driverReduces disk I/O for logsjson-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 CaseRecommended Base ImageAdditional Tuning
Static Sitenginx:alpineEnable HTTP/2, add add_header Strict-Transport-Security
Databasepostgres:15-alpineUse max_connections=200, enable pg_stat_statements
Monitoringprom/prometheusScrape metrics from all services via Docker socket (/var/run/docker.sock)
**CI/CD  
This post is licensed under CC BY 4.0 by the author.