While pre-built images cover many needs, building your own Docker image lets you package your specific application exactly as needed. This guide covers writing an effective Dockerfile.
What a Dockerfile Does
A text file containing instructions for building a Docker image — starting from a base image, then adding your application code, dependencies, and configuration in defined layers.
A Basic Dockerfile Example (Node.js Application)
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Understanding Key Instructions
| Instruction | Purpose |
|---|---|
| FROM | The base image to build upon |
| WORKDIR | Sets the working directory for subsequent instructions |
| COPY | Copies files from your build context into the image |
| RUN | Executes a command during image build (installing dependencies, etc.) |
| EXPOSE | Documents which port the container listens on (informational, doesn't actually publish it) |
| CMD | The default command run when a container starts from this image |
Building the Image
docker build -t myapp:1.0 .
. specifies the build context (current directory) — Docker sends this entire directory to the build process, so keep it focused on what's actually needed.
Choosing a Good Base Image
Prefer smaller, purpose-built base images (like Alpine variants) over full general-purpose OS images when compatible — smaller images build faster, transfer faster, and have less attack surface.
Optimizing Layer Caching
COPY package*.json ./
RUN npm ci --production
COPY . .
Copying dependency manifests and installing dependencies before copying the rest of your code means Docker can reuse the cached dependency-install layer on subsequent builds, as long as dependencies haven't changed — significantly speeding up iterative builds.
Using .dockerignore
node_modules
.git
.env
*.log
Prevents unnecessary files from being included in the build context, keeping builds faster and avoiding accidentally including sensitive files (like .env) in your image.
Setting a Non-Root User
RUN addgroup -g 1001 appgroup && adduser -D -u 1001 -G appgroup appuser
USER appuser
See How to Run Docker Containers as a Non-Root User — a meaningful security improvement over the default root user.
Tagging Images Meaningfully
docker build -t myapp:1.2.0 -t myapp:latest .
Use specific version tags, not just latest, for genuine traceability of what's actually deployed — latest alone makes it hard to know exactly which code version is running.
Testing Your Built Image
docker run --rm -p 3000:3000 myapp:1.0
Common Errors
Build succeeds but the application fails to start — verify the CMD instruction correctly matches how your application should actually be started, and check for missing runtime dependencies not included in the build.
Build is very slow — review your Dockerfile's instruction order for caching efficiency, and check your .dockerignore is excluding unnecessary large files/directories from the build context.
Continue Reading
- Docker Multi-Stage Builds: Reducing Image Size
- Understanding Docker Image Layers and Caching
- How to Run Docker Containers as a Non-Root User
Browse more articles in Docker & Containers.