Docker Compose 101: Run a Two-Container Website
Docker Compose describes a multi-container application in compose.yaml. This lab runs Caddy as a reverse proxy in front of an NGINX container that serves a static HTML page. Compose creates a private network, so Caddy reaches NGINX using the service name site.
Browser on this host -> Caddy proxy (127.0.0.1:8080) -> NGINX site (port 80)Create the Project and HTML Page
Project FilesCreate a project folder under your home directory and add one HTML file. The page stays on the host so you can edit it without rebuilding an image. Run the later commands from this project folder, or include the same cd command at the start of each new terminal session.
test ! -e "$HOME/docker-compose-first-app" && test ! -L "$HOME/docker-compose-first-app" || { echo "Project path already exists; choose a fresh path." >&2; exit 1; }mkdir -p "$HOME/docker-compose-first-app/site" || exit 1cd "$HOME/docker-compose-first-app" || exit 1
cat > site/index.html <<'EOF'<!doctype html><html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Compose First App</title> </head> <body> <h1>Hello from Docker Compose</h1> <p>Caddy is proxying this page from the NGINX site container.</p> </body></html>EOF❯ Illustrative Output (Results Vary)
$ find . -maxdepth 2 -type f -print./site/index.htmlWrite the Caddyfile and Compose Configuration
Two ServicesCaddy accepts requests on port 80 inside its container and forwards them to the Compose service named site. NGINX serves the host’s site/ directory through a read-only bind mount. The two named volumes keep Caddy’s /data and /config state outside the container lifecycle.
The NGINX health check requests its local page. Compose waits for that check to pass before starting the proxy. The site container has no published host port; requests reach it through Caddy on the Compose network.
cd "$HOME/docker-compose-first-app" || exit 1
cat > Caddyfile <<'EOF':80 { reverse_proxy site:80}EOF
cat > compose.yaml <<'EOF'services: proxy: image: caddy:2-alpine restart: unless-stopped ports: - "127.0.0.1:8080:80" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config depends_on: site: condition: service_healthy
site: image: nginx:alpine restart: unless-stopped volumes: - ./site:/usr/share/nginx/html:ro healthcheck: test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"] interval: 5s timeout: 3s retries: 5 start_period: 5s
volumes: caddy_data: caddy_config:EOF❯ Illustrative Output (Results Vary)
$ find . -maxdepth 2 -type f -print./Caddyfile./compose.yaml./site/index.htmlAsk Compose to Parse the Configuration
Configuration Checkdocker compose config resolves and validates the Compose model before creating containers. Fix YAML indentation or service-name errors if Compose reports them. The printed model can be longer than this excerpt.
cd "$HOME/docker-compose-first-app" || exit 1docker compose config❯ Illustrative Output (Results Vary)
name: docker-compose-first-appservices: proxy: image: caddy:2-alpine depends_on: site: condition: service_healthy site: image: nginx:alpine healthcheck: retries: 5volumes: caddy_config: {} caddy_data: {}Start Both Containers and Test the Proxy
Run and VerifyStart the services in the background, check their state, then request the page through Caddy. The first start downloads the images if they are not already on the host. Wait for the site health status to become healthy before testing the page.
cd "$HOME/docker-compose-first-app" || exit 1docker compose up --detachdocker compose pscurl --fail --show-error http://127.0.0.1:8080❯ Illustrative Output (Results Vary)
NAME SERVICE STATUS docker-compose-first-app-proxy-1 proxy Up docker-compose-first-app-site-1 site Up (healthy)
<!doctype html><html lang="en">...<h1>Hello from Docker Compose</h1>Stop the Stack and Choose What to Keep
Lifecycledocker compose down removes the containers and the Compose network but keeps the named volumes. That lets Caddy reuse its state next time. To remove the two project volumes as well, run the second command; the host-side HTML and Caddyfile remain in the project directory.
cd "$HOME/docker-compose-first-app" || exit 1docker compose down
# Optional: remove this Compose project's named volumes too.docker compose down --volumes❯ Illustrative Output (Results Vary)
[+] Running 3/3 ✔ Container docker-compose-first-app-proxy-1 Removed ✔ Container docker-compose-first-app-site-1 Removed ✔ Network docker-compose-first-app_default Removed