With Docker images, you can deploy containers quickly without the overhead of full virtual machines. Base images pulled from Docker Hub are useful starting points, but real-world projects almost always require customization — installing packages, setting environment variables, or configuring application defaults. Doing this manually every time is error-prone and slow. That is exactly the problem Dockerfiles solve.
A Dockerfile is a plain-text file containing a sequential set of instructions that Docker uses to build an image automatically and repeatably. Once you have a Dockerfile, rebuilding the same image — or building variations of it — is a single command.
This guide walks through the core Dockerfile instructions, creates a working example, and shows how to build images from it.
Core Dockerfile Instructions
A Dockerfile is named exactly Dockerfile (no extension) and lives in a directory known as the build context. Docker reads it top to bottom when building an image. The following instructions cover the majority of real-world use cases:
| Instruction | Purpose |
|---|---|
FROM | Specifies the base image to build on top of. Every Dockerfile must start with FROM (or ARG before it). |
RUN | Executes a shell command during the image build process. Each RUN creates a new image layer. |
CMD | Sets the default command to run when a container starts. Can be overridden at runtime. |
ENTRYPOINT | Sets a fixed executable for the container. Unlike CMD, it is not easily overridden. |
ENV | Defines environment variables available at build time and runtime. |
EXPOSE | Documents which port the container listens on. Does not actually publish the port. |
ADD | Copies files from the host (or a URL) into the container filesystem. Also auto-extracts tar archives. |
COPY | Copies files or directories from the build context into the container. Preferred over ADD for simple copies. |
WORKDIR | Sets the working directory for subsequent RUN, CMD, ENTRYPOINT, COPY, and ADD instructions. |
USER | Sets the user (UID or username) that subsequent instructions and the container process run as. |
VOLUME | Creates a mount point and marks it as holding externally-mounted data. |
LABEL | Adds metadata to an image as key-value pairs (replaces the deprecated MAINTAINER instruction). |
You do not need to use every instruction in every Dockerfile. A minimal, functional Dockerfile may use only FROM, RUN, and CMD.
Writing Your First Dockerfile
The example below builds a custom Ubuntu image that is fully updated and has the build-essential package installed — a common starting point for development environments.
Step 1: Create a build directory
mkdir ~/mydockerbuild
cd ~/mydockerbuild
Step 2: Create the Dockerfile
Create a file named Dockerfile in that directory with the following content:
# Dockerfile
FROM ubuntu:latest
LABEL maintainer="Your Name <your.email@example.com>"
RUN apt-get -y update && \
apt-get -y upgrade && \
apt-get install -y build-essentialCode language: PHP (php)
A few things worth noting about this Dockerfile:
FROM ubuntu:latest— pulls the latest official Ubuntu image as the base. In production, pinning to a specific version (e.g.,ubuntu:22.04) is strongly recommended for reproducibility.LABEL maintainer— the modern replacement for the deprecatedMAINTAINERinstruction. Labels are arbitrary key-value metadata.- Chaining
RUNcommands with&&— combining update, upgrade, and install into a singleRUNinstruction keeps them in one layer, avoiding the cached-stale-index problem that occurs when they are split across separateRUNlines.
Building the Image
With your Dockerfile in place, build the image using docker build. The -t flag tags the image with a human-readable name, and the trailing . tells Docker to use the current directory as the build context.
docker build -t myubuntu-dev .
If you want to build multiple purpose-specific images from the same Dockerfile, simply run the command with different tag names:
docker build -t webdev .
docker build -t appdev .
docker build -t secdev .
The trailing . is required — it defines the build context Docker sends to the daemon.
Verifying your images
Once the build completes, list all locally available images:
docker images
You should see each tagged image listed with its ID, creation time, and size.
Running a Container from Your Image
To start an interactive container from a built image, use docker run:
docker run -it myubuntu-dev /bin/bash
-ikeeps STDIN open.-tallocates a pseudo-TTY, giving you an interactive shell./bin/bashis the command to run inside the container.
Best Practices to Keep in Mind
- Pin base image versions. Using
ubuntu:latestmeans your build may differ between runs as the upstream image changes. Use a specific tag likeubuntu:22.04for stable, reproducible builds. - Minimize layers. Each
RUNinstruction creates a layer. Combine related commands with&&and clean up package manager caches in the same layer (e.g.,rm -rf /var/lib/apt/lists/*) to keep image sizes small. - Use
COPYinstead ofADDfor simple file copies.ADDhas implicit behavior (URL fetching, tar extraction) that can be surprising. - Avoid running as root. Use the
USERinstruction to switch to a non-root user before the finalCMDorENTRYPOINT. - Use a
.dockerignorefile. Similar to.gitignore, this prevents unnecessary files (logs, local configs,node_modules) from being sent to the Docker daemon as part of the build context.
Summary
Dockerfiles give you a declarative, version-controllable way to define exactly what goes into a Docker image. The workflow is straightforward: write a Dockerfile, run docker build -t <name> ., and run docker run against the resulting image. The example above is intentionally simple, but the same principles scale to multi-stage builds, complex microservice images, and CI/CD pipelines where image consistency is critical.
Once you are comfortable with the basics, explore multi-stage builds — a powerful pattern for producing lean production images without separate build environments.