Skip to content

Repository files navigation

Containerisation Exercises

Docker Docker Compose Kubernetes

These exercises accompany the Containerisation module of the Java, React, and MySQL full-stack bootcamp. You will package applications, run a multi-service stack, debug and shrink an image, and deploy to a local Kubernetes cluster.

There is one exercise per session:

  1. Introduction to Containerisation in dockerfile-react/
  2. Multi-Container Applications in compose-stack/
  3. Debugging and Optimisation in debug-optimise/
  4. Introduction to Orchestration in kubernetes-intro/

Work through them in order. Each exercise has its own directory with a starter project and a short brief below. Commit your work as you go. When you push to main, the autograder runs and reports a score on your feedback pull request.

Before you start

Open the repository in the provided dev container (or GitHub Codespaces). It includes Docker, the Docker CLI, kubectl, and k3d, so you do not need to install anything locally.

To check your tools are available:

docker --version
docker compose version
kubectl version --client
k3d version

The exercises publish services on ports 5173, 8080, and 8081. If something else is already using one of those, Docker refuses to start the container with port is already allocated, so stop whatever holds the port or publish on a different one.

How grading works

Each push to main triggers the shared autograding workflow. It runs the test suites it finds in tests/ and awards two marks out of five: Functionality (from the automated tests) and Code Quality (from an automated review of your work).

The tests for exercises 2, 3, and 4 are initially disabled, so that exercises you have not reached yet are not counted against you. Each one is a file in tests/ ending in .bats.disabled:

tests/compose-stack.bats.disabled
tests/debug-optimise.bats.disabled
tests/kubernetes-intro.bats.disabled

The workflow only runs files ending in .bats, so you enable the checks for an exercise checks by dropping the .disabled suffix. The instructions below include the command to rename the relevant file; ensure you run that before pushing and committing your work. The tests for Exercise 1 are enabled already.

IMPORTANT: Do not change the contents of any file in tests/ — renaming the files is the only edit you should make there.


📌 1. Introduction to Containerisation

Directory: dockerfile-react/

The starter is a plain Vite and React app. Your task is to write a Dockerfile that builds and serves it.

Steps

  1. Open dockerfile-react/ and read package.json to see how the app runs.
  2. Create a file named Dockerfile in dockerfile-react/.
  3. Start from a Node base image with FROM.
  4. Use WORKDIR, COPY, and RUN npm ci to install dependencies.
  5. Add a CMD that serves the app on port 5173 and binds to all interfaces (npm run preview -- --host 0.0.0.0). Build the app first so preview has something to serve.
  6. Expose the port with EXPOSE 5173.
  7. Build the image and tag it:
docker build -t dockerfile-react ./dockerfile-react
  1. Run the container in the background with port mapping, giving it a name so you can refer to it:
docker run -d --name react-app -p 5173:5173 dockerfile-react
  1. Read the logs to confirm the preview server started, then open localhost:5173 in your browser. Add -f to follow the output if the container is still starting up:
docker logs react-app
# or
docker logs -f react-app
  1. Stop and remove the container when you have finished:
docker stop react-app
docker rm react-app

Note that docker logs reads the output of a container that already exists. If the build itself fails, there is no container yet and the error is in the build output instead. You will practise diagnosing a container that fails to start in exercise 3.

What success looks like

  • docker build completes without errors.
  • The app responds on the mapped port when the container runs.
  • docker logs shows the preview server running and the address it is serving on.
  • Your Dockerfile uses FROM, COPY, RUN, and CMD.

📌 2. Multi-Container Applications

Directory: compose-stack/

The starter has a Spring Boot API in backend/ and a static frontend in frontend/. Your task is to write a Dockerfile for the backend and a docker-compose.yml that runs the frontend, the backend, and a MySQL database together.

Steps

  1. Write compose-stack/backend/Dockerfile for the Spring Boot app, using a multi-stage build: compile the jar with Maven in the first stage, then copy just the jar into a JRE image to run it with java -jar. Note that pom.xml sets <finalName>app</finalName>, so the jar Maven produces is app.jar.
  2. Write compose-stack/docker-compose.yml with three services named frontend, backend, and db.
  3. For frontend, serve the static files in frontend/ with the nginx:alpine image. Mount the directory into the image's web root (/usr/share/nginx/html) and publish it on port 8081. If 8081 is taken on your machine, make the mapping overridable with "${FRONTEND_PORT:-8081}:80" so you can run FRONTEND_PORT=8082 docker compose ... up instead of editing the file.
  4. Use the official mysql:8.4 image for db. Set the database name, user, and password with environment variables.
  5. Mount db/init.sql into /docker-entrypoint-initdb.d/ on the db service. MySQL runs the scripts it finds there the first time it starts, which creates the messages table and seeds a row.
  6. Configure the backend service to read its database connection from environment variables, and point it at the db service by name. The variables the app expects are SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, and SPRING_DATASOURCE_PASSWORD, and the host in the JDBC URL is the service name db.
  7. Add a named volume for the database so MySQL data survives a restart.
  8. Make the backend wait for the database. MySQL takes a few seconds to accept connections on first start, and the backend exits if it cannot connect, so add a healthcheck to db and a depends_on condition of service_healthy on backend.
  9. Bring the stack up and confirm the API responds. The first run has to download the Maven dependencies and build the jar, so allow a few minutes:
