A practical, self-contained reference for operating a small containerized
service end to end. It packages a tiny HTTP service and shows the pieces that
usually surround one in production: a hardened container image, a multi-service
docker compose stack behind an nginx reverse proxy, GitHub Actions CI/CD,
Terraform infrastructure-as-code, and a set of robust automation scripts.
Everything here runs locally and needs no cloud credentials.
┌──────────────────────────────────────────┐
│ docker compose │
│ │
HTTP :8080 │ edge network backend network │
───────────────► ┌───────────────┐ ┌───────────────┐ │
client / curl │ nginx │ :8000 │ app (Flask │ │
│ reverse proxy ├───────►│ + gunicorn) │ │
└───────────────┘ └───────┬───────┘ │
│ │ :6379 │
│ ┌───────▼───────┐ │
│ │ redis │ │
│ │ (+ volume) │ │
│ └───────────────┘ │
└──────────────────────────────────────────┘
app endpoints: GET / GET /health GET /metrics
- app — Flask service run under gunicorn, only reachable on the internal networks (no published port of its own).
- nginx — the single public entry point (
:8080), reverse-proxying to the app and exposing/,/health, and/metrics. - redis — an optional backing store with a persistent named volume, used to demonstrate healthcheck ordering and volume backups.
| Path | What it contains |
|---|---|
app/ |
Flask service, requirements*.txt, and the pytest suite. |
docker/Dockerfile |
Multi-stage build: dependency builder + slim, non-root runtime. |
.dockerignore |
Keeps the build context (and image) small. |
docker-compose.yml |
app + nginx + redis, with healthchecks, networks, and a volume. |
nginx/nginx.conf |
Reverse-proxy configuration for the app. |
.github/workflows/ci.yml |
Lint (ruff) + test (pytest) + Docker build on push/PR. |
.github/workflows/docker-publish.yml |
Build & push the image to GHCR on v* tags. |
terraform/ |
Cloud-free IaC (null + local providers) with variables, a module, outputs. |
scripts/ |
bootstrap.sh, deploy.sh, healthcheck.sh, backup.sh. |
Makefile |
Shortcuts for every common task (make help). |
- Docker with the Compose plugin (
docker compose) — for the container stack. - Python 3.11+ — for running/testing the app outside a container.
- Terraform 1.3+ — optional, only for the IaC workflow.
- make, curl, bash — used by the helper targets and scripts.
make install # create .venv and install dev deps
make test # run the pytest suite
make lint # run ruff
source .venv/bin/activate
python app/app.py # serve on http://localhost:8000make deploy # build images, start the stack, wait for health
curl http://localhost:8080/
curl http://localhost:8080/health
curl http://localhost:8080/metrics
make down # tear everything downmake deploy wraps scripts/deploy.sh, which builds the images, starts the
stack, and polls the nginx health endpoint until it is ready.
A minimal Flask app (app/app.py) built with an application factory so tests
can spin up isolated instances. Endpoints:
GET /— service metadata (name, version, available endpoints).GET /health— liveness probe returning status and uptime.GET /metrics— Prometheus-formatted counters and gauges.
In containers it runs under gunicorn with two workers; the image includes a
HEALTHCHECK that hits /health.
ci.yml runs on every push and pull request to main:
- Lint —
ruff checkover the app package. - Test —
pytest. - Docker build — builds the image from
docker/Dockerfile(without pushing) using Buildx and the GitHub Actions cache, gated on lint+test.
docker-publish.yml runs when a v* tag is pushed:
- Logs in to GHCR with the built-in
GITHUB_TOKEN. - Derives tags from the version with
docker/metadata-action({{version}},{{major}}.{{minor}}, and the commit SHA). - Builds and pushes to
ghcr.io/<owner>/<repo>.
git tag v1.0.0
git push origin v1.0.0 # triggers the GHCR publish workflowThe terraform/ directory is a fully self-contained configuration using only
the null and local providers, so it runs anywhere without cloud
credentials. It demonstrates the core Terraform building blocks:
- variables — project name, environment list, image, and a
replicasmap. - a module (
modules/render) — invoked once per environment withfor_each, rendering a per-environment JSON manifest and running a scopedlocal-execprovisioner. - resources —
local_file(rendered manifests + summary) andnull_resource(simulated provisioning keyed on content checksums). - outputs — manifest paths, the summary path, and total replica count.
make tf-init # terraform -chdir=terraform init -backend=false
make tf-validate # terraform -chdir=terraform validate
make tf-plan # preview the rendered filesApplying it writes manifests into terraform/generated/ (git-ignored).
All scripts use set -euo pipefail and resolve their own paths, so they can be
run from anywhere.
scripts/bootstrap.sh— create the venv, install dev deps, check tooling.scripts/deploy.sh— build + start the stack and verify health (--no-buildto skip the build).scripts/healthcheck.sh— poll an endpoint until it returns HTTP 200.scripts/backup.sh— snapshot the redis volume into a timestamped tarball.
Released under the MIT License. Copyright (c) 2026 mooceanstudio.