Writing Your First Dockerfile: A Step-by-Step Guide
You've heard about containers. You know they're supposed to make deployment easier. But when you sit down to write your first Dockerfile, you stare at a blank file and wonder where to start. This guide walks you through creating a production-ready Dockerfile from scratch, with concrete examples you can adapt to your own projects.
A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. Think of it as a recipe for building your application container. Every line in the file is an instruction that tells Docker what to do, in what order, and with what parameters.
Understanding Dockerfile Instructions
Before writing your first Dockerfile, you need to understand the core instructions. These commands form the building blocks of any container image.
FROM - The Foundation
The FROM instruction specifies the base image for your container. This is the starting point. All other instructions build upon this base.
This line pulls the official Node.js 18 image with Alpine Linux as the base. Alpine is lightweight, making it ideal for production containers. You can use any official image from Docker Hub or your own private registry.
WORKDIR - Setting the Stage
WORKDIR sets the working directory for any subsequent RUN, COPY, ADD, CMD, ENTRYPOINT, VOLUME, and EXPOSE instructions in the Dockerfile.
This creates the directory if it doesn't exist and changes the working directory. Always use WORKDIR instead of RUN mkdir followed by WORKDIR — it's cleaner and more reliable.
COPY and ADD - Bringing Files In
COPY copies new files or directories from your host system into the container filesystem. ADD does the same but has some extra features like automatic extraction of archives.
The first line copies your package.json and package-lock.json files to the container's current directory. The second line copies all files from your local directory to the container's current directory. Use COPY for most cases — it's more predictable and transparent.
RUN - Executing Commands
RUN executes any command in the container and commits the changes. This is where you install dependencies, build your application, or perform any setup tasks.
This command installs your production dependencies. The --only=production flag ensures only production dependencies are installed, keeping the image smaller.
CMD and ENTRYPOINT - Defining the Container's Main Process
CMD and ENTRYPOINT specify the default command to run when the container starts. They're similar but have key differences in how they interact with other instructions.
This runs the Node.js server when the container starts. The JSON array format is preferred because it's more reliable and allows for proper argument passing.
Building a Complete Dockerfile
Let's build a complete Dockerfile for a Node.js application. This example demonstrates best practices and common patterns.
This Dockerfile follows a logical flow: start with a base image, set up the working directory, copy and install dependencies, copy your application code, expose the port, and define the startup command.
Multi-Stage Builds for Smaller Images
One of the biggest advantages of Docker is the ability to use multi-stage builds. This technique allows you to use different stages in your build process, discarding intermediate artifacts to create a smaller final image.
This Dockerfile uses two stages. The first stage builds your application, and the second stage creates the production image. The COPY --from=builder instruction copies only the necessary files from the build stage to the production stage, keeping the final image small.
Best Practices for Production Dockerfiles
Writing a Dockerfile is easy. Writing a production-ready Dockerfile requires attention to detail. Here are the key practices to follow.
Use Specific Image Tags
Always use specific image tags instead of latest. The latest tag can change, breaking your builds unexpectedly.
Minimize the Number of Layers
Each RUN, COPY, and ADD instruction creates a new layer. Combine commands to reduce the number of layers.
Use .dockerignore
Create a .dockerignore file to exclude unnecessary files from the build context. This speeds up builds and reduces image size.
Leverage Build Cache
Docker caches layers by default. Structure your Dockerfile to maximize cache hits. Put instructions that change frequently at the end.
Use Non-Root Users
Run containers as non-root users to improve security. This prevents privilege escalation attacks.
Health Checks
Add health checks to monitor your container's health. This helps Docker and orchestration systems understand when to restart unhealthy containers.
Common Dockerfile Patterns
Different applications require different Dockerfile patterns. Here are some common examples.
Node.js Applications
Python Applications
Go Applications
Static Site (Nginx)
Debugging Dockerfiles
Writing a Dockerfile is only half the battle. Debugging issues is where most developers struggle. Here are some common problems and solutions.
Issue: Container exits immediately
Check your CMD or ENTRYPOINT instruction. Ensure the command is correct and the application starts successfully.
Issue: Permission denied errors
Run as a non-root user or fix file permissions.
Issue: Large image size
Use multi-stage builds, Alpine-based images, and .dockerignore to reduce image size.
Issue: Build is slow
Optimize layer caching, use .dockerignore, and consider using BuildKit.
Testing Your Dockerfile
Before deploying your Dockerfile to production, test it thoroughly. Here's how.
Build the Image
Run the Container
Verify the Container
Check the Image Size
Test Health Checks
Conclusion
Writing a Dockerfile is a fundamental skill for modern development. You've learned the core instructions, best practices, and common patterns. Remember these key points:
- Start with a specific base image tag
- Use WORKDIR to set the working directory
- Copy dependencies separately to leverage build cache
- Use multi-stage builds for smaller production images
- Run as a non-root user for security
- Add health checks for monitoring
- Use .dockerignore to exclude unnecessary files
The best way to learn is by doing. Take an existing project, write a Dockerfile for it, and iterate based on your needs. Platforms like ServerlessBase can help you deploy and manage your containers, but understanding how to write effective Dockerfiles gives you full control over your application's environment.
Ready to containerize your first application? Start with a simple Node.js or Python project, follow the patterns in this guide, and build from there.