Build guide / Hands-on tutorial

Docker Compose Home Server Stack: Nginx, PostgreSQL, Redis

Build a home server stack with Docker Compose. Generate a configuration for Nginx, PostgreSQL and Redis, deploy on Windows or Linux, with health checks and persistent volumes.

Why Docker Compose for a home server

Docker Compose lets you define an entire server stack 鈥?web server, database, cache, media server 鈥?in a single YAML file. One command starts everything. One command tears it down. Every service runs in isolation with explicit networking, volumes and restart policies.

For a home server, this means:

This tutorial walks through building a starter stack with Nginx, PostgreSQL and Redis on a Windows mini PC (Beelink EQi12 with Docker Desktop and WSL2). The same file works on Linux with minor path adjustments.

What you will have when finished: a running three-service Docker Compose stack with health checks, persistent data volumes, no exposed database ports, and a verified restart policy.

Prerequisites

If Docker Desktop is not yet installed, the Windows home-server build guide covers WSL2 and Docker Desktop setup from scratch.

Step 1: generate your Compose file

Use the Docker Compose Starter Generator to create a conservative starting configuration. Select the services you want, set your project name and timezone, then copy the generated YAML.

The generator produces a file with:

Why no exposed database ports: publishing PostgreSQL or Redis to the LAN means any device on your network can attempt connections. Keep them internal and access them through other containers or a controlled tunnel.

Save the output as compose.yaml in a project directory:

C:\Users\YourName\homelab\
鈹溾攢鈹€ compose.yaml
鈹斺攢鈹€ .env

Step 2: create the environment file

The generated Compose file references ${POSTGRES_PASSWORD} for the database password. This value must come from a .env file in the same directory 鈥?never hardcode passwords in the YAML.

Create .env:

POSTGRES_PASSWORD=replace-with-a-long-random-secret

Generate a strong password using a password manager or a command like:

# PowerShell
-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | ForEach-Object {[char]$_})

On Linux:

openssl rand -base64 32

Important: add .env to your .gitignore if you version-control this directory. Never commit secrets.

Step 3: review the generated configuration

Before starting the stack, understand what each section does.

Services

Each service block defines one container:

services:
  nginx:
    image: nginx:1.27-alpine
    restart: unless-stopped
    ports:
      - "8080:80"
    environment:
      TZ: Asia/Shanghai
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1/"]
      interval: 30s
      timeout: 5s
      retries: 3
FieldPurpose
imageWhich container image to pull. Alpine variants are smaller.
restartWhen to restart. unless-stopped keeps it running after Docker or host reboots.
portsHost-to-container port mapping. "8080:80" maps host port 8080 to Nginx port 80.
environmentVariables passed into the container. TZ sets the timezone for log timestamps.
healthcheckPeriodic check Docker runs to determine if the service is healthy.

Volumes

volumes:
  postgres-data:
  redis-data:

Named volumes persist data across container restarts and recreations. Without them, every docker compose down would delete your database.

What the generator deliberately does not include

Step 4: validate and start the stack

Open a terminal in the project directory and validate the configuration:

docker compose config

This command checks syntax, resolves variables from .env, and prints the final configuration. If there are errors, fix them before proceeding.

Start the stack:

docker compose up -d

The -d flag runs containers in the background. Docker pulls the required images on the first run.

Docker Compose stack fully deployed with all services running
Docker Compose stack after `docker compose up -d` completes. All three containers are running with their assigned ports. The first run pulls images from Docker Hub; subsequent starts are nearly instant.

Step 5: verify all services are healthy

Check container status:

docker compose ps

Expected output:

NAME          IMAGE                  STATUS                   PORTS
homelab-nginx-1     nginx:1.27-alpine      Up (healthy)      0.0.0.0:8080->80/tcp
homelab-postgres-1  postgres:17-alpine     Up (healthy)      5432/tcp
homelab-redis-1     redis:7-alpine         Up (healthy)      6379/tcp

All three should show (healthy) after the health check interval passes. If a service shows (unhealthy) or keeps restarting, check its logs:

Docker Compose ps output showing three healthy containers: nginx, postgres, redis
Expected output of `docker compose ps` after a successful startup. All three services show (healthy) status with their port mappings visible. The health check interval must pass before the status changes from (starting) to (healthy).
docker compose logs postgres

Test each service

Nginx 鈥?open http://localhost:8080 in a browser. You should see the default Nginx welcome page.

Nginx default welcome page served from Docker container on localhost port 8080
Nginx welcome page served from the Docker container. Accessing `http://localhost:8080` confirms the container is running, the port mapping is correct, and the health check will transition to (healthy) once the interval passes.

PostgreSQL 鈥?connect from the host:

docker compose exec postgres psql -U app -d app -c "SELECT version();"

This should return the PostgreSQL version string.

Redis 鈥?connect from the host:

docker compose exec redis redis-cli ping

