Dockerfile Guide

A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. Using docker build, users can create an automated build that executes several command-line instructions in succession.

Basic Dockerfile Structure

# Specify base image
FROM node:18-alpine

# Set working directory
WORKDIR /app

# Copy dependency files
COPY package*.json ./

# Install dependencies
RUN npm install --production

# Copy application files
COPY . .

# Expose port
EXPOSE 3000

# Define command to run
CMD ["node", "index.js"]

Dockerfile Instructions

FROM

Sets the base image for subsequent instructions.

FROM ubuntu:22.04
FROM node:18-alpine
FROM python:3.11-slim

WORKDIR

Sets the working directory for any RUN, CMD, ENTRYPOINT, COPY, and ADD instructions.

WORKDIR /app
WORKDIR /usr/src/app

COPY vs ADD

COPY - Copies files from host to container

COPY package.json .
COPY src/ ./src/
COPY . .

ADD - Similar to COPY but also supports URLs and tar extraction

⚠️ Warning: Prefer COPY over ADD for better clarity and predictability.

RUN

Executes commands in a new layer and creates a new image. Used for installing packages and running shell commands.

# Single line
RUN apt-get update && apt-get install -y nginx

# Multi-line (more readable)
RUN apt-get update && \
    apt-get install -y \
    nginx \
    curl \
    wget

CMD

Specifies the default command to run when a container starts. Only one CMD is allowed.

# Shell form
CMD npm start

# Exec form (preferred)
CMD ["node", "index.js"]

# With parameters
CMD ["node", "index.js", "--env", "production"]
💡 Tip: Use exec form (JSON array) for better signal handling and proper handling of process termination.

ENTRYPOINT

Similar to CMD but cannot be overridden. Arguments passed to docker run are appended to ENTRYPOINT.

ENTRYPOINT ["npm"]
CMD ["start"]

ENV

Sets environment variables that persist during the build and in the running container.

ENV NODE_ENV=production
ENV APP_PORT=3000
ENV APP_USER=node

ARG

Defines variables available only during build time (not at runtime).

ARG NODE_VERSION=18
FROM node:${NODE_VERSION}-alpine

EXPOSE

Documents which ports the container listens on (doesn't actually publish ports).

EXPOSE 80
EXPOSE 443
EXPOSE 3000 8080

LABEL

Adds metadata to the image.

LABEL maintainer="[email protected]"
LABEL version="1.0"
LABEL description="My application"

Multi-stage Builds

Optimize image size by using multiple FROM instructions in a single Dockerfile.

# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
💡 Tip: Multi-stage builds help reduce final image size significantly by excluding build tools and dependencies.

Layer Caching

Optimize build times by ordering instructions from least to most frequently changed.

❌ Bad Order:
COPY . .
RUN npm install  # This runs on every code change
COPY package.json .
RUN npm install
✅ Good Order:
COPY package*.json .
RUN npm install    # Cached if package.json unchanged
COPY . .           # Only runs when source changes
CMD ["npm", "start"]

Complete Example

Here's a production-ready Dockerfile for a Node.js application:

# Multi-stage build
FROM node:18-alpine AS builder

# Create app directory
WORKDIR /app

# Install dependencies only when needed
COPY package*.json ./
RUN npm ci --only=production && \
    npm cache clean --force

# Copy application code
COPY . .

# Production image
FROM node:18-alpine

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# Copy from builder
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app .

# Switch to non-root user
USER nodejs

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s \
  CMD node healthcheck.js

# Start application
CMD ["node", "index.js"]

.dockerignore File

Create a .dockerignore file to exclude unnecessary files from the build context:

node_modules
npm-debug.log
.git
.gitignore
.env
README.md
dist
*.md
.coverage
.vscode

Building and Running

Build an image

docker build -t myapp:latest .
docker build -t myapp:v1.0.0 -t myapp:latest .
docker build -f Dockerfile.prod -t myapp:prod .

Run the container

docker run -d -p 3000:3000 --name myapp myapp:latest
docker run -d -p 3000:3000 -e NODE_ENV=production --name myapp myapp:latest

Next Steps