Table of Contents
- Overview
- Typical Dockerfile Structure
- Common Dockerfile Instructions
- Best Practices
- Example Minimal Dockerfile
- File Naming and Placement
- Differences: COPY vs ADD
- Conclusion
Docker: Dockerfile Overview
What Is a Dockerfile?
A Dockerfile is a text document that contains a series of commands and instructions used to assemble a Docker image. Think of it as a recipe or blueprint for building a containerized environment — specifying everything from the base operating system, to the application code, dependencies, configuration, and runtime instructions.
Why Should You Know About Dockerfiles?
- Consistency: Dockerfiles allow you to automate exactly how applications are packaged, configured, and run, eliminating the “it works on my machine” problem across different systems.
- Portability: By describing all the steps required to set up your app, Dockerfiles make it possible to run your software in any environment that supports Docker — whether that's a developer laptop, a testing pipeline, or a cloud provider.
- Automation: They enable repeatable, version-controlled builds. You can store your Dockerfile in source control, track changes, and be confident that anyone building the image gets the same result.
- Security and Best Practices: By codifying things like non-root users, environmental variables, and dependency handling, you can enforce modern software security and operational standards.
How Does a Dockerfile Work?
- Reads Sequentially: Docker reads the Dockerfile from top to bottom, executing each instruction in order.
- Builds in Layers: Each command (like
FROM
,COPY
,RUN
) results in a new layer in the image. Layers are cached, making rebuilds faster by only rebuilding changed parts. - Produces an Image: When the build process is complete, you have a Docker image. This image is a snapshot — a ready-to-run, self-contained package.
- Containers Spawn from Images: You use this image to spawn containers. Each container is an isolated, reproducible environment based on your Dockerfile’s specifications.
In summary:
A Dockerfile is an essential tool for automating application delivery with Docker. It lets you define your environment once, then reliably recreate it anywhere, enabling modern DevOps workflows and cloud-native development.
Typical Dockerfile Structure
To build a functional Docker image, your Dockerfile should be organized with the following components in this order. Here is how you can structure your Dockerfile, step by step:
-
Set the Base Image:
Define which parent image your build will start from. This provides the OS and potentially language runtime.
FROM python:3.12
-
Configure the Working Directory:
Set the working directory where subsequent commands and file copies will be executed.
WORKDIR /usr/local/app
-
Copy Dependency Files:
Bring in requirement or dependency files to allow efficient layer caching and minimize rebuilds.
COPY requirements.txt ./
-
Install Dependencies:
Run installation commands to prepare your container environment.
RUN pip install --no-cache-dir -r requirements.txt
-
Copy Application Source Code:
Copy over your main app files after dependencies are satisfied.
COPY src ./src
-
Expose Ports:
Indicate which network ports the container will listen on.
EXPOSE 5000
-
Set User (Optional for Security):
Run the container as a non-root user for added security.
RUN useradd app
USER app
-
Define the Startup Command:
Specify the default command the container runs upon startup.
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
Each of these steps forms a layer in your final Docker image, helping ensure it is efficient, maintainable, and secure.
Common Dockerfile Instructions
Dockerfiles use specific instructions to define how an image is built. Here are some frequently used instructions and what they do, step by step:
-
FROM
Specifies the base image to start your build from. It sets the foundational environment.
FROM ubuntu:22.04
-
WORKDIR
Sets the working directory inside the image for subsequent commands.
WORKDIR /app
-
COPY
Copies files or directories from your local machine into the image.
COPY . /app
-
RUN
Executes commands during the build, commonly used to install packages or dependencies.
RUN apt-get update && apt-get install -y curl
-
ENV
Sets environment variables within the image for configuration.
ENV NODE_ENV=production
-
EXPOSE
Documents the port(s) the container will listen on during runtime.
EXPOSE 80
-
USER
Defines which user to run commands or the application as (improving security).
USER appuser
-
CMD
Sets the default command that will run when the container starts.
CMD ["node", "server.js"]
By combining these instructions thoughtfully, you create repeatable, efficient, and secure Docker images.
Best Practices
Following these best practices will help you create Dockerfiles that are efficient, maintainable, and secure. Move through each step as you write or review your own Dockerfiles:
-
Use Official and Minimal Base Images:
Start with trusted and lightweight base images to reduce security risks and improve build speed. -
Leverage Layer Caching:
Copy and install dependencies separately from application code so Docker caches unchanged layers, resulting in faster builds.
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . . -
Add a .dockerignore File:
Prevent unnecessary files from being copied into the image by including a .dockerignore file. This reduces image size and build context.
.dockerignore
__pycache__/
node_modules/ -
Avoid Running as Root:
Create and switch to a non-root user to limit permissions inside the container and improve security.
RUN useradd appuser
USER appuser -
Clean Up Intermediate Files:
Remove temporary files and caches during the build process to keep images small and efficient.
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/* -
Document Exposed Ports and Metadata:
Use EXPOSE and LABEL to clarify ports used and add descriptive metadata to your image.
EXPOSE 8080
LABEL maintainer="yourname@example.com" -
Prefer COPY over ADD:
Use COPY to add files unless you need the special features of ADD (like tar extraction). -
Combine RUN Instructions When Possible:
Group several commands in a single RUN instruction to reduce image layers.
RUN apt-get update && apt-get install -y curl git
-
Always Pin Dependency Versions:
Specify exact versions of packages and dependencies to ensure predictable builds.
RUN pip install flask==2.3.2
Consistently following these steps helps prevent common issues, keeps images streamlined, and supports secure, repeatable deployments.
Example Minimal Dockerfile
Here is a simple and minimal Dockerfile example that you can use as a starting point for lightweight containers. Follow these steps to create one:
- Specify the base image:
Use a small base image like Alpine Linux for minimal size.
FROM alpine:latest
- Set a default command:
Define a default command that runs when the container starts.
CMD ["sh"]
This Dockerfile runs a minimal shell environment inside an Alpine container, resulting in a very small image.
File Naming and Placement
To ensure your Docker build works as expected, it's important to follow these steps when naming and placing your Dockerfile:
-
Name the File Correctly:
The file must be named exactlyDockerfile
—with no file extension like.txt
or.docker
.
Dockerfile ✅
Dockerfile.txt ❌
Dockerfile.docker ❌
-
Use the Root of Your Project:
Place the Dockerfile in the root directory of your project, alongside your main source code and configuration files.
This gives Docker access to the entire context of your application during build./ ├── Dockerfile ├── app.py ├── requirements.txt └── src/ └── ...
-
Include a .dockerignore File:
To control what gets sent to the Docker daemon during build, create a.dockerignore
file in the same folder.
This helps reduce build time and prevents accidental inclusion of sensitive or unnecessary files..dockerignore contents: __pycache__/ *.log node_modules/ .env
-
Alternative File Names (Advanced):
If you need to use a different file name, you can specify it with the-f
flag when building the image:
docker build -f Dockerfile.dev -t my-app .
But by default, Docker looks for a file calledDockerfile
.
Keeping your Dockerfile properly named and placed ensures consistent builds and easier project upkeep.
Differences: COPY vs ADD
Both COPY
and ADD
are Dockerfile instructions used to transfer files into your image, but they have important differences. Here’s a breakdown to help you decide which to use:
-
Basic Functionality:
COPY
copies files and directories from your local build context into the image.
ADD
can do everythingCOPY
does, plus it has some additional features. -
Remote File Support:
ADD
supports copying files from remote URLs.
COPY
does not support URLs and only copies local files. -
Tar Archive Extraction:
WhenADD
copies a local tar file (e.g., .tar, .tar.gz), it will automatically extract its contents into the image.
COPY
copies the archive as-is without extracting. -
Layer Impact and Transparency:
SinceADD
has more complex behaviors, it is sometimes less transparent and can result in unexpected builds.
COPY
is simpler and preferred when you just need to add local files. -
Best Practice:
UseCOPY
for most cases where you need to copy local files or directories.
UseADD
only if you need to:
- Automatically extract a local tar archive during image build.
- Download and add a remote file by specifying a URL.
-
Example:
COPY app /app
— Copies the localapp
directory into the image.
ADD https://example.com/package.tar.gz /tmp/
— Downloads and adds the remote tar.gz file.
By choosing between these instructions carefully, you can keep your builds predictable and efficient.
Conclusion
Throughout this blog post, we explored the foundational elements of working with Dockerfiles — the blueprint for creating Docker container images. You’ve seen not only how Dockerfiles are structured, but also the best ways to use them effectively in your development and deployment workflows.
Here’s a quick recap of what we covered:
- A Dockerfile is a step-by-step set of instructions to build a container image.
- The structure typically starts with setting a base image, defining a working directory, copying files, installing dependencies, and setting commands to run the app.
- Instructions like
FROM
,COPY
,RUN
,CMD
, andEXPOSE
serve specific roles in shaping your image. - Following practices like pinning versions, using
.dockerignore
, and avoiding running as root improves security and consistency. - A minimal Dockerfile can start with just a couple of lines using something like the Alpine image.
- Naming the Dockerfile correctly and placing it in the project root ensures Docker works as expected.
- When deciding between
COPY
andADD
, go withCOPY
unless you need features like remote downloads or tar extraction.
By now, you should feel confident writing optimized, readable, and reusable Dockerfiles for your own projects. Whether you’re building production-grade applications or lightweight test environments, the steps we've covered will help make your images smaller, more secure, and easier to maintain.
Thanks for following along! If you enjoyed this walkthrough, stay tuned for future posts on Docker Compose, multi-stage builds, and container orchestration. Happy shipping! 🐳