Expected response: PONG.

Write a test key and verify persistence:

docker compose exec redis redis-cli SET test-key "hello-homelab"
docker compose exec redis redis-cli GET test-key

Step 6: add your own services

Once the starter stack is running, extend it with services you actually need.

Example: add a web application

  app:
    image: your-app:1.0
    restart: unless-stopped
    environment:
      DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@postgres:5432/app
      REDIS_URL: redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

The depends_on with condition: service_healthy ensures the database and cache are ready before the application starts. The service names postgres and redis are DNS-resolvable within the Compose network.

Example: add Jellyfin

  jellyfin:
    image: jellyfin/jellyfin:10
    restart: unless-stopped
    ports:
      - "8096:8096"
    volumes:
      - jellyfin-config:/config
      - jellyfin-cache:/cache
      - /path/to/media:/media:ro
    environment:
      TZ: Asia/Shanghai

For the complete Jellyfin setup including Intel QSV hardware transcoding, see the Jellyfin Windows setup guide.

Example: add Home Assistant

  homeassistant:
    image: ghcr.io/home-assistant/home-assistant:stable
    restart: unless-stopped
    ports:
      - "8123:8123"
    volumes:
      - hass-config:/config
    environment:
      TZ: Asia/Shanghai
    privileged: true

Step 7: manage the stack day to day

Stop everything

docker compose down

This stops and removes all containers. Named volumes are preserved 鈥?your data survives.

Stop and delete data

docker compose down -v

The -v flag removes named volumes. Use this only when you want a clean slate.

Update images

docker compose pull
docker compose up -d

Docker pulls the latest image for each pinned tag, then recreates containers that have changed. Existing containers with unchanged images are not restarted.

View logs

# All services
docker compose logs -f

# One service
docker compose logs -f postgres

# Last 50 lines
docker compose logs --tail=50 nginx

Check resource usage

docker stats

This shows real-time CPU, memory and network usage per container. On the EQi12, the three-service starter stack (Nginx + PostgreSQL + Redis) used less than 200MB of RAM at idle.

Step 8: make the stack survive reboots

On Windows with Docker Desktop, the startup chain is:

  1. Windows boots.
  2. Docker Desktop starts (if configured to auto-start).
  3. WSL2 backend initializes.
  4. Docker Engine becomes available.
  5. Compose containers start based on their restart policy.

For this to work unattended:

The Docker Desktop auto-start guide covers the complete startup chain with a three-cycle verification method.

On Linux with Docker Engine as a systemd service, containers with restart: unless-stopped recover automatically after a reboot 鈥?no additional configuration needed.

Step 9: backup your data

Docker volumes are not automatically backed up. Create a backup routine:

How much backup storage do you need? Use the RAID & ZFS capacity calculator to size your backup pool, and the network transfer time calculator to estimate how long a full backup takes over your LAN.

PostgreSQL backup

docker compose exec postgres pg_dump -U app app > backup-$(date +%Y%m%d).sql

Redis backup

docker compose exec redis redis-cli BGSAVE
docker cp $(docker compose ps -q redis):/data/dump.rdb ./redis-backup.rdb

Restore test

A backup you have never restored is only an assumption. Periodically test restoration, and use a unique marker string each time so an old backup cannot pass a new test. Our Docker Compose backup and restore drill runs the full verification with measured recovery times.

# Create a temporary database
docker compose exec postgres psql -U app -d temp_restore < backup-20260721.sql

# Verify data
docker compose exec postgres psql -U app -d temp_restore -c "SELECT count(*) FROM your_table;"

# Clean up
docker compose exec postgres psql -U app -c "DROP DATABASE temp_restore;"

The Windows home-server build guide includes a complete backup and restore verification workflow.

Common problems

“port is already allocated”

Another process or container is using the host port. Change the mapping in compose.yaml:

ports:
  - "8081:80"  # Changed from 8080 to 8081

Or find and stop the conflicting container:

docker ps --format "table {{.Names}}\t{{.Ports}}" | findstr 8080

“network already exists”

A previous Compose project left a network behind. Remove it:

docker network ls
docker network rm <network-name>

Container keeps restarting

Check the logs for the crash reason:

docker compose logs --tail=100 postgres

Common causes: missing environment variables, incorrect volume paths, or a service that cannot connect to its dependency. The depends_on condition helps, but the application itself must also handle connection retries gracefully.

“permission denied” on Linux

Docker commands require the user to be in the docker group:

sudo usermod -aG docker $USER
# Log out and back in, or:
newgrp docker

Security considerations

What to do next

Bottom line

Docker Compose turns a home server setup from a list of manual installation steps into a single file. Generate a starter configuration with the Docker Compose Generator, add a .env file with a strong password, validate with docker compose config, and start with docker compose up -d. The same workflow scales from three services to thirty 鈥?and the file itself documents what is running, how it connects, and where the data lives.