Skip to content

Week 7 Workshop

Marc Hanheide edited this page Mar 19, 2026 · 6 revisions

CMP9134 — Week 7 Workshop: Containerisation

20 March 2026 · Prof. Marc Hanheide

You will spend this session getting a real multi-container application running, reading it to understand how the pieces fit together, and then extending it. By the end you will have implemented new robot control endpoints and watched your CI pipeline test them automatically.


Before you start

1. Fork the template repository

Go to:

https://github.com/UoL-SoCS/CMP9134_2526_template

Click Fork (top-right) → keep the defaults → Create fork.

This gives you your own copy of the repository under your GitHub account. All your assessment work should live in your fork.


2. Open in VS Code Dev Container

Clone your fork to your machine, then open the folder in VS Code:

File → Open Folder → select the cloned repo

VS Code will detect the .devcontainer/ folder and show a notification:

"Reopen in Container"

Click it. VS Code will:

  • Build the Docker images for the backend and frontend
  • Pull the pre-built robot simulator image
  • Start all three containers
  • Install all recommended extensions inside the container

This takes 2–4 minutes the first time. Grab a coffee.

If the notification doesn't appear: open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P) and run: Dev Containers: Reopen in Container


3. Find your forwarded ports

Once the dev container is running, click the Ports tab in the VS Code bottom panel (next to Terminal):

Label Port What it is
Frontend 80 The web dashboard — your main UI
Backend API (uvicorn) 8000 The FastAPI backend you will extend
Robot API (provided) 5000 The virtual robot simulator

VS Code adds a 🌐 icon next to each forwarded port. Click it to open that service in your browser.

On GitHub Codespaces? The URLs will be something like https://xxx-8000.app.github.dev instead of localhost. Use whatever URL VS Code gives you — the port numbers are the same.


Task 0 — Verify everything is running (~10 minutes)

Open a terminal inside the dev container (Terminal → New Terminal) and run:

docker compose ps

You should see three services listed. Note their state — are they all healthy?

Now open each of these in your browser (use the Ports tab):

URL What you should see
http://localhost:80 The GCS dashboard — position, battery, status cards
http://localhost:8000/docs FastAPI interactive API docs (Swagger UI)
http://localhost:5000/docs Robot simulator API docs

Checkpoint questions — answer these before moving on:

  1. How many Docker containers are running right now? Run docker compose ps to check.
  2. Open the dashboard at port 80. Is the robot status updating? How often?
  3. Open the robot API docs at port 5000. What endpoints does the robot expose?

Task 1 — Read the stack before you touch it (~20 minutes)

The template is deliberately annotated — every key line in docker-compose.yml, devcontainer.json, and the Dockerfile has a comment explaining what it does and why.

Open these files in VS Code and read through them:

  • docker-compose.yml
  • .devcontainer/devcontainer.json
  • .devcontainer/docker-compose.devcontainer.yml
  • backend/Dockerfile

As you read, answer the following questions. Write your answers in a scratch file or a comment — they will be useful when writing your assessment report.


Questions on docker-compose.yml:

  1. There are three services: robot-api, backend, and frontend. How does the backend know the URL of the robot API? (Hint: look at the environment: section of the backend service.)

  2. The backend uses depends_on with condition: service_started. The frontend uses condition: service_healthy. What is the difference, and why does the frontend need the stricter condition?

  3. The robot API exposes port 5000 — but there is no ports: mapping for it in docker-compose.yml. How can the backend still reach it?

  4. There is a commented-out database: service. What image would it use? What would you need to uncomment to make it persist data between restarts?


Questions on the Dockerfile (backend/Dockerfile):

  1. The Dockerfile has three stages: base, development, and production. Which stage does the dev container use? Which does the CI pipeline use? (Hint: check docker-compose.devcontainer.yml and .github/workflows/ci.yml.)

  2. The production stage creates a non-root user called gcs. Why is this good practice?

  3. The development stage starts with CMD ["uvicorn", "main:app", ... "--reload"]. But in the dev container, uvicorn is not running by default. What command overrides this, and why? (Hint: docker-compose.devcontainer.yml.)


Live exploration:

Watch the robot simulator logs in real time. In your terminal:

docker compose logs robot-api --follow

Leave this running for 30 seconds. You should see:

  • Normal requests coming in
  • Occasionally, a chaos monkey outage being triggered (503 responses)

Press Ctrl+C to stop.

Now trace a request manually. The dashboard at port 80 calls /api/status — but who actually handles that request?

# Call the backend directly (port 8000)
curl http://localhost:8000/api/status

# Call the robot directly (port 5000)
curl http://localhost:5000/api/status

Are the responses the same? Where does the backend get its data from? (Check backend/robot_client.py and backend/main.py.)


Task 2 — Extend the robot client (~30 minutes)

Open backend/robot_client.py.

The get_status() method is already implemented. The move() and reset() methods exist but raise NotImplementedError:

async def move(self, x: int, y: int) -> dict[str, Any]:
    raise NotImplementedError("Move command not implemented yet")

async def reset(self) -> dict[str, Any]:
    raise NotImplementedError("Reset command not implemented yet")

Your job is to implement them.


Step 2a — Check the robot API docs

Open http://localhost:5000/docs in your browser.

Find the POST /api/move endpoint. Click it → Try it out → fill in x and y values → Execute.

Note:

  • What is the request body format? (JSON with x and y fields)
  • What does a successful response look like?
  • What happens if you give coordinates outside the 0–20 grid?

Do the same for POST /api/reset.


