In this workshop, You'll containerize a Node.js + Express web app backed by Postgres, orchestrate it with docker-compose, and wire up a GitHub Actions CI pipeline that builds your image, scans it with Trivy (Image Scanning tools), and pushes it to Docker Hub.
The app code under app/ is complete. Your job is to fill in the
#TODO(...) markers in the infrastructure files. Every TODO maps to a
checklist item in the Self-Verification section below — tick them off as you go.
Tick each box as you complete it. The workshop is done when all 10 are ticked.
- AC-01:
docker build -t workshop-app .exits with status 0 locally. - AC-02: Dockerfile uses a multi-stage build (
grep -c '^FROM' Dockerfile= 2). - AC-03:
.dockerignoreexcludes at leastnode_modules,.git, and.env. - AC-04: Dockerfile contains a
HEALTHCHECKinstruction. - AC-05:
docker compose up -dbrings up two healthy containers within 60 seconds. - AC-06:
webwaits fordbhealth (docker compose configshowscondition: service_healthy). - AC-07:
.github/workflows/ci.ymltriggers onpushtomain. - AC-08: The CI step
scanusesaquasecurity/trivy-actionwithseverity: CRITICALandexit-code: '1', and reports 0 CRITICAL findings. - AC-09: The CI step
pushpushes two tags::latestand:${{ github.sha }}. - AC-10: The
build-scan-pushjob goes green on your fork after Docker Hub secrets are configured.
TODO convention: look for
# TODO(step-N<letter>)(or// TODO(step-N<letter>)) comments inside the target file. Each step below tells you which file and which TODOs to complete.
- Fork this repo to your own GitHub account.
- Clone your fork locally and
cdinto it. - Copy
.env.exampleto.env.
Open app/src/index.js. Notice the three routes:
GET /→{ ok: true }GET /health→ 200 if it can reach Postgres, 503 otherwise (the Dockerfile HEALTHCHECK hits this)GET /tasks→ list from thetaskstable, seeded byapp/db/init.sql
You will not modify any file under app/.
Open .dockerignore and complete TODO(step-3). Required entries:
Verify (AC-03):
grep -E '^(node_modules|\.git|\.env)$' .dockerignoreShould print three matching lines.
Open Dockerfile and complete TODO(step-4a) through TODO(step-4g) (7 markers).
Pay special attention to step-4f — the HEALTHCHECK uses a Node one-liner
because node:20.11-slim does not include curl or wget.
Open docker-compose.yml and complete TODO(step-5a) through TODO(step-5h) (8 markers).
The key wiring: web depends_on db with condition: service_healthy, so the
app doesn't try to connect before Postgres is ready.
Verify (AC-05, AC-06):
docker compose up -d
docker compose ps
docker compose config | grep -A1 depends_onSmoke test (also confirms AC-04 at runtime):
docker inspect --format='{{.State.Health.Status}}' \
$(docker compose ps -q web) # should print: healthy
curl -fsS http://localhost:3000/health # should print {"status":"ok"}
curl -fsS http://localhost:3000/tasks # should list the seed taskOpen .github/workflows/ci.yml and complete TODO(step-6a) through TODO(step-6e) (5 markers).
Delete the workflow_dispatch: placeholder trigger after you add the real ones in step-6a.
Add secrets to your fork before pushing:
- GitHub → your fork → Settings → Secrets and variables → Actions → New repository secret
- Add
DOCKERHUB_USERNAME(your Docker Hub username) - Add
DOCKERHUB_TOKEN(the Personal Access Token from Prerequisites)
Verify (AC-07..AC-10):
git add .
git commit -m "complete docker workshop"
git push
# Then open the "Actions" tab on your fork and watch the job run.When build-scan-push goes green, your image is live on Docker Hub at
docker.io/<your-username>/workshop-app:latest and :${{ github.sha }}.
.
├── README.md # this file
├── .gitignore
├── .dockerignore # 1 TODO
├── .env.example
├── Dockerfile # 7 TODOs (step-4a..g)
├── docker-compose.yml # 8 TODOs (step-5a..h)
├── .github/workflows/ci.yml # 5 TODOs (step-6a..e)
└── app/
├── package.json # complete — do not edit
├── package-lock.json # complete — do not edit
├── db/init.sql # complete — do not edit
└── src/
├── index.js # complete — do not edit
└── db.js # complete — do not edit
Use these commands to pull a classmate's published image from Docker Hub and verify it works end-to-end.
# 1. Pull your classmate's image from Docker Hub
docker pull <their-dockerhub-username>/workshop-app:latest
# 2. Create an isolated network and start a temporary Postgres container
docker network create peer-test
docker run -d \
--name peer-db \
--network peer-test \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=workshop \
postgres:16-alpine
# 3. Run their app connected to the database
docker run -d \
--name peer-app \
--network peer-test \
-p 3000:3000 \
-e DATABASE_URL=postgres://postgres:postgres@peer-db:5432/workshop \
<their-dockerhub-username>/workshop-app:latest
# 4. Wait ~10 seconds for Postgres to initialize, then verify
curl -fsS http://localhost:3000/health # expected: {"status":"ok"}
# 5. Clean up when done
docker rm -f peer-app peer-db
docker network rm peer-testGood luck!