Dockerfile Basics
Core Dockerfile instructions
Section titled “Core Dockerfile instructions”A Dockerfile is a sequence of instructions. Docker reads them top to bottom and executes each one to build a new layer. Here are the six instructions you will use in almost every Dockerfile.
FROM — choose your base
Section titled “FROM — choose your base”Every Dockerfile must start with FROM. It sets the starting point — the base image all subsequent instructions build on.
FROM node:22-alpinenode:22-alpine is the official Node.js image built on Alpine Linux (~50 MB). Always pick the most specific, smallest base image that meets your needs.
WORKDIR — set the working directory
Section titled “WORKDIR — set the working directory”WORKDIR creates a directory inside the image and makes it the current working directory for all subsequent instructions. Using it avoids hard-coding paths everywhere.
WORKDIR /appIf the directory does not exist, Docker creates it. All COPY, RUN, and CMD instructions that follow operate relative to /app.
COPY — bring files into the image
Section titled “COPY — bring files into the image”COPY copies files or directories from the build context (your project folder on the host) into the image filesystem.
COPY package.json package-lock.json ./COPY src/ ./src/The first argument is the source (relative to the build context). The second is the destination inside the image (relative to WORKDIR).
RUN — execute commands during build
Section titled “RUN — execute commands during build”RUN executes a shell command and commits the result as a new layer. Use it to install dependencies, compile code, or run any one-time setup.
RUN npm ci --omit=devAlways combine related commands with && to keep them in one layer and reduce image size:
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*ENV — set environment variables
Section titled “ENV — set environment variables”ENV sets environment variables that are baked into the image and available at both build time and runtime.
ENV NODE_ENV=productionENV PORT=3000EXPOSE — document the port
Section titled “EXPOSE — document the port”EXPOSE is documentation — it tells readers and tools which port the application listens on. It does not actually publish the port; you still need -p on docker run.
EXPOSE 3000Building a real app image
Section titled “Building a real app image”Here is a complete Dockerfile for a minimal Node.js HTTP server:
# syntax=docker/dockerfile:1FROM node:22-alpineWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci --omit=devCOPY src/ ./src/ENV NODE_ENV=productionENV PORT=3000EXPOSE 3000CMD ["node", "src/server.js"]Build it:
docker build -t myapp:1.0 .Expected build output (layers are numbered, cached layers print CACHED):
[+] Building 12.3s (9/9) FINISHED => [internal] load build definition from Dockerfile 0.0s => [internal] load .dockerignore 0.0s => [1/6] FROM docker.io/library/node:22-alpine 4.1s => [2/6] WORKDIR /app 0.0s => [3/6] COPY package.json package-lock.json ./ 0.0s => [4/6] RUN npm ci --omit=dev 6.8s => [5/6] COPY src/ ./src/ 0.0s => [6/6] ENV NODE_ENV=production PORT=3000 0.0s => exporting to image 0.9s => => naming to docker.io/library/myapp:1.0Run the container:
docker run --rm -p 3000:3000 myapp:1.0Hands-on practice
Section titled “Hands-on practice”The snippet below includes a self-contained Dockerfile and a tiny inline server.js written with a shell echo — no external files needed. Paste it in Play with Docker and run it.
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
# Inline a minimal server so no host files are needed
RUN echo 'const h=require("http");h.createServer((_,r)=>{r.writeHead(200);r.end("Hello from Docker!\n");}).listen(3000,()=>console.log("Listening on 3000"));' > server.js
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "server.js"]
# --- build & run ---
# docker build -t myapp .
# docker run --rm -p 3000:3000 myapp
# curl http://localhost:3000