Step 2b — Implement move()

In robot_client.py, replace the NotImplementedError in move() with a real implementation.

Model it on get_status() — it uses httpx.AsyncClient to send a GET request. move() should send a POST request to /api/move with a JSON body:

{"x": x, "y": y}

The httpx POST call looks like:

response = await client.post(url, json={"x": x, "y": y}, timeout=5.0)

Make sure to:

  • Raise RobotConnectionError on any exception (match the pattern in get_status())
  • Return the parsed JSON response

Step 2c — Implement reset()

Do the same for reset() — it's a POST to /api/reset with no body.


Step 2d — Add endpoints to main.py

Open backend/main.py. You can see the stub route skeletons in the comments at the bottom.

Add these two routes:

@app.post("/api/move")
async def move_robot(x: int, y: int):
    """Send a move command to the robot."""
    try:
        return await robot.move(x, y)
    except RobotConnectionError as exc:
        return {"error": str(exc)}


@app.post("/api/reset")
async def reset_robot():
    """Reset the robot simulation."""
    try:
        return await robot.reset()
    except RobotConnectionError as exc:
        return {"error": str(exc)}

Hot reload: Because the dev container runs uvicorn with --reload, your changes take effect as soon as you save the file. No restart needed.

(You will need to start uvicorn first — see below if you haven't already.)


Step 2e — Start the backend

The dev container keeps the container alive but does not start uvicorn automatically (so you can make changes before it starts). In your terminal:

cd /workspace/backend
uvicorn main:app --host 0.0.0.0 --port 8000 --reload

Leave this running. You should see:

INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO:     Started reloader process

Step 2f — Test your new endpoints

Open http://localhost:8000/docs in your browser. You should now see:

  • GET /api/status
  • POST /api/move
  • POST /api/reset

Test each one using the Try it out button in Swagger.

For /api/move, try:

  • Valid coordinates: x=5, y=3
  • Out-of-range coordinates: x=25, y=0
  • Watch the dashboard at port 80 — does the position update?

Stretch: Handle the chaos monkey

The robot has a chaos monkey — it randomly injects 503 errors and HTTP latency into every request. Your move() and reset() implementations will sometimes fail.

Add basic retry logic to robot_client.py:

import asyncio

async def move(self, x: int, y: int) -> dict[str, Any]:
    for attempt in range(3):           # try up to 3 times
        try:
            async with httpx.AsyncClient() as client:
                response = await client.post(
                    f"{self._base}/api/move",
                    json={"x": x, "y": y},
                    timeout=5.0,
                )
                response.raise_for_status()
                return response.json()
        except Exception as exc:
            if attempt == 2:           # last attempt — give up
                raise RobotConnectionError(f"Robot unreachable after 3 attempts: {exc}") from exc
            await asyncio.sleep(0.5)   # wait before retrying

Why does this matter? Your assessment brief explicitly requires robust error handling for the chaos monkey. The Critical Reflection section (S1, 20% of marks) expects you to discuss engineering decisions like this.


Task 3 — Commit and watch CI run (~15 minutes)

You have extended the backend. Now push your changes and watch the CI pipeline test them.

Step 3a — Commit your changes

In your terminal (you can open a second terminal tab):

cd /workspace
git add backend/robot_client.py backend/main.py
git commit -m "implement move and reset robot endpoints"
git push

Step 3b — Watch the Actions run

Go to your forked repository on GitHub → Actions tab.

You will see a workflow called CI — Build, Test & Push running. It has two jobs:

Job What it does
Test (Python 3.11 / 3.12) Runs pytest inside the container
Docker Compose integration Builds the full stack, starts it, and hits /health

Click into each job and read through the steps.

Questions:

  1. The test job runs on two Python versions simultaneously. Why?
  2. The compose integration test hits http://localhost:8080/health. Which service handles that request? Trace the path.
  3. Do your new /api/move and /api/reset endpoints get tested by the CI pipeline? If not, what kind of test would you write to cover them?

This last question is not rhetorical — the answer is what Week 8 (Testing & TDD) is about, and it is worth 20% of your assessment marks.


What to take away

Concept Where you saw it Assessment section
Multi-container orchestration docker-compose.yml S4 (20%)
Dev containers = reproducible environments .devcontainer/ S4 distinction
Multi-stage Docker builds backend/Dockerfile S4 — production vs dev
Services communicate by name, not IP ROBOT_API_URL=http://robot-api:5000 S1 (architecture discussion)
Chaos monkey → retry logic robot_client.py stretch task S1 (SE principles)
CI/CD pipeline runs tests in containers .github/workflows/ci.yml S4 (CI/CD evidence)
Healthchecks control startup order depends_on: condition: service_healthy S4

Your assessment requires docker-compose and containerisation as a marked criterion (20%). The template gives you the infrastructure — your job is to implement the application on top of it.


Quick reference

# Start the full stack
docker compose up --build

# Check running containers
docker compose ps

# Follow logs for a service
docker compose logs backend --follow
docker compose logs robot-api --follow

# Start the backend with hot reload (inside devcontainer)
cd /workspace/backend
uvicorn main:app --host 0.0.0.0 --port 8000 --reload

# Stop everything
docker compose down

Ports (inside the dev container):

Service Port URL
Frontend dashboard 80 http://localhost:80
Your FastAPI backend 8000 http://localhost:8000/docs
Robot simulator API 5000 http://localhost:5000/docs

Questions? Raise your hand or post in the module Teams channel.

Clone this wiki locally