Skip to content

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)

01

Create the Project and HTML Page

Project Files

Create 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.

Terminal window
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 1
cd "$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.html

02

Write the Caddyfile and Compose Configuration

Two Services

Caddy 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.

Terminal window
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.html

03

Ask Compose to Parse the Configuration

Configuration Check

docker 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.

Terminal window
cd "$HOME/docker-compose-first-app" || exit 1
docker compose config
❯ Illustrative Output (Results Vary)
name: docker-compose-first-app
services:
proxy:
image: caddy:2-alpine
depends_on:
site:
condition: service_healthy
site:
image: nginx:alpine
healthcheck:
retries: 5
volumes:
caddy_config: {}
caddy_data: {}

04

Start Both Containers and Test the Proxy

Run and Verify

Start 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.

Terminal window
cd "$HOME/docker-compose-first-app" || exit 1
docker compose up --detach
docker compose ps
curl --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>

05

Stop the Stack and Choose What to Keep

Lifecycle

docker 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.

Terminal window
cd "$HOME/docker-compose-first-app" || exit 1
docker 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

Comments