TL;DR — This guide walks you through building a minimal, production‑ready micro‑service (Flask + Docker + GitHub Actions) that you can ship today. It demonstrates API design, containerisation, automated testing, and basic observability—exactly the kind of hands‑on work hiring managers look for in a portfolio project.
Building a personal portfolio piece that feels like real work is one of the most effective ways to signal engineering competence without needing a full‑time job. Hiring managers on LinkedIn and in technical interviews want to see that you can take an idea, package it, ship it, and iterate on it. The project we’ll create is a tiny but complete micro‑service: a JSON‑based “Hello, World” API with a health endpoint, containerised with Docker, CI‑tested via GitHub Actions, and instrumented with simple metrics. Along the way you’ll practice version‑controlled code, container best practices, and basic observability—skills that translate directly to roles in backend engineering, DevOps, and platform teams.
Why This Project Stands Out on a CV
- API design & HTTP semantics – You’ll define routes, request/response models, and status codes, showing you can build consumable services.
- Containerisation – Dockerising the app demonstrates knowledge of isolated environments, multi‑stage builds, and reproducible deployments.
- CI/CD pipeline – GitHub Actions workflows that run tests, lint, and build images prove you can automate quality gates.
- Observability basics – Adding a health check and Prometheus‑compatible metrics gives you a foothold into monitoring patterns used in production.
- Git‑first workflow – Using pull requests, code review, and branch protection mirrors industry‑standard collaboration.
The project signals readiness for backend, full‑stack, or platform engineering roles where you’ll be expected to ship code, keep it running, and instrument it for health checks.
Architecture Overview
The system consists of a single Python process running Flask, wrapped by Docker, with optional health‑check and metrics endpoints. A minimal GitHub Actions pipeline builds, tests, and pushes the image.
+----------------+ +-------------------+ +-------------------+
| Developer | ---> | GitHub repo | ---> | Docker image |
| (code, PRs) | | (source) | | (build, push) |
+----------------+ +-------------------+ +-------------------+
|
v
+----------------+ +-------------------+ +-------------------+
| Docker run | ---> | Flask API | ---> | Health / Metrics |
| (container) | | (Python) | | endpoints |
+----------------+ +-------------------+ +-------------------+
- Flask app (
app.py) – Exposes/(welcome) and/healthz(simple 200 OK). - Dockerfile – Multi‑stage build: install dependencies, copy source, run with
gunicornfor modest concurrency. - GitHub Actions – Lint (
flake8), test (pytest), build image, push to Docker Hub (or GitHub Packages). - Health endpoint – Used by orchestrators (Kubernetes, Docker Compose) to verify liveness.
This one‑component architecture is deliberately simple, yet each piece mirrors patterns you’ll encounter in larger systems.
Building It Step by Step
Step 1 – Initialise the repository
mkdir portfolio-product && cd portfolio-product
git init
Create a README.md with a brief description; this will be the first file hiring managers see.
Step 2 – Write the Flask application (app.py)
"""Minimal Flask micro‑service for a portfolio project."""
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/", methods=["GET"])
def index():
return jsonify({"message": "Hello, portfolio!"}), 200
@app.route("/healthz", methods=["GET"])
def health():
"""Kubernetes‑style health check."""
return "", 200
if __name__ == "__main__":
# Development server; production uses gunicorn
app.run(host="0.0.0.0", port=8080)
Why it matters: The code shows proper JSON responses, status codes, and a health endpoint that container orchestrators expect.
Step 3 – Add a requirements.txt
Flask==2.3.3
gunicorn==21.2.0
pytest==7.4.3
flake8==4.0.1
These pins ensure reproducible builds.
Step 4 – Write tests (tests/test_app.py)
import pytest
from app import app
client = app.test_client()
def test_index():
resp = client.get("/")
assert resp.status_code == 200
data = resp.get_json()
assert data == {"message": "Hello, portfolio!"}
def test_health():
resp = client.get("/healthz")
assert resp.status_code == 200
Why it matters: Automated tests give you confidence that changes don’t break the API—exactly what a CI pipeline validates.
Step 5 – Create a Dockerfile
# syntax=docker/dockerfile:1
FROM python:3.11-slim AS builder
# Install dependencies in a virtual env
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy source code
COPY . .
# Production stage: use gunicorn for concurrency
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /app /app
ENV PYTHONUNBUFFERED=1
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8080", "app:app"]
Why it matters: Multi‑stage builds keep the final image lean, and gunicorn illustrates production‑grade WSGI serving.
Step 6 – Add a .github/workflows/ci.yml for CI/CD
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test-and-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with flake8
run: |
flake8 . --count --show --select=E9,F6,F8,W501
- name: Run tests
run: |
pytest
- name: Build Docker image
run: |
docker build -t your-docker-username/portfolio-product:{{ github.sha }} .
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: your-docker-username
password: ${{ secrets.DOCKER_TOKEN }}
- name: Push Docker image
run: |
docker push your-docker-username/portfolio-product:{{ github.sha }}
Why it matters: A single YAML file automates linting, testing, and container publishing—exactly the pipeline expectations for junior‑to‑mid level roles.
Step 7 – Run locally
docker build -t portfolio-product .
docker run -p 8080:8080 portfolio-product
Visit http://localhost:8080/ and http://localhost:8080/healthz to verify both endpoints return the expected JSON and 200 OK.
Running and Testing It
Local Python run (for quick iteration):
python -m pip install -r requirements.txt python app.pyThen curl the endpoints:
curl http://127.0.0.1:8080/ curl http://127.0.0.1:8080/healthzDocker run (full container experience):
docker run -d -p 8080:8080 portfolio-product curl http://localhost:8080/healthz # should return 200 with no bodyGitHub Actions – Push to
main; the workflow will run lint, tests, and build the image. Check the “Actions” tab for pass/fail status.Verification – After the workflow succeeds, pull the image and run it to confirm the same endpoints work in the container.
Extending It: Your Roadmap to Senior‑Level
- Add persistence with SQLite or PostgreSQL – Store user‑generated data and demonstrate ORM usage (e.g., SQLAlchemy). Matters because real services almost always need a data layer.
- Instrument with OpenTelemetry – Export traces and metrics to a local Jaeger or Prometheus instance. Matters for observability in microservice environments.
- Deploy to a cloud platform – Use GCP Cloud Run, AWS Fargate, or Azure Container Apps with a managed database. Matters to show you can move from local to production.
- Implement rate limiting & authentication – Employ Flask‑Limiter and JWT validation. Matters for production‑grade API security.
- Add a CI gate for security scanning – Integrate
banditfor Python security issues andtrivyfor container vulnerability checks. Matters because supply‑chain security is a hiring priority. - Write benchmarking scripts – Use
locustorwrkto measure throughput and latency under load. Matters to quantify performance and discuss scaling trade‑offs.
Each upgrade follows a clear, one‑line rationale, turning the toy project into a portfolio piece that mirrors the complexity of senior‑level engineering work.
Key Takeaways
- A minimal, containerised micro‑service is a concrete way to demonstrate API design, DevOps pipelines, and observability.
- Docker multi‑stage builds and GitHub Actions CI/CD are industry‑standard patterns you can list on a CV.
- Health endpoints and basic metrics give you a foothold into monitoring systems used in production.
- Incremental upgrades (persistence, tracing, cloud deployment) map directly to senior‑level responsibilities.
- Real, runnable code (Flask, Python, Docker, YAML) makes the project interview‑ready and instantly verifiable.
Further Reading
- Flask documentation – Quickstart – the canonical guide for building the app.
- Dockerfile reference – explains the multi‑stage build pattern used.
- GitHub Actions workflow syntax – for customising the CI pipeline.
- OpenTelemetry Python client – the primary source for adding tracing and metrics.
- Prometheus node exporter metrics – if you want to expose the health endpoint as a metric.
- SQLAlchemy ORM documentation – for the persistence upgrade path.