Docker and Kubernetes course menu
Docker Project: Notes App with Nginx, API and PostgreSQL
Build a complete three-service app with Docker Compose: an Nginx reverse proxy, a Python API, PostgreSQL with a volume, health checks, a private network and a non-root user.
On this page
In this project you will build a small but realistic system of three containers:
Browser --> nginx :8080 --> api :8000 --> db :5432
(public) (private) (private)Only Nginx is reachable from outside. The API and database are on a private network, so this project makes a good portfolio piece.
Step 1: Project layout#
mkdir notes-app
cd notes-app
mkdir api web
cat > .env <<'EOF'
POSTGRES_USER=notes
POSTGRES_PASSWORD=change-me-locally
POSTGRES_DB=notes
EOF
cat > .gitignore <<'EOF'
.env
EOF
sed 's/=.*/=/' .env > .env.example
cat .env.examplePOSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=Step 2: The API#
cat > api/app.py <<'EOF'
import os
import psycopg
from flask import Flask, jsonify, request
app = Flask(__name__)
DB_URL = os.environ["DATABASE_URL"]
def init_db():
with psycopg.connect(DB_URL) as conn:
conn.execute("CREATE TABLE IF NOT EXISTS notes (id serial PRIMARY KEY, body text NOT NULL)")
init_db()
@app.get("/api/health")
def health():
with psycopg.connect(DB_URL) as conn:
conn.execute("SELECT 1")
return jsonify(status="ok")
@app.get("/api/notes")
def list_notes():
with psycopg.connect(DB_URL) as conn:
rows = conn.execute("SELECT id, body FROM notes ORDER BY id").fetchall()
return jsonify([{"id": i, "body": b} for i, b in rows])
@app.post("/api/notes")
def add_note():
body = (request.get_json(silent=True) or {}).get("body", "").strip()
if not body:
return jsonify(error="body is required"), 400
with psycopg.connect(DB_URL) as conn:
row = conn.execute("INSERT INTO notes (body) VALUES (%s) RETURNING id", (body,)).fetchone()
return jsonify(id=row[0], body=body), 201
EOF
cat > api/requirements.txt <<'EOF'
flask==3.1.*
gunicorn==23.*
psycopg[binary]==3.2.*
EOF
cat > api/Dockerfile <<'EOF'
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
RUN useradd --create-home app
USER app
EXPOSE 8000
HEALTHCHECK --interval=5s --timeout=3s --retries=5 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health')"
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "app:app"]
EOFStep 3: The web server#
Nginx serves a static page, and passes /api/ to the API container, which is called a reverse proxy:
cat > web/index.html <<'EOF'
<!doctype html>
<title>Notes</title>
<h1>Notes</h1>
<ul id="notes"></ul>
<script>
fetch("/api/notes").then((r) => r.json()).then((notes) => {
document.getElementById("notes").innerHTML =
notes.map((n) => `<li>${n.body.replace(/</g, "<")}</li>`).join("");
});
</script>
EOF
cat > web/nginx.conf <<'EOF'
server {
listen 80;
root /usr/share/nginx/html;
location /api/ {
proxy_pass http://api:8000;
proxy_set_header Host $host;
}
}
EOF
cat > web/Dockerfile <<'EOF'
FROM nginx:1.30-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html/index.html
EOFhttp://api:8000 uses the Compose service name.
Step 4: compose.yaml#
Two networks keep the database away from Nginx:
cat > compose.yaml <<'EOF'
services:
web:
build: ./web
ports:
- "8080:80"
depends_on:
api:
condition: service_healthy
networks: [front]
restart: unless-stopped
api:
build: ./api
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
depends_on:
db:
condition: service_healthy
networks: [front, back]
mem_limit: 256m
restart: unless-stopped
db:
image: postgres:17-alpine
env_file: .env
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
interval: 2s
timeout: 3s
retries: 15
networks: [back]
restart: unless-stopped
networks:
front:
back:
volumes:
db-data:
EOF
docker compose config --services | sortapi
db
webStep 5: Start it#
--wait blocks until every service is healthy:
docker compose up -d --build --wait --quiet-pull > /dev/null 2>&1
docker compose ps --format '{{.Service}} {{.Health}}' | sortapi healthy
db healthy
webStep 6: Use it#
curl -s -X POST http://localhost:8080/api/notes -H "Content-Type: application/json" -d '{"body": "Learn Docker"}'
curl -s -X POST http://localhost:8080/api/notes -H "Content-Type: application/json" -d '{"body": "Build a project"}'
curl -s http://localhost:8080/api/notes
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:8080/api/notes -H "Content-Type: application/json" -d '{}'
curl -s http://localhost:8080/ | grep -o '<h1>.*</h1>'{"body":"Learn Docker","id":1}
{"body":"Build a project","id":2}
[{"body":"Learn Docker","id":1},{"body":"Build a project","id":2}]
400
<h1>Notes</h1>Step 7: Check the isolation#
The API runs as a normal user, and Nginx cannot reach the database:
docker compose exec api whoami
docker compose exec web sh -c 'nslookup db > /dev/null 2>&1 && echo "web can see db" || echo "web cannot see db"'
curl -s --max-time 2 http://localhost:5432 > /dev/null || echo "Database port is closed to the host"app
web cannot see db
Database port is closed to the hostStep 8: Check that data survives#
docker compose down > /dev/null 2>&1
docker compose up -d --wait > /dev/null 2>&1
curl -s http://localhost:8080/api/notes
docker compose down -v > /dev/null 2>&1[{"body":"Learn Docker","id":1},{"body":"Build a project","id":2}]Extend the project#
- Add GitHub Actions to build and push the images.
- Add Redis to cache the list of notes.
- Run the API with several replicas:
docker compose up -d --scale api=3. - Add a
DELETEroute, tests, and a nightlypg_dumpbackup. - Deploy it to a cheap server, and add HTTPS.
- Convert it to Kubernetes with Deployments and Services.
Put it on your resume#
Publish it on GitHub, with a README that shows the diagram above and one start-up command. You can honestly describe it as: "Containerised a three-tier app with Docker Compose, using health checks, a private database network and a non-root user."
Key takeaway#
Next: Docker interview questions.
Finished this lesson?
Related lessons
Recommended Docker and Kubernetes resources
- Docker Deep Dive by Nigel Poulton
A practical guide to how Docker works, for readers who have finished the basics.
Some links are affiliate links. If you buy through them, Laviyx Learn may earn a commission at no extra cost to you. As an Amazon Associate, we earn from qualifying purchases.