Running containers safely
🎯You'll be able to
Run an app as an unprivileged user, keep secrets out of images, and set limits so one container can't starve the rest.
🏠The everyday picture
A hotel gives each guest a key card that opens their own room, not a master key. It also doesn't print the safe's code on the room's welcome card, where every later guest would read it.
📘What it really is
-
Don't run as root. By default a container's process runs as user ID 0, root. Without extra protection, that is the same user ID as root on the host, so a bug that lets an attacker break out of the container starts them with the highest privileges. The official Node.js images include a user called
node; addUSER nodebeforeCMD. Check withdocker run --rm --entrypoint id routerani:1.0.0 -u: it should print a number other than 0 (thenodeuser is 1000). -
Keep secrets out of the image. Anything in a layer can be read by anyone who can pull the image:
ENV DB_PASSWORD=...andARGvalues used inRUNshow up indocker historyanddocker inspect;-
a file copied in and deleted in a later layer is still in the earlier layer. Pass secrets at run time (environment variables from the platform, or mounted secret files). For secrets needed during the build, such as a private package token, use BuildKit's
RUN --mount=type=secret,id=npm_token ..., which doesn't store them in a layer. -
Set resource limits. A container with no limits can use all the host's memory.
docker run --memory 256m(ormemory = 256in Terraform) caps it; if the app goes over, the kernel stops it ("OOM killed") instead of the whole server slowing to a crawl. Setting swap equal to memory (memory_swap = 256) stops Docker allowing extra swap. -
Reduce what the container can do. Extra options for hardening:
--read-onlymakes the container's filesystem read-only (give it a small writable/tmpif needed);--cap-drop ALLremoves Linux capabilities the app doesn't need;-
--security-opt no-new-privilegesstops a process gaining privileges later. -
Publish only what must be reached.
-p 5432:5432on a database exposes it on the host's network; leave it off and let other containers reach it on a private network (CD2).
📝A worked example
Input: docker run --rm --entrypoint id routerani:latest -u (starter image), then the same for your routerani:1.0.0.
Expected output: 0 for the starter (root) and 1000 for yours. Then docker history --no-trunc routerani:latest | grep -i password
prints nothing, because no password is baked in. In the starter Terraform, the password routerani123 is in main.tf instead: still a
problem, which CD3 fixes with a generated password.
🌍Where it's used
- Kubernetes platforms can refuse to run containers as root (
runAsNonRoot: true), so images built as root fail to deploy there. - Payment and health systems must show auditors that images carry no credentials and run with least privilege.
- Shared build servers limit memory per container so one runaway job doesn't stop everyone's builds.
📝Case scenario
A startup published its app image to a public registry for a partner. The Dockerfile had ENV AWS_SECRET_ACCESS_KEY=... for a build step.
Within a day, someone pulled the image, read the key from docker inspect and started expensive servers on the company's account. The fix
was to remove the key, rotate it, delete the published tag and pass credentials only at run time.
🖱️Try it
Run docker run --rm --memory 64m --entrypoint node routerani:1.0.0 -e "const a=[]; for(;;) a.push(Buffer.alloc(1e6))" and then
echo $?. Success: the command stops with exit code 137, which means the process was killed for using too much memory.
⚠️Common mistakes
- Leaving out
USER. Sign:id -uin the container prints 0. Fix: addUSER node(or create a user) beforeCMD. - Deleting a secret in a later layer. Sign:
docker historyshows theCOPYof a key file. Fix: never copy it; use build secrets or run-time settings. - No memory limit. Sign: one leaking container slows every other service on the host. Fix: set a limit sized from measured use.
🛠️For your project
Project 1 test 3 fails if the container runs as root, and test 2 fails if a database password is written in a .tf file.
✅Quick check
Why is ENV API_KEY=... in a Dockerfile a problem?
Show the answer
Anyone who can pull the image can read it with docker inspect or docker history.
What happens when a container goes over its memory limit?
Show the answer
The kernel kills the process (exit code 137) instead of letting it slow down the host.
