hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Deploying Node.js Apps with Docker

Why Docker

  • The "Works on My Machine" Problem
  • Docker Concepts

Your First Dockerfile

  • Writing a Dockerfile
  • Multi-Stage Builds
  • The .dockerignore File

Running Containers

  • Running and Managing Containers
  • Environment Variables and Secrets
  • Health Checks

Multi-Container Apps

  • Docker Compose
  • Adding a Reverse Proxy
  • Persistent Data

Deploying to Production

  • Deploying to a VPS
  • HTTPS with Let's Encrypt
  • Zero-Downtime Deploys

CI/CD

  • Building Images in CI
  • Automated Deployment

Production Hardening

  • Container Security
  • Logging and Monitoring
  • Deployment Checklist and Capstone

Container Security

Containers are not sandboxes

Docker containers provide isolation, but they are not impenetrable. If an attacker exploits a vulnerability in your app, they are inside the container. Reducing what they can do inside limits the damage.

Run as non-root

By default, the process inside a container runs as root. If an attacker gains code execution, they are root inside the container — and can potentially escape to the host.

Add a non-root user to your Dockerfile:

# In the production stage
FROM node:22-alpine AS production

# Create a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --omit=dev

COPY --from=build /app/dist ./dist

# Change ownership of the app directory
RUN chown -R appuser:appgroup /app

# Switch to non-root user
USER appuser

EXPOSE 3000
CMD ["node", "dist/server.js"]

The USER appuser instruction means the Node.js process runs as appuser, not root. An attacker who gains code execution cannot install packages, modify system files, or access root-only resources.

[!NOTE] If your app writes to the filesystem (SQLite database, uploads), the non-root user needs write permission to those directories. Volumes mounted at runtime inherit the container’s user, so chown is only needed for directories inside the image.

Read-only filesystem

Make the container’s filesystem read-only. The app can only write to explicitly mounted volumes:

docker run -d \
  --read-only \
  --tmpfs /tmp \
  -v app-data:/data \
  myapp

--read-only makes the entire filesystem read-only. --tmpfs /tmp creates a writable tmpfs (in-memory filesystem) for temporary files. The app-data volume at /data is writable for the database.

In Docker Compose:

services:
  app:
    build: .
    read_only: true
    tmpfs:
      - /tmp
    volumes:
      - app-data:/data

An attacker cannot write scripts, modify binaries, or create files outside the mounted volume. This severely limits what they can do after gaining access.

Resource limits

Without limits, a single container can consume all the server’s CPU and memory, starving other containers and the host:

services:
  app:
    build: .
    deploy:
      resources:
        limits:
          cpus: "1.0" # Maximum 1 CPU core
          memory: 512M # Maximum 512 MB RAM
        reservations:
          cpus: "0.25" # Guaranteed 0.25 CPU cores
          memory: 128M # Guaranteed 128 MB RAM

With docker run:

docker run -d \
  --cpus="1.0" \
  --memory="512m" \
  myapp

If the app exceeds the memory limit, Docker kills the container (OOM kill). This is better than the alternative: the entire server running out of memory.

Minimal image

The Alpine-based multi-stage build from earlier is already a security win: fewer packages mean fewer potential vulnerabilities. But you can go further:

# Check what is in the image
docker run --rm myapp apk list --installed

Remove anything the app does not need. The fewer binaries in the container, the less an attacker has to work with.

Do not run privileged

Never use --privileged unless you know exactly what you are doing. It gives the container full access to the host, defeating the purpose of containerization.

# NEVER in production
docker run --privileged myapp

# This is fine
docker run myapp

Exercises

Exercise 1: Add the non-root user to your Dockerfile. Build and run. Verify: docker exec myapp whoami should print appuser, not root.

Exercise 2: Run with --read-only. Try to create a file inside the container: docker exec myapp touch /test. It should fail. Verify you can still write to the volume.

Exercise 3: Set a memory limit of 256 MB. Check usage with docker stats. What happens if the app exceeds the limit?

Why should containers run as a non-root user?

← Automated Deployment Logging and Monitoring →

© 2026 hectoday. All rights reserved.