Skip to content

Dockerfile Basics

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.

Every Dockerfile must start with FROM. It sets the starting point — the base image all subsequent instructions build on.

FROM node:22-alpine

node: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 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 /app

If the directory does not exist, Docker creates it. All COPY, RUN, and CMD instructions that follow operate relative to /app.

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 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=dev

Always 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 sets environment variables that are baked into the image and available at both build time and runtime.

ENV NODE_ENV=production
ENV PORT=3000

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 3000

Here is a complete Dockerfile for a minimal Node.js HTTP server:

# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY src/ ./src/
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["node", "src/server.js"]

Build it:

Terminal window
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.0

Run the container:

Terminal window
docker run --rm -p 3000:3000 myapp:1.0

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
Which instruction sets the working directory for all subsequent Dockerfile instructions?
What does EXPOSE actually do?
Why should you combine multiple shell commands in a single RUN instruction using &&?
Which instruction copies files from the host build context into the image?