Running containers as root is a security risk. Configure your services to use non-root users for defense in depth.

The problem

By default, many containers run as root:

services:
  app:
    image: nginx
    # Runs as root user (uid 0) - security risk!

If compromised, attackers have root privileges inside the container.

The solution

Set the user in compose.yml:

services:
  app:
    image: node:20
    user: "1000:1000"  # Run as uid:gid 1000
    working_dir: /app
    volumes:
      - ./app:/app

Or use the image’s built-in user:

services:
  nginx:
    image: nginx:alpine
    user: "nginx"  # Use nginx user from image

Creating users in Dockerfile

Best practice: create a dedicated user in your image:

FROM node:20-alpine

# Create app user and group
RUN addgroup -g 1001 -S appuser && \
    adduser -u 1001 -S appuser -G appuser

# Create app directory with correct ownership
RUN mkdir -p /app && chown -R appuser:appuser /app

# Switch to non-root user
USER appuser

WORKDIR /app
COPY --chown=appuser:appuser package*.json ./
RUN npm ci --only=production
COPY --chown=appuser:appuser . .

CMD ["node", "server.js"]

Use it in compose.yml:

services:
  api:
    build: .
    # Already runs as appuser from Dockerfile
    ports:
      - "3000:3000"

Handling file permissions

When using volumes with non-root users:

services:
  app:
    image: node:20
    user: "1000:1000"
    volumes:
      - ./data:/data  # Must be writable by uid 1000
    # Fix permissions on startup
    entrypoint: |
      sh -c 'chown -R 1000:1000 /data 2>/dev/null || true && npm start'

Better approach - use init container:

services:
  # Fix permissions before app starts
  init-permissions:
    image: busybox
    user: root
    volumes:
      - ./data:/data
    command: chown -R 1000:1000 /data

  app:
    image: node:20
    user: "1000:1000"
    volumes:
      - ./data:/data
    depends_on:
      init-permissions:
        condition: service_completed_successfully

Common issues and solutions

Port binding below 1024:

services:
  web:
    image: nginx
    user: "nginx"
    ports:
      - "8080:8080"  # Use high ports for non-root
    # Configure nginx to listen on 8080 instead of 80

Reading secrets:

services:
  app:
    image: myapp
    user: "1000:1000"
    secrets:
      - source: db_password
        uid: "1000"  # Make secret readable by user
        mode: 0400

secrets:
  db_password:
    file: ./secrets/db_password.txt

Verify user

Check which user is running:

docker compose exec app whoami
# Should output: appuser (not root)

docker compose exec app id
# uid=1000(appuser) gid=1000(appuser)

Further reading