-
Notifications
You must be signed in to change notification settings - Fork 3
Week 7 Workshop
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.
Go to:
https://github.com/UoL-SoCS/CMP9134_2526_template
Click Use this template (top-right) → Create a new repository → choose yourself as owner, and name your repository → choose visibility private (advised to avoid any possible collusion risk of your work, but up to you) → click Create Repository
This gives you your own repository under your GitHub account, using the template.
All your work today (and possibly your assessment work?) should live in your own repository. This is your playground, start playing...
Open VSCode and clone your repository to your machine and open it.
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
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? You can also try GitHub Codespaces, which is basically VSCode in the cloud. A devcontainer allows you to define the working environment so a cloud "computer" is set up exactly with your development environments. The URLs will be something like
https://xxx-8000.app.github.devinstead oflocalhost. Use whatever URL VS Code gives you — the port numbers are the same.
Open a terminal OUTSIDE of the dev container (Terminal → New Terminal) and run:
docker ps(or simply check Docker Desktop on Windows)
You should see three services listed. Note their state — are they all running/healthy?
Now, VSCode, open each of these in your browser (use the Ports tab):
| URL | What you should see |
|---|---|
| Frontend | The basic dashboard — position, battery, status cards |
| Backend API | go to /docs, FastAPI interactive API docs (Swagger UI) |
| Robot API | go to /docs, Robot simulator API docs |
Checkpoint questions — answer these before moving on:
- How many Docker containers are running right now? Run
docker compose psto check. - Open the dashboard. Is the robot status showing as connected?
- Open the robot API docs. What endpoints does the robot expose? Does it meet your expectations.
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.ymlbackend/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. You have to use a search engine to do some research yourself!
Questions on docker-compose.yml:
-
There are three services:
robot-api,backend, andfrontend. How does the backend know the URL of the robot API? (Hint: look at theenvironment:section of the backend service.) -
The backend uses
depends_onwithcondition: service_started. The frontend usescondition: service_healthy. What is the difference, and why does the frontend need the stricter condition? -
The robot API exposes port 5000 — but there is no
ports:mapping for it indocker-compose.yml. How can the backend still reach it? -
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):
-
The Dockerfile has three stages:
base,development, andproduction. Which stage does the devcontainer use? Which does the CI pipeline use? (Hint: checkdocker-compose.devcontainer.yml,docker-compose.yml, and.github/workflows/ci.yml.) -
The production stage creates a non-root user called
gcs(seebackend/Dockerfile). Why is this good practice? -
The development stage defines the command to execute upon start with
CMD ["uvicorn", "main:app", ... "--reload"]. But in the dev container,uvicornis 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 --followLeave 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/statusAre the responses the same? Where does the backend get its data from?
(Check backend/robot_client.py and backend/main.py.)
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.
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
xandyfields) - 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.
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
RobotConnectionErroron any exception (match the pattern inget_status()) - Return the parsed JSON response
Do the same for reset() — it's a POST to /api/reset with no body.
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.)
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 --reloadLeave this running. You should see:
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process
Open http://localhost:8000/docs in your browser. You should now see:
GET /api/statusPOST /api/movePOST /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?
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 retryingWhy 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.
You have extended the backend. Now push your changes and watch the CI pipeline test them.
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 pushGo 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:
- The test job runs on two Python versions simultaneously. Why?
- The compose integration test hits
http://localhost:8080/health. Which service handles that request? Trace the path. - Do your new
/api/moveand/api/resetendpoints 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.
| 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.
# 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 downPorts (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.