DOCKER
DEVOPS FOUNDATIONS
Badr Tajini · ESILV · 2026–2027
⌘
My Laptop / local terminal
Execution guidance

Type in your project folder on your laptop. Open its local browser. Docker Desktop uses a local Linux VM; no SSH is needed.

Environment setup ↗

✓ visited · ● current · ○ unvisited
Navigation only, not assessment.

TD06 · My Laptop

01 / WHERE AM I?

TD6 — Release an exact artifact and practise rollback

Duration: 180 minutes. Assessment: TD6 integrates the Final Docker Delivery Pack; it has no separate course-grade weight.

Where are we in the project?

TD1–TD5 produced one working API image, private PostgreSQL and proxy stack, persistent data, recovery tests, hardening and an exact-digest security review. TD6 uses the same repository and Compose stack to produce a new release. After this lab, the final project is the integrated form of those same files and results.

This stage adds: An immutable release, smoke tests and a known-good way back.

TD06 · My Laptop

02 / TODAY'S MISSION

Today's mission

Run source and policy tests, build and publish one release candidate from one full Git SHA, scan and approve its exact digest D, deploy D by pulling it on your Docker host, and prove a safe regression fails before rollback to known-good digest P and passes afterward.

TD06 · My Laptop

03 / FINAL ARCHITECTURE IMAGE

CI builds once and publishes digest D. A human approves that digest; the target pulls and tests it. Passing tests retain D. Failure restores and verifies previous known-good digest P. Application rollback does not rewind database data.
TD6 / Forward by digest, back by proof

TD06 · My Laptop

04 / MY ENVIRONMENT

Before you start

Before you start

Work from your private project repository based on the student starter. It should contain your TD5 Dockerfile, Compose model, app, database and proxy files, tests, and starter runbook/CI files that you must complete. Use a private GitHub repository with Actions and GHCR package access. First make sure the TD5 stack passes smoke and regression and runs an exact ghcr.io image digest.

My Laptop: run Bash locally or in Ubuntu WSL connected to Docker Desktop. My Own Cloud: SSH to your Ubuntu Docker host and run the same project work there; view the loopback proxy through a separate laptop SSH tunnel. Hades is reference only. CI needs GitHub Actions/GHCR. A private package pull may require a short-lived token with package access. If a platform, permission or independent reviewer is unavailable, record BLOCKED at that gate; do not invent a successful run or approval.

Keep your terminal open. Later steps use saved D/P and source SHA values. Never copy credentials into evidence.

The starter operational scripts stop with exit 64 until you implement their marked tasks. Complete your regression and runtime-contract checks from the published lab requirements; they must fail on a mismatched identity or a broken required behavior. A supplied stub is not an executed check.

My Laptop

Type in your local Bash terminal. Docker runs on your laptop or inside Docker Desktop’s Linux VM. Open browser checks on your laptop.

My Own Cloud

After SSH, type Docker commands in your own Ubuntu server’s terminal. For browser checks, use a separate laptop terminal for the SSH tunnel. The server’s localhost and your laptop’s localhost are different.

Check my setup and tunnel instructions

TD06 · My Laptop

05 / STEP-BY-STEP WORK

05 / STEP-BY-STEP WORK

Do the work, one step at a time.

Follow the commands in order. Keep the same terminal open so values from earlier steps remain available.

0/5steps self-marked
Stored only in this browser; no Docker or grading check runs here.
STEP 01 / 05

Step 1 — Record the known-good release and finish safe runbooks · 25 minutes

Why: Rollback needs a specific previous digest P and a script that cannot accept a mutable tag. Capture the currently running API reference before any candidate is deployed.

set -euo pipefail
. scripts/course-env.sh
set -a; . ./release.env; set +a
mkdir -p evidence/TD06
previous_image="$(docker inspect --format '{{.Config.Image}}' \
  "$(docker compose -p docker-delivery ps -q delivery-api)")"
