Practice brief
Container Security Exercises
Every container you have built so far runs as root. In Module 13 you fix that: you add a non-root user to the Dockerfile so the API process runs with minimal privileges. This is a single-line change with a significant security impact.
Build from scratch
Add a Non-Root User to the Dockerfile
If an attacker exploits a vulnerability in the TaskFlow API, they get the privileges of the process running the API. Right now that process runs as root inside the container — the most powerful user on the system. If the container's root maps to the host's root (which it can under certain configurations), that is a serious problem. In this exercise you will add three lines to the Dockerfile that create a non-root user and switch the API process to run as that user.
Done when
Try it yourself first. Open the guided path if you get blocked.
Step 1 — Confirm the API is currently running as root
docker compose up -d
docker compose exec api ps aux ps aux shows all processes inside the running api container. Look at the USER column. You will see root. Every process in the container — including the Node.js API — is running with root privileges. This is the default for Docker images that do not explicitly set a user.
Step 2 — Add a non-root user to the Dockerfile
Open your Dockerfile. After the RUN npm install line and before the COPY . . line, add these three lines. RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser addgroup -S creates a system group named appgroup. adduser -S creates a system user named appuser and adds them to appgroup. The -S flag means system account — no home directory, no password, no shell. USER appuser tells Docker that all subsequent instructions and the final CMD should run as this user. The USER instruction must come after the npm install step because npm install writes to /app, which is owned by root. Once you switch to appuser, you cannot write to directories owned by root.
Step 3 — Fix file ownership before switching users
After USER appuser, the COPY . . instruction copies your application files. But because the user is now appuser, those files may end up owned by root (from the build context), and appuser might not have read access to them. Add a chown to the COPY instruction. COPY --chown=appuser:appgroup . . The --chown flag tells Docker to set the ownership of copied files to appuser:appgroup as they are copied into the image. This ensures the application files are readable and executable by the user who will run the app.
Step 4 — Rebuild the image and restart the stack
docker compose down
docker compose up -d --build --build forces Compose to rebuild the api image from the updated Dockerfile rather than using the cached version. After the build, Compose starts both services fresh.
Step 5 — Confirm the API now runs as appuser
docker compose exec api ps aux Run ps aux inside the container again. The USER column should now show appuser instead of root. The API is still working — it just no longer has root privileges. This is the minimal privilege principle: a process should only have the permissions it needs to function.
Step 6 — Verify the API still responds
curl http://localhost:8000/health The security change should not affect the API's behaviour. A 200 response confirms the application runs correctly as a non-root user. If you get a permission denied error in the logs, check that the --chown flag was applied to the COPY . . instruction.
Break-fix
The Non-Root User That Cannot Read the App
A teammate added a non-root user to the Dockerfile and switched to it with USER appuser. The image builds. But when the container starts, it exits immediately with permission denied. They added the USER instruction after the npm install step, which is correct. But they forgot to fix the ownership of the application files. Reproduce this, understand why the error happens, and fix it.
Reproduce this
docker compose down You're done when
Show investigative hints
- Run docker compose logs api after the container exits. The error will say permission denied and name a file path. That tells you exactly which files the user cannot read.
- Files copied into a Docker image with COPY inherit their ownership from the build context. When you COPY as root, the files are owned by root. A non-root user cannot execute a file owned by root without read permission.
- The COPY instruction supports a --chown flag: COPY --chown=appuser:appgroup . . sets the owner as the files are copied. This must come after the USER instruction so the user exists when the build runs.
Show diagnosis and fix rationale
Switching to USER appuser changes the runtime user, but files copied into the image may still be owned by root. If the app user cannot read or execute the application files, the process exits with permission denied. Ensure the user and group exist before the final copy, then copy the app files with COPY --chown=appuser:appgroup . . so the runtime user owns what it needs.