
Welcome back to the Docker Simplified series! So far, we’ve covered the essentials of Docker, worked with containers, and explored Docker images. Now it’s time to unlock the secrets behind creating those images: the Dockerfile.
In this chapter, we’ll take a deep dive into Dockerfiles, the simple yet powerful files that define how Docker images are built. You’ll learn about their structure, key instructions, best practices, and advanced concepts to help you craft efficient and effective Dockerfiles for your projects.

Dockerfile is a text document that contains a series of instructions and arguments. These instructions are used to create a Docker image automatically. It’s essentially a script of successive commands Docker will run to assemble an image, automating the image creation process.
Anatomy of a Dockerfile
A Dockerfile typically consists of the following components:
- Base image declaration
- Metadata and label information
- Environment setup
- File and directory operations
- Dependency installation
- Application code copying
- Execution command specification
Let’s dive deep into each of these components and the instructions used to implement them.
Dockerfile Instructions
FROM
The FROM instruction initializes a new build stage and sets the base image for subsequent instructions.
FROM ubuntu:20.04
This instruction is typically the first one in a Dockerfile. It’s possible to have multiple FROM instructions in a single Dockerfile for multi-stage builds.
LABEL
LABEL adds metadata to an image in key-value pair format.
LABEL version="1.0" maintainer="john@example.com"
description="This is a sample Docker image"
Labels are useful for image organization, licensing information, annotations, and other metadata.
ENV
ENV sets environment variables in the image.
ENV APP_HOME=/app NODE_ENV=production
These variables persist when a container is run from the resulting image.
WORKDIR
WORKDIR sets the working directory for any subsequent RUN, CMD, ENTRYPOINT, COPY, and ADD instructions.
WORKDIR /app
If the directory doesn’t exist, it will be created.
COPY and ADD
Both COPY and ADD instructions copy files from the host into the image.
COPY package.json .
ADD https://example.com/big.tar.xz /usr/src/things/
COPY is generally preferred for its simplicity. ADD has some extra features like tar extraction and remote URL support, but these can make build behavior less predictable.
RUN
RUN executes commands in a new layer on top of the current image and commits the results.
RUN apt-get update && apt-get install -y nodejs
It’s a best practice to chain commands with && and clean up in the same RUN instruction to keep layers small.
CMD
CMD provides defaults for an executing container. There can only be one CMD instruction in a Dockerfile.
CMD ["node", "app.js"]
CMD can be overridden at runtime.
ENTRYPOINT
ENTRYPOINT configures a container that will run as an executable.
ENTRYPOINT ["nginx", "-g", "daemon off;"]
ENTRYPOINT is often used in combination with CMD, where ENTRYPOINT defines the executable and CMD supplies default arguments.
EXPOSE
EXPOSE informs Docker that the container listens on specified network ports at runtime.
EXPOSE 80 443
This doesn’t actually publish the port; it functions as documentation between the person who builds the image and the person who runs the container.
VOLUME
VOLUME creates a mount point and marks it as holding externally mounted volumes from native host or other containers.
VOLUME /data
This is useful for any mutable and/or user-serviceable parts of your image.
ARG
ARG defines a variable that users can pass at build-time to the builder with the docker build command.
ARG VERSION=latest
This allows for more flexible image builds.
Best Practices for Writing Dockerfiles
- Use multi-stage builds: This helps create smaller final images by separating build-time dependencies from runtime dependencies.
FROM node:14 AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
2. Minimize the number of layers: Combine commands where possible to reduce the number of layers and image size.
3. Leverage build cache: Order instructions from least to most frequently changing to maximize build cache usage.
4. Use .dockerignore: Exclude files not relevant to the build, similar to .gitignore.
5. Don’t install unnecessary packages: Keep the image lean and secure by only installing what’s needed.
6. Use specific tags: Avoid latest tag for base images to ensure reproducible builds.
7. Set the WORKDIR: Always use WORKDIR instead of proliferating instructions like RUN cd … && do-something.
8. Use COPY instead of ADD: Unless you explicitly need the extra functionality of ADD, use COPY for transparency.
9. Use environment variables: Especially for version numbers and paths, making the Dockerfile more flexible.
Advanced Dockerfile Concepts
Health Checks
You can use the HEALTHCHECK instruction to tell Docker how to test a container to check that it’s still working.
HEALTHCHECK --interval=30s --timeout=10s CMD curl -f http://localhost/ || exit 1
Shell and Exec Forms
Many Dockerfile instructions can be specified in shell form or exec form:
- Shell form: RUN apt-get install python3
- Exec form: RUN [“apt-get”, “install”, “python3”]
The exec form is preferred as it’s more explicit and avoids issues with shell string munging.
BuildKit
BuildKit is a new backend for Docker builds that offers better performance, storage management, and features. You can enable it by setting an environment variable:
export DOCKER_BUILDKIT=1
Conclusion
In this blog, we’ve explored the ins and outs of Dockerfiles, from their anatomy and key instructions to advanced concepts and best practices. By now, you should feel confident writing efficient and effective Dockerfiles to create custom images tailored to your applications. With a solid understanding of Dockerfiles, you’re equipped to build and deploy containers that are lightweight, portable, and scalable.
In the next part of this series, we’ll dive into Docker Networking, where we’ll uncover how containers communicate, explore Docker’s networking options, and learn to configure networks for real-world scenarios.
Stay tuned, and as always, don’t forget to follow and hit the 👏 button below if you found this guide helpful. Happy coding! 🚀
Thank you for reading! 💚
