Writing Secure Dockerfiles
Writing Secure Dockerfiles
Dockerfile security begins with choosing appropriate base images. The selection between minimal images like Alpine Linux and full distributions like Ubuntu impacts both security and functionality. Alpine Linux's smaller size reduces attack surface but may require additional work for compatibility. Distroless images eliminate package managers and shells, further reducing attack vectors but complicating debugging. Organizations must balance security benefits against operational requirements when selecting base images.
Multi-stage builds significantly improve image security by separating build environments from runtime environments. Build stages can include compilers, development tools, and source code that shouldn't exist in production images. The final stage copies only necessary artifacts, excluding build tools that could aid attackers. This approach reduces image size and eliminates unnecessary components that increase attack surface.
# Example: Secure multi-stage Dockerfile for a Node.js application
# Build stage with development dependencies
FROM node:16-alpine AS builder
# Create non-root user for build process
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Set working directory
WORKDIR /app
# Copy package files first for better layer caching
COPY package*.json ./
# Install dependencies with security audit
RUN npm ci --only=production && \
npm audit fix && \
npm cache clean --force
# Copy application source
COPY --chown=nodejs:nodejs . .
# Build application
RUN npm run build
# Production stage with minimal dependencies
FROM node:16-alpine AS production
# Install security updates
RUN apk update && \
apk upgrade && \
apk add --no-cache dumb-init && \
rm -rf /var/cache/apk/*
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Set working directory
WORKDIR /app
# Copy only production dependencies and built application
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
# Security hardening
RUN chmod -R 550 /app && \
find /app -type d -exec chmod 550 {} \; && \
find /app -type f -exec chmod 440 {} \;
# Expose only necessary port
EXPOSE 3000
# Switch to non-root user
USER nodejs
# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
# Run with Node.js security flags
CMD ["node", "--max-old-space-size=256", "--max-http-header-size=8192", "dist/server.js"]
User management within Dockerfiles critically impacts container security. Running containers as root remains a common but dangerous practice. Creating dedicated non-root users for applications prevents privilege escalation attacks. User IDs should be explicitly defined to ensure consistency across environments. File ownership and permissions require careful configuration to maintain security while allowing necessary access.