case "$previous_image" in ghcr.io/*@sha256:*) ;; *) echo 'BLOCKED: P is not a GHCR digest'; exit 1;; esac
printf 'captured_at_utc=%s\nprevious_image=%s\n' \
  "$(date -u +%FT%TZ)" "$previous_image" > evidence/TD06/previous-release.txt
bash scripts/smoke.sh | tee evidence/TD06/before-smoke.txt
bash scripts/regression.sh | tee evidence/TD06/before-regression.txt
bash -n scripts/deploy.sh scripts/rollback.sh
set +e
bash scripts/deploy.sh delivery-api:mutable-tag > evidence/TD06/mutable-rejected.txt 2>&1
mutable_status=$?
set -e
printf 'exit_code=%s\n' "$mutable_status" >> evidence/TD06/mutable-rejected.txt
test "$mutable_status" -ne 0

Complete your starter scripts/deploy.sh and scripts/rollback.sh yourself. Their contract is: accept only an immutable GHCR digest, record current/previous/pending state on the deployment host, pull instead of build, verify the activated image and service behavior, recover to P after a failed candidate where possible, and preserve the database volume. An interrupted operation must leave a reviewable marker. Test rejection of a mutable tag before running a real deployment.

Expected and verify: P is a lowercase GHCR digest, baseline smoke/regression pass, and the tag rejection exits nonzero without changing the running image. Run docker inspect --format '{{.Config.Image}}' "$(docker compose -p docker-delivery ps -q delivery-api)" again to compare with P.

If it fails: Preserve the error and stop before release. A missing P or failed baseline cannot be repaired by guessing a tag.

STEP 02 / 05

Step 2 — Make CI validate and publish one candidate · 35 minutes

Why: A release digest must come from one known source revision. Complete your starter GitHub Actions workflow. Pull requests validate without package publication. A release-branch run validates source, builds/pushes one API candidate, then creates an SBOM, vulnerability result, digest and source-linked artifact. Give the publishing job package-write permission only where needed, pin third-party actions, and keep SSH/host deployment credentials out of CI.

Run local syntax and supplied-application checks before pushing your own completed workflow. These checks do not certify your delivery definitions: implement the workflow validation against the public safety requirements, and demonstrate that a deliberately violated requirement is rejected. No private grading validator is included:

python3 -m venv .venv
lab_python=.venv/bin/python
"$lab_python" -m pip install --require-hashes --requirement requirements-dev.txt
bash -n scripts/deploy.sh scripts/rollback.sh scripts/regression.sh scripts/verify_runtime_contract.sh
"$lab_python" -m compileall -q app tests
"$lab_python" -m pytest -q
docker compose --env-file .env.example -p docker-delivery config --quiet
git diff --check
git add .github/workflows/container-delivery.yml scripts/deploy.sh scripts/rollback.sh
git diff --cached --check
git commit -m 'td6: define immutable release workflow'
release_sha="$(git rev-parse HEAD)"
printf 'release_sha=%s
' "$release_sha" > evidence/TD06/release-source.txt
git push origin HEAD

Inspect the Actions run for this full source SHA. If a push does not start the workflow, check branch and path filters; if you change them, commit a new revision and use its new SHA. A green badge for another commit is not evidence. If your starter lacks a requirement or validator, record BLOCKED and use the published setup/help route; do not copy a private solution.

STEP 03 / 05

Step 3 — Inspect the CI artifact and obtain digest-specific approval · 35 minutes

Why: The reviewer approves what CI actually built, not a tag that may move. Audit one run ID and download its exact release artifact. Install/authenticate gh on a trusted terminal for your private repository; no token belongs in evidence.

run_id="$(gh run list --workflow container-delivery.yml --commit "$release_sha" \
  --limit 20 --json databaseId,headSha --jq '.[0].databaseId')"
test -n "$run_id"
gh run watch "$run_id" --exit-status
gh run view "$run_id" --json url,headSha,event,status,conclusion \
  > evidence/TD06/ci-run.json
python3 - "$release_sha" <<'PY'
import json, pathlib, sys
run = json.loads(pathlib.Path('evidence/TD06/ci-run.json').read_text())
if run.get('headSha') != sys.argv[1] or run.get('conclusion') != 'success':
    raise SystemExit('BLOCKED: wrong or unsuccessful CI run')
PY
mkdir -p evidence/TD06/ci
gh run download "$run_id" --name release --dir evidence/TD06/ci
(cd evidence/TD06/ci && sha256sum --check artifact-checksums.sha256)
python3 -m json.tool evidence/TD06/ci/sbom.cdx.json >/dev/null
python3 -m json.tool evidence/TD06/ci/vulnerabilities.json >/dev/null
candidate_image="$(sed -n 's/^image=//p' evidence/TD06/ci/release.env)"
artifact_sha="$(sed -n 's/^git_sha=//p' evidence/TD06/ci/release.env)"
case "$candidate_image" in ghcr.io/*@sha256:*) ;; *) echo 'BLOCKED: D is not a GHCR digest'; exit 1;; esac
test "$artifact_sha" = "$release_sha"
test "$candidate_image" != "$previous_image"
printf 'candidate=%s\nsource=%s\nrun_id=%s\n' \
  "$candidate_image" "$release_sha" "$run_id" > evidence/TD06/candidate.txt

Review the workflow URL in ci-run.json, job conclusions, scan policy result, CycloneDX file, vulnerability JSON, checksums, full source SHA and exact D. The workflow's vulnerability JSON is intentionally filtered to fixed High/Critical candidates; retain TD5's broader scan for context. A zero row in the filtered file is not proof of zero risk. If possible, independently inspect D with docker buildx imagetools inspect "$candidate_image" after registry authentication.

Ask an authorized reviewer other than the active deployer to record the decision for this exact D in evidence/TD06/APPROVAL.md:

candidate=<exact D>
source_sha=<40-character release SHA>
run_url=<actual Actions run URL>
previous=<exact P>
reviewer=<reviewer alias>
decided_at_utc=<actual UTC time>
decision=APPROVED or REJECTED
scan_disposition=<actual finding/review decision>
rollback_trigger=<failed smoke, version, critical CRUD/persistence test, or runtime contract>
reason=<specific reason>

Record the actual decision; the template is not an approval. If CI, GHCR, artifact or reviewer access is absent, write BLOCKED and stop before deployment.

Expected and verify: The run SHA equals release_sha, the artifact SHA equals it, checksums pass, and the approval names D and P exactly. Any mismatch or REJECTED decision blocks promotion.

If it fails: Reconcile the source, run and digest. Do not edit a downloaded artifact to make identities appear to match.

STEP 04 / 05

Step 4 — Deploy that digest and test the live service · 35 minutes

Why: The target host must pull the approved D. It must not rebuild. The same commands run on My Laptop's Docker host or your own cloud Docker host; only SSH entry and the browser tunnel differ.

First check the approval and configure temporary private-package access on the Docker host. Run the block in a private terminal with shell tracing off. The temporary Docker credential directory is removed when the shell exits; do not put a token in a file, log or command argument.

grep -Fx "candidate=$candidate_image" evidence/TD06/APPROVAL.md
grep -Fx "source_sha=$release_sha" evidence/TD06/APPROVAL.md
grep -Fx "previous=$previous_image" evidence/TD06/APPROVAL.md
grep -Fx 'decision=APPROVED' evidence/TD06/APPROVAL.md
set +x
ghcr_config="$(mktemp -d)"
chmod 700 "$ghcr_config"
export DOCKER_CONFIG="$ghcr_config"
trap 'docker logout ghcr.io >/dev/null 2>&1 || true; rm -f -- "$DOCKER_CONFIG/config.json"; rmdir -- "$DOCKER_CONFIG"' EXIT
read -r -p 'GitHub username with package access: ' GHCR_USERNAME
read -r -s -p 'GHCR read:packages token: ' ghcr_token
printf '\n'
printf '%s' "$ghcr_token" | docker login ghcr.io --username "$GHCR_USERNAME" --password-stdin
unset ghcr_token

If the package is public, a registry login may be unnecessary. For a private package, repository access alone is not package access. Stop as BLOCKED if login or pull is denied. Keep this terminal open through Step 5 so rollback can pull P if it is not cached.

If deploy/state/current-image does not yet exist, first establish P using --initialize after its TD5 checks and reviewer approval. This is a one-time state transition. If it exists, assert it already names P. Then deploy D:

if test -f deploy/state/current-image; then
  test "$(cat deploy/state/current-image)" = "$previous_image"
else
  bash scripts/deploy.sh --initialize "$previous_image" | tee evidence/TD06/initialize-previous.txt
fi
bash scripts/deploy.sh "$candidate_image" | tee evidence/TD06/deploy.txt
test "$(cat deploy/state/current-image)" = "$candidate_image"
active_image="$(docker inspect --format '{{.Config.Image}}' \
  "$(docker compose -p docker-delivery ps -q delivery-api)")"
test "$active_image" = "$candidate_image"
EXPECTED_GIT_SHA="$release_sha" bash scripts/smoke.sh | tee evidence/TD06/after-smoke.txt
DELIVERY_API_IMAGE="$candidate_image" EXPECTED_GIT_SHA="$release_sha" \
  bash scripts/regression.sh | tee evidence/TD06/after-regression.txt
test "$(docker inspect --format '{{.Config.Image}}' \
  "$(docker compose -p docker-delivery ps -q delivery-api)")" = "$candidate_image"
printf 'active_image=%s\n' "$candidate_image" > evidence/TD06/active-release.txt

The explicit DELIVERY_API_IMAGE=D on regression matters because that test recreates the API container. It prevents a stale release.env or shell variable from silently recreating P. The deploy script checks /version against D's OCI full revision and the target's host publication contract. Regression also creates a real item and verifies it survives API recreation. The PostgreSQL data and secret volumes stay in place.

Expected and verify: P is recorded as previous, D as current, the running container reports D, /version.git_sha is release_sha, and smoke plus critical CRUD/persistence regression pass. If any test fails, the runbook attempts bounded recovery to P and retains truthful state.

If it fails: Preserve the deploy log, active image and error. Check whether deploy/state/pending-operation exists before retrying. Never delete the database volume or manually call a candidate a success.

STEP 05 / 05

Step 5 — Inject a safe regression and execute rollback · 30 minutes

Why: The same committed test must fail while D is active and pass once P is active. The safe fixture changes the expected Git SHA, not application data or host exposure.

previous_sha="$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$previous_image")"
case "$previous_sha" in ????????????????????????????????????????) ;; *) echo 'BLOCKED: P lacks full OCI revision'; exit 1;; esac
test "$previous_sha" != "$release_sha"
set +e
DELIVERY_API_IMAGE="$candidate_image" EXPECTED_GIT_SHA="$previous_sha" \
  bash scripts/regression.sh > evidence/TD06/regression-before-rollback.txt 2>&1
regression_status=$?
set -e
printf 'exit_code=%s\n' "$regression_status" >> evidence/TD06/regression-before-rollback.txt
test "$regression_status" -ne 0
test "$(docker inspect --format '{{.Config.Image}}' \
  "$(docker compose -p docker-delivery ps -q delivery-api)")" = "$candidate_image"
bash scripts/rollback.sh | tee evidence/TD06/rollback.txt
test "$(cat deploy/state/current-image)" = "$previous_image"
EXPECTED_GIT_SHA="$previous_sha" bash scripts/smoke.sh | tee evidence/TD06/after-rollback-smoke.txt
DELIVERY_API_IMAGE="$previous_image" EXPECTED_GIT_SHA="$previous_sha" \
  bash scripts/regression.sh | tee evidence/TD06/regression-after-rollback.txt
rolled_back_image="$(docker inspect --format '{{.Config.Image}}' \
  "$(docker compose -p docker-delivery ps -q delivery-api)")"
test "$rolled_back_image" = "$previous_image"
printf 'rolled_back_image=%s\n' "$rolled_back_image" > evidence/TD06/rolled-back-release.txt

regression-before-rollback.txt must show the expected/observed SHA mismatch and nonzero exit code. regression-after-rollback.txt must show the same suite passing after P returns; rolled-back-release.txt must contain P. A restart of D is not rollback. Application image rollback does not restore PostgreSQL data; a real incompatible schema migration needs its own reviewed restore/migration plan and backup. Do not perform a database restore for this safe exercise.

Write evidence/TD06/OBSERVATIONS.md with actual UTC time, Docker version, release SHA, CI run URL and result, D, P, scan/SBOM checksums, reviewer decision, active image before/after, smoke and regression outputs and exit codes, rollback trigger, and remaining limitation. Use BLOCKED for any unexecuted gate. The active image after rollback has P's older OCI source SHA; the operational definition and evidence commit can have a newer SHA. Record both honestly.

If it fails: Keep both test outputs, state files and active image coordinate. Stop for diagnosis rather than trying a different digest or changing database data.

Open checkpoint →

TD06 · My Laptop

06 / LIVE CHECKPOINT

Run the source checks in your terminal, then compare your observations. Browser markers are your own record, not a verification result.

Check it

Run after Step 5's rollback on the deployment host. This verifies the active P and saved CI/release observations; it does not manufacture CI success or approval.

set -euo pipefail
. scripts/course-env.sh
active_image="$(cat deploy/state/current-image)"
previous_image="$(sed -n 's/^previous_image=//p' evidence/TD06/previous-release.txt)"
test "$active_image" = "$previous_image"
export DELIVERY_API_IMAGE="$active_image"
api_id="$(docker compose -p docker-delivery ps -q delivery-api)"
test "$(docker inspect --format '{{.Config.Image}}' "$api_id")" = "$active_image"
export EXPECTED_GIT_SHA="$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$active_image")"
bash scripts/smoke.sh
bash scripts/regression.sh
bash scripts/verify_runtime_contract.sh
(cd evidence/TD06/ci && sha256sum --check artifact-checksums.sha256)
grep -Fx 'decision=APPROVED' evidence/TD06/APPROVAL.md
grep -Eq '^exit_code=[1-9][0-9]*$' evidence/TD06/regression-before-rollback.txt
python3 - <<'CHECK'
import json
from pathlib import Path
p=Path('evidence/TD06')
run=json.loads((p/'ci-run.json').read_text())
sha=(p/'release-source.txt').read_text().strip().split('=',1)[1]
assert run['conclusion']=='success' and run['headSha']==sha
assert (p/'after-regression.txt').stat().st_size
assert (p/'regression-after-rollback.txt').stat().st_size
CHECK
printf 'PASS: active rollback target and recorded CI/release checks agree\n'

Expected: P is active, behavior passes, the saved run matches the release SHA, and the deliberate D test has a nonzero result. If an external gate was never run, stop with BLOCKED rather than creating its missing files.

Final pack handoff — after TD6

Use your own TD1–TD6 repository as the integrated file set and the student-visible Project brief for the submission contract. The final pack uses this repository's Dockerfile, hardened compose.yaml, proxy, database, tests, workflow and runbooks. Verify the currently active image and attach an SBOM for that same digest; after the guided rollback, that is normally TD5's P. If identities differ, stop and scan the actual active digest.

mkdir -p evidence/FINAL final-docker-delivery-pack/security
active_image="$(docker inspect --format '{{.Config.Image}}' \
  "$(docker compose -p docker-delivery ps -q delivery-api)")"
scanned_image="$(cat evidence/TD05/scanned-digest.txt)"
test "$active_image" = "$scanned_image"
install -m 0644 evidence/TD05/sbom.cdx.json final-docker-delivery-pack/security/sbom.cdx.json
python3 -m json.tool final-docker-delivery-pack/security/sbom.cdx.json >/dev/null
DELIVERY_API_IMAGE="$active_image" bash scripts/smoke.sh | tee evidence/FINAL/integrated-smoke.txt
DELIVERY_API_IMAGE="$active_image" bash scripts/regression.sh | tee evidence/FINAL/integrated-regression.txt
test "$(docker inspect --format '{{.Config.Image}}' \
  "$(docker compose -p docker-delivery ps -q delivery-api)")" = "$active_image"

Write final observations from the outputs actually obtained, including the active digest, source/definition SHAs, recovery status and one remaining limit. Review the generated final material before saving it. A package check is not an instructor grade or proof of unrun deployment.

Kubernetes handoff from your project

The digest becomes the workload image identity. Proxy/API/DB ports inform Service and Ingress design; /healthz and /readyz map to different probes. Config names and the secret-file path inform ConfigMap/Secret projection, while the PostgreSQL named volume and tested restore inform persistent-data design. UID, capabilities, read-only roots, resource limits and SIGTERM grace inform security and lifecycle settings. Compose depends_on does not become Kubernetes ordering or reconciliation. This is an architectural handoff only; no Kubernetes administration or manifests are part of TD6.

Final pack: use the project brief to assemble your own definitions, observations and defense evidence.

TD06 · My Laptop

07 / WHAT CHANGED IN MY PROJECT

What did we add to the project?

Before: the hardened stack with a tested digest and scan. This TD added: source-linked CI, human approval, immutable deployment and rollback. After: the verified previous digest P is active after the rollback exercise; D and its evidence are retained. Next: integrate these same artifacts for the final project and rehearse your individual explanation during 30 November–4 December 2026.

See the evolving project architecture ↗

TD06 · My Laptop

08 / ENGINEER MODE

REAL VPS CONTEXT / NO STUDENT ACCESS TO HADES. See how this concept looks on a real VPS; perform and justify the assessed work on your own machine.

Engineer Mode

On a VPS, GitHub Actions builds and publishes the image; the host only pulls an approved digest. Keep registry authentication temporary and host-local. Compare the active image, OCI revision, source SHA and HTTP version before claiming deployment. An interrupted operation requires review of saved state before retry. A rollback to the prior image does not restore database data. Hades is a reference context; no current CI-to-Hades release or rollback result is asserted.

TD06 · My Laptop

09 / HELP

I'm stuck

Symptom First check and correction Verify again
No release workflow run Compare pushed SHA, branch and workflow paths; push an actual build-affecting change or use the approved manual trigger Run headSha equals release SHA
PR appears able to publish Inspect job if and permissions; keep packages: write only in the publish job PR jobs end without package push
GHCR pull denied Confirm package visibility/access and a read:packages token; re-login on the actual Docker host docker pull D or runbook pull succeeds
Candidate starts but version mismatches Compare OCI revision, artifact SHA, running .Config.Image and /version All four identify D/source SHA
Rollback state is missing or pending Inspect deploy/state/ and preserve pending-operation; reconcile the exact P/D records before retry P active and same regression passes

Share only the command, redacted error, and My Laptop or My Own Cloud. Never share a registry token, secret, key or raw database dump.

Open full-resolution image · pinch to zoom ↗