docker compose -f compose-stack/docker-compose.yml up --build
curl http://localhost:8080/api/messages

You should get back the row seeded by db/init.sql. Open localhost:8081 to see the frontend list it.

  1. Add a message of your own, so you can tell data you created from the seeded row:
curl -X POST -H 'Content-Type: application/json' \
  -d '{"body":"added before restart"}' http://localhost:8080/api/messages
  1. Tear the stack down and bring it back, then confirm both messages are still there:
docker compose -f compose-stack/docker-compose.yml down
docker compose -f compose-stack/docker-compose.yml up
curl http://localhost:8080/api/messages
  1. Enable the tests for this exercise, then commit and push your work:
git mv tests/compose-stack.bats.disabled tests/compose-stack.bats

What success looks like

  • docker compose config reports a valid file.
  • All three services are defined, with a named volume on the database.
  • The backend reads its database settings from environment variables.
  • curl http://localhost:8080/api/messages returns the seeded row, and the frontend page lists it.
  • Data survives a down and up cycle. Note that down -v removes the volume as well, which resets the database.

📌 3. Debugging and Optimisation

Directory: debug-optimise/

The starter includes a Spring Boot app and a Dockerfile that is both broken and much larger than it needs to be. Your task is to fix the fault and shrink the image.

Steps

  1. Build the provided image and record its size. The build itself succeeds, so this step should complete without errors:
docker build -t debug-optimise ./debug-optimise
docker images --filter reference=debug-optimise
  1. Run it, and notice that the container starts and then immediately stops:
docker run -d --name debug-optimise -p 8080:8080 debug-optimise
docker ps -a --filter name=debug-optimise
  1. Read the logs to find the cause, then fix it:
docker logs debug-optimise

The container exits within a second, so there is no running process for docker exec to attach to. If you want to look around the filesystem to confirm what the build actually produced, start a throwaway shell in the image instead:

docker run --rm -it --entrypoint sh debug-optimise
  1. Reorder the Dockerfile instructions so dependency layers are cached and only your code invalidates the cache.
  2. Switch to a smaller base image (a slim or alpine variant) and use a multi-stage build so the final image contains only what it needs to run.
  3. Add a .dockerignore file in debug-optimise/ to keep the build context small.
  4. Rebuild and run again, then confirm the container stays up and answers on the mapped port. The old container still holds the name, so remove it first:
docker rm debug-optimise
docker build -t debug-optimise ./debug-optimise
docker run -d --name debug-optimise -p 8080:8080 debug-optimise
curl http://localhost:8080/
  1. Compare before and after image sizes with docker images to confirm the optimisation worked.
  2. Enable the tests for this exercise, then commit and push your work:
git mv tests/debug-optimise.bats.disabled tests/debug-optimise.bats

What success looks like

  • The image builds and the container starts cleanly.
  • The final image is meaningfully smaller than the original, and under 300MB.
  • A .dockerignore file is present and excludes build output.

📌 4. Introduction to Orchestration

Directory: kubernetes-intro/

Your task is to write Kubernetes manifests that deploy a container image and expose it inside the cluster.

Steps

  1. Start a local cluster:
k3d cluster create dev
  1. Write kubernetes-intro/deployment.yaml for a Deployment named api with 2 replicas. Give the Pods the label app: api and use the image traefik/whoami:latest on container port 8080. That image echoes back details of each request it receives, which makes it easy to see which Pod answered. It listens on port 80 unless you tell it otherwise, so set the environment variable WHOAMI_PORT_NUMBER to 8080:
env:
  - name: WHOAMI_PORT_NUMBER
    value: 8080
  1. Write kubernetes-intro/service.yaml for a Service named api that selects app: api and exposes port 80 targeting container port 8080.
  2. Apply both manifests and confirm both Pods are running:
kubectl apply -f kubernetes-intro/
kubectl rollout status deployment/api
kubectl get pods
  1. Check that the Service reaches the Pods by name from inside the cluster. whoami reports the hostname of the Pod that answered, so sending a batch of requests shows the Service spreading them across both replicas:
kubectl run curl-test --rm -i --restart=Never --image=curlimages/curl --command -- \
  sh -c 'for i in $(seq 10); do curl -s http://api/ | grep Hostname; done'

Two notes on the output. The Service picks a Pod at random for each connection rather than strictly alternating, so several identical hostnames in a row are normal and an uneven split such as six and four does not mean anything is misconfigured; across ten requests you should simply see both Pod names. You will also see a warning about credentials being recorded and the line "If you don't see a command prompt, try pressing enter" — both come from the -i flag, and neither needs any action from you.

  1. Delete one Pod and watch Kubernetes replace it:
kubectl delete pod <pod-name>
kubectl get pods --watch
  1. Inspect a Pod with kubectl describe pod <pod-name> and kubectl logs <pod-name>.
  2. Delete the cluster when you have finished:
k3d cluster delete dev
  1. Enable the tests for this exercise, then commit and push your work:
git mv tests/kubernetes-intro.bats.disabled tests/kubernetes-intro.bats

What success looks like

  • Both manifests are valid Kubernetes YAML.
  • The Deployment requests 2 replicas, and both Pods reach Running.
  • The Service selector matches the Pod labels in the Deployment.
  • Calling the Service by name from inside the cluster returns a response from either replica.

References

About

Student exercises for the Containerisation bootcamp course module

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages