Most Docker tutorials are either too abstract or too production-heavy. This is the small multi-service stack I built to understand how containers actually wire together: a Node API, a Python worker, and a static web front end, all started with one command.

The services

ServiceRuntimePurposePort
APINode + ExpressReturns random and sorted numbers3001
WorkerPythonPolls the API and logs resultsnone
WebNginxServes a static page that displays data8080
Cloud architecture checkpoint illustration for this section.

The API

mkdir -p multilang/api multilang/worker multilang/web
cd multilang
cat > api/index.js <<'EOF'
const express = require("express");
const app = express();
function generateNumbers() {
return Array.from({ length: 15 }, () => Math.floor(Math.random() * 100));
}
app.get("/numbers", (_req, res) => {
res.json({ numbers: generateNumbers() });
});
app.get("/sorted", (_req, res) => {
const nums = generateNumbers();
const sorted = [...nums].sort((a, b) => a - b);
res.json({ original: nums, sorted });
});
app.listen(3001, () => console.log("api running on 3001"));
EOF
cat > api/package.json <<'EOF'
{
"name": "api",
"version": "1.0.0",
"main": "index.js",
"scripts": { "start": "node index.js" },
"dependencies": { "express": "^4.19.2" }
}
EOF
cd api && npm install && cd ..

The worker

cat > worker/main.py <<'EOF'
import time
import requests
API_URL = "http://api:3001/sorted"
while True:
try:
r = requests.get(API_URL, timeout=5)
data = r.json()
print("original:", data["original"])
print("sorted: ", data["sorted"])
print("---")
except Exception as e:
print("worker error:", e)
time.sleep(5)
EOF
cat > worker/requirements.txt <<'EOF'
requests
EOF

The web page

cat > web/index.html <<'EOF'
<!doctype html>
<html>
<head><title>Sorting Demo</title></head>
<body>
<h1>Sorting Demo</h1>
<button onclick="loadData()">Generate & Sort</button>
<pre id="output"></pre>
<script>
async function loadData() {
const res = await fetch('http://localhost:3001/sorted');
const data = await res.json();
document.getElementById('output').textContent =
'Original: ' + JSON.stringify(data.original) + '\n' +
'Sorted: ' + JSON.stringify(data.sorted);
}
</script>
</body>
</html>
EOF

Dockerfiles

cat > api/Dockerfile <<'EOF'
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install --production
COPY . .
EXPOSE 3001
CMD ["npm", "start"]
EOF
cat > worker/Dockerfile <<'EOF'
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "main.py"]
EOF
cat > web/Dockerfile <<'EOF'
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
EOF

Compose

cat > docker-compose.yml <<'EOF'
services:
api:
build: ./api
ports:
- "3001:3001"
worker:
build: ./worker
web:
build: ./web
ports:
- "8080:80"
EOF

Run it:

docker compose up --build

Verify:

  • http://localhost:8080 shows the page.
  • curl http://localhost:3001/sorted returns sorted data.
  • Compose logs show the worker printing arrays.
Cloud operations checkpoint illustration for this section.

What I learned

The value was not the code. It was seeing three runtimes packaged, wired by service names, and launched with one command. Once you do that once, containerization stops feeling mysterious and starts feeling mechanical.

Common problems I hit:

  • Missing package-lock.json caused the API build to fail.
  • The web page hit a CORS error because the browser made the API call directly. I either disabled CORS for the demo or proxied through Nginx.
  • Port conflicts happened when another project was already on 3001 or 8080.

That is the normal debugging noise of multi-container setups. This lab gives you a small enough stack that the noise is manageable.