Docker Multilang: One Compose File, Three Runtimes
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
| Service | Runtime | Purpose | Port |
|---|---|---|---|
| API | Node + Express | Returns random and sorted numbers | 3001 |
| Worker | Python | Polls the API and logs results | none |
| Web | Nginx | Serves a static page that displays data | 8080 |
The API
mkdir -p multilang/api multilang/worker multilang/webcd multilangcat > 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"));EOFcat > api/package.json <<'EOF'{"name": "api","version": "1.0.0","main": "index.js","scripts": { "start": "node index.js" },"dependencies": { "express": "^4.19.2" }}EOFcd api && npm install && cd ..
The worker
cat > worker/main.py <<'EOF'import timeimport requestsAPI_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)EOFcat > worker/requirements.txt <<'EOF'requestsEOF
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-alpineWORKDIR /appCOPY package.json package-lock.json ./RUN npm install --productionCOPY . .EXPOSE 3001CMD ["npm", "start"]EOFcat > worker/Dockerfile <<'EOF'FROM python:3.11-slimWORKDIR /appCOPY requirements.txt ./RUN pip install -r requirements.txtCOPY . .CMD ["python", "main.py"]EOFcat > web/Dockerfile <<'EOF'FROM nginx:alpineCOPY index.html /usr/share/nginx/html/index.htmlEOF
Compose
cat > docker-compose.yml <<'EOF'services:api:build: ./apiports:- "3001:3001"worker:build: ./workerweb:build: ./webports:- "8080:80"EOF
Run it:
docker compose up --build
Verify:
http://localhost:8080shows the page.curl http://localhost:3001/sortedreturns sorted data.- Compose logs show the worker printing arrays.
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.jsoncaused 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.