Your First Docker Compose Stack (nginx hello-world)
A starter Docker Compose stack that serves a local folder with nginx. Demonstrates images, containers, ports, volumes, and the five daily-driver compose commands on Linux, macOS, and Windows.
yaml#!/usr/bin/env bash
# First Docker Compose stack — nginx serving a local folder.
# Works on Linux, macOS (Docker Desktop), and WSL2 on Windows.
set -euo pipefail
STACK_DIR="${HOME}/stacks/hello"
mkdir -p "${STACK_DIR}/site"
cd "${STACK_DIR}"
# Write the compose file
cat > docker-compose.yml <<'EOF'
services:
web:
image: nginx:latest
container_name: hello-web
ports:
- "8080:80"
volumes:
- ./site:/usr/share/nginx/html:ro
restart: unless-stopped
EOF
# Write a test page
cat > site/index.html <<'EOF'
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>It works</title></head>
<body><h1>It works. I run my own web server now.</h1></body>
</html>
EOF
# Start the stack
docker compose up -d
echo ""
echo "Stack is running. Open: http://localhost:8080"
echo ""
echo "Useful commands (run from ${STACK_DIR}):"
echo " docker compose ps — show running containers"
echo " docker compose logs -f — live logs"
echo " docker compose pull — pull newer images"
echo " docker compose down — stop and remove containers"
What this does
Creates a minimal Docker Compose stack that runs nginx to serve a local HTML folder. The intent is to teach the four core concepts — images, containers, ports, volumes — by doing, then leave you with the five commands you’ll use every day for every self-hosted service.
Why this approach
Docker Compose is how the vast majority of self-hosted services are actually deployed. When you look at a service’s documentation for “how to self-host this,” it almost always gives you a docker-compose.yml file to run. Understanding what the file contains — images, ports, volumes, environment variables — is the prerequisite for every service you’ll deploy afterward.
The nginx example in this playbook is deliberately minimal because the goal is to understand the concepts, not to install nginx. A real homelab server won’t run nginx by itself; it runs Plex, Immich, Home Assistant, or a dozen other services. But those services all follow exactly the same pattern as the nginx stack: pull an image, map some ports, mount some volumes, set some environment variables, and run. Once you understand the pattern on a simple example, the Arr stack’s six-service compose file is just more of the same thing.
One concept that trips up beginners is the difference between a named volume (managed by Docker, persists across container replacements) and a bind mount (a path on the host filesystem). This playbook uses a bind mount for the served HTML folder (./html:/usr/share/nginx/html) — which means you can see and edit the files on your host — and shows how to add a named volume for data that needs to persist but doesn’t need to be edited directly. The choice between the two matters for every service: config files often work better as bind mounts (you edit them directly), databases and application data often work better as named volumes (Docker manages their lifecycle).
Prerequisites
- Linux: Docker Engine + Compose plugin (docker-install-linux playbook)
- macOS: Docker Desktop for Mac — includes Compose
- Windows: Docker Desktop with WSL2 backend — run compose commands in the WSL2 terminal or PowerShell
The four key concepts in this stack
| Concept | Where it appears | What it means |
|---|---|---|
| Image | nginx:latest |
The read-only template downloaded from Docker Hub |
| Container | hello-web |
The running instance created from the image |
| Port | "8080:80" |
Host port 8080 → container port 80; how your browser reaches it |
| Volume | ./site:/usr/share/nginx/html:ro |
A host folder mounted into the container; data lives on the host |
The five daily-driver commands
docker compose up -d # start (or apply changes) — the one you run most
docker compose ps # what's running and is it healthy?
docker compose logs -f # live log stream — first stop when something breaks
docker compose pull # download newer versions of images
docker compose down # stop and remove containers (volumes are kept)
Edit the page without restarting
The ./site folder is a bind mount — changes on the host are immediately visible inside the container. Edit site/index.html and reload the browser. No compose restart needed.
Change the port
Edit the ports: mapping in docker-compose.yml and re-run docker compose up -d:
ports:
- "9090:80" # now accessible on port 9090
Persist data with a named volume
The ./site bind mount is great when you want to edit the files yourself. For app data you never touch directly (databases, config that the app manages), use a named volume:
services:
db:
image: postgres:16
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
Named volumes survive docker compose down and are managed by Docker. List them with docker volume ls.
Stop and clean up
# Stop and remove the containers (keep volumes and images):
docker compose down
# Also remove the named volumes (WARNING: deletes data):
docker compose down -v
# Also remove the downloaded images:
docker compose down --rmi all
macOS notes
- Docker Desktop for Mac includes the Compose plugin —
docker composeworks in the macOS Terminal exactly as on Linux. - The host path
./sitein the volume mount refers to the directory on your Mac, not inside Docker’s VM. It is shared automatically. localhost:8080resolves to the container; no IP lookup needed.
Windows (WSL2 + Docker Desktop) notes
Run the script from a WSL2 terminal (Ubuntu or Debian distribution). Docker Desktop with the WSL2 backend makes docker and docker compose available inside WSL2 automatically. Access the running stack at http://localhost:8080 from any Windows browser.
Next stacks to try
Once this clicks, every self-hosted guide is a variation on the same four concepts:
- Uptime Kuma monitoring — single-service compose, named volume
- Homepage dashboard — config via YAML files in a bind mount
- Immich photo backup — multi-service compose with a database