You can now build images, run containers, wire up networks and compose multi-service apps. The final step is doing all of that safely and maintainably. Containers share the host kernel, so a careless configuration can turn one compromised container into a compromised server. This lesson collects the practices that separate hobby setups from production-grade Docker.
By default, processes inside a container run as root. If an attacker escapes the application, they hold root inside the container — and any mounted volume or kernel vulnerability becomes far more dangerous. Create and switch to an unprivileged user in your Dockerfile:
FROM node:20-alpine
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --chown=app:app . .
USER app
CMD ["node", "server.js"]Many official images already provide a user (for example node); using USER node costs nothing and closes a whole class of attacks.
Every package in an image is potential attack surface. Prefer slim or alpine base images, use multi-stage builds so compilers and dev dependencies never reach production, and maintain a thorough .dockerignore (exclude .git, node_modules, .env, logs). Pin base images to specific versions — node:20.14-alpine rather than node:latest — so builds are reproducible and upgrades are deliberate rather than accidental.
Anything written in a Dockerfile or copied into an image is recoverable with docker history or by exporting layers. That includes secrets you later delete in another layer. Instead:
docker run time, not ENV in the Dockerfile.RUN --mount=type=secret,id=npmrc npm ci exposes the file only during that step..env files out of images and out of version control.Docker lets you shrink what a container is allowed to do:
docker run -d \
--read-only --tmpfs /tmp \
--cap-drop=ALL --cap-add=NET_BIND_SERVICE \
--security-opt no-new-privileges \
--memory=512m --cpus=1 \
--restart unless-stopped \
myapp:1.4.2Here the filesystem is read-only except a scratch /tmp, all Linux capabilities are dropped except binding low ports, privilege escalation is blocked, and the container cannot starve the host of memory or CPU. Never use --privileged unless you fully understand why you need it, and avoid mounting the Docker socket (/var/run/docker.sock) into containers — that is effectively root on the host.
Images age badly: yesterday’s secure base image accumulates CVEs. Build scanning into your workflow:
docker scout cves myapp:1.4.2 # or: trivy image myapp:1.4.2Rebuild images regularly to pick up patched base layers, sign images if your registry supports it, and pull only from trusted registries. In CI, fail the pipeline on critical vulnerabilities so insecure images never ship.
HEALTHCHECK so orchestrators know when a container is alive but broken.unless-stopped) rather than ad-hoc scripts.LABEL org.opencontainers.image.revision=$GIT_SHA) for traceability.docker system prune on build hosts to control disk usage.Before shipping, verify: non-root user, pinned slim base image, multi-stage build, no secrets in layers, capabilities dropped, resource limits set, healthcheck defined, image scanned, and the Docker daemon itself patched and reachable only by trusted users (membership in the docker group is root-equivalent).
--tmpfs for scratch space.That wraps up the Docker course — you can now take an application from a bare Dockerfile to a hardened, production-ready deployment. For a natural next step, explore orchestration with Kubernetes or revisit Docker Compose to add healthchecks and resource limits to your existing stacks.