How to Build Docker Images with Dockerfile

By 

•

Updated on

•

13 min read

Dockerfile example used to build a Docker image on Ubuntu

When you want an application to run the same way on your laptop, a test server, and production, you package it as a Docker image. The image contains the application and everything it needs to run. A container is a runtime instance of that image.

Images are built from a Dockerfile, a plain text file with the build instructions. In this tutorial, we will explain what a Dockerfile is, how to create one, and how to build a Docker image from it. The walkthrough uses an Ubuntu 26.04 example, so you can follow the full process from Dockerfile to running container.

Quick Reference

For a printable quick reference, see the Docker cheatsheet .

CommandDescription
docker build -t name .Build image from Dockerfile in current directory
docker build -t name:tag .Build with a specific tag
docker build -f path/Dockerfile .Use a custom Dockerfile location
docker build --no-cache -t name .Build without using the layer cache
docker build --build-arg VAR=val .Pass a build-time argument
docker image lsList all local images
docker rmi image-nameRemove an image
docker run -d -p host:container imageRun a container from an image

What is Dockerfile

A Dockerfile is a text file that contains all the commands a user could run on the command line to create an image. It includes all the instructions needed by Docker to build the image.

Docker images are made up of a series of filesystem layers representing instructions in the image’s Dockerfile that make up an executable software application.

The Dockerfile takes the following form:

dockerfile
# Comment
INSTRUCTION arguments

INSTRUCTION is not case-sensitive, but the convention is to use UPPERCASE for its names.

Below is the list with a short description of some of the most used Dockerfile instructions:

  • ARG - This instruction allows you to define variables that can be passed at build-time. You can also set a default value.
  • FROM - The base image for building a new image. This instruction must be the first non-comment instruction in the Dockerfile. Each additional FROM starts a new build stage. The only exception from this rule is when you want to use a variable in the FROM argument. In this case, FROM can be preceded by one or more ARG instructions.
  • LABEL - Used to add metadata to an image, such as description, version, author, etc. You can specify more than one LABEL, and each LABEL instruction is a key-value pair.
  • RUN - The commands specified in this instruction will be executed during the build process. Each RUN instruction creates a new layer on top of the current image.
  • ADD - Used to copy files and directories from the specified source to the specified destination on the Docker image. The source can be local files or directories or a URL. If the source is a local tar archive, then it is automatically unpacked into the Docker image.
  • COPY - Similar to ADD, but it never unpacks archives and cannot fetch remote sources. It can copy from the build context, from another build stage, or from another image using --from.
  • ENV - This instruction allows you to define an environment variable.
  • CMD - Used to specify the default command that will be executed when you run a container. If the Dockerfile contains more than one CMD instruction, only the last one takes effect.
  • ENTRYPOINT - Similar to CMD, this instruction defines what command will be executed when running a container.
  • WORKDIR - This directive sets the current working directory for the RUN, CMD, ENTRYPOINT, COPY, and ADD instructions.
  • USER - Sets the username or UID for subsequent RUN instructions in the current stage and for CMD and ENTRYPOINT at runtime. Use --chown with COPY or ADD to set file ownership.
  • VOLUME - Creates a mount point for data that should be stored outside the container filesystem. The Dockerfile cannot set a host directory. You choose it with the -v option when you run the container.
  • EXPOSE - Documents the port on which the container listens at runtime. It does not publish the port. Publishing is done with the -p option of docker run.

For a complete reference and detailed explanation of Dockerfile instructions, see the official Dockerfile reference page.

Ubuntu 26.04 Dockerfile Example

The most common scenario when creating Docker images is to pull an existing image from a registry (usually from Docker Hub) and specify the changes you want to make on the base image. Popular base images include Alpine, which is very small, and full distribution images such as Ubuntu and Debian.

Info
Docker Hub is a cloud-based registry service used for storing and distributing Docker images in public or private repositories.

In this example, we will create a Docker image for the Redis server. We will use Ubuntu 26.04 as a base image.

First, create a directory that will contain the Dockerfile and all the necessary files:

Terminal
mkdir ~/redis_docker

Navigate to the directory and create the following Dockerfile:

Terminal
cd ~/redis_docker
nano Dockerfile
Dockerfiledockerfile
FROM ubuntu:26.04

RUN apt-get update && \
    apt-get install -y --no-install-recommends redis-server && \
    rm -rf /var/lib/apt/lists/*

EXPOSE 6379

CMD ["redis-server", "--protected-mode", "no"]

Here is what each line in the Dockerfile does:

  • Line 1 defines the base image.
  • The RUN instruction on line 3 updates the apt index, installs the redis-server package without recommended packages, and removes the downloaded package lists to keep the image smaller.
  • The EXPOSE instruction documents the port on which the Redis server listens.
  • The CMD instruction on the last line sets the default command that will be executed when the container starts. Protected mode is disabled so Redis accepts connections from outside the container.

Save the file and close the editor.

Building the Image

The next step is to build the image. To do so, run the following command from the directory where the Dockerfile is located:

Terminal
docker build -t linuxize/redis .

The option -t specifies the image name and optionally a username and tag in the username/imagename:tag format. The dot at the end is the build context.

On current Docker versions, docker build uses the BuildKit builder. The output of the build process will look something like this:

output
[+] Building 21.2s (6/6) FINISHED                                docker:default
 => [internal] load build definition from Dockerfile                       0.0s
 => => transferring dockerfile: 236B                                       0.0s
 => [internal] load metadata for docker.io/library/ubuntu:26.04            1.5s
 => [internal] load .dockerignore                                          0.0s
 => => transferring context: 2B                                            0.0s
 => [1/2] FROM docker.io/library/ubuntu:26.04@sha256:f144425ff09be612d6d9  9.5s
 => => resolve docker.io/library/ubuntu:26.04@sha256:f144425ff09be612d6d9  0.0s
 => => sha256:5fa77813dc560f299656013d742628128cac51fdfda9793 392B / 392B  0.2s
 => => sha256:06db5ba23986104cd78dcfa853ddf2354c943d2ef 40.86MB / 40.86MB  9.0s
 => => extracting sha256:06db5ba23986104cd78dcfa853ddf2354c943d2efefc333e  0.5s
 => => extracting sha256:5fa77813dc560f299656013d742628128cac51fdfda97934  0.0s
 => [2/2] RUN apt-get update &&     apt-get install -y --no-install-recom  9.5s
 => exporting to image                                                     0.7s
 => => exporting layers                                                    0.2s
 => => exporting manifest sha256:905e87a396b9f60919e41bb18b855259b2908f06  0.0s
 => => exporting config sha256:0ba7cf572ca745e2b3df3554d47ea4b2f9472ce03f  0.0s
 => => exporting attestation manifest sha256:4e547b8d7590e0bb18605ef66ab6  0.0s
 => => exporting manifest list sha256:53296c6a1d2bf33ddaba24a940af91f14e4  0.0s
 => => naming to docker.io/linuxize/redis:latest                           0.0s
 => => unpacking to docker.io/linuxize/redis:latest                        0.5s

Each [1/2] and [2/2] line is one step from the Dockerfile. The first step pulls the ubuntu:26.04 base image, and the second runs the RUN instruction. The last lines show the image being exported and tagged as linuxize/redis:latest.

When the build process is completed, the new image will be listed in the image list:

Terminal
docker image ls
output
IMAGE                   ID             DISK USAGE   CONTENT SIZE   EXTRA
linuxize/redis:latest   53296c6a1d2b        189MB         44.4MB

DISK USAGE is the total space used by the stored image, including both compressed content and unpacked layers when using the containerd image store. CONTENT SIZE is the size of the compressed image content. Docker versions older than 29 print a different table with REPOSITORY, TAG, and SIZE columns.

If you want to push the image to Docker Hub, see Push images to a repository .

Build Context and Custom Dockerfile Path

When you run docker build, the last argument is the build context. Local source paths for COPY and ADD are relative to the root of this context and cannot access host files outside it. COPY --from can read from other build stages, images, or named contexts, and ADD can fetch remote URLs.

By default, Docker looks for a file named Dockerfile in the root of the context. To use a Dockerfile with a different name or location, specify its path with the -f option:

Terminal
docker build -f Dockerfile.dev -t linuxize/redis:dev .

The context and the Dockerfile do not have to be in the same place. The following command builds from the ~/redis_docker directory while you are in your home directory:

Terminal
docker build -f redis_docker/Dockerfile -t linuxize/redis redis_docker

Running a Container

This example requires Docker 28.0 or later because older versions can expose ports published to localhost to other devices on the same local network. See Docker’s port publishing documentation for details. To run a container from the image, use:

Terminal
docker run -d -p 127.0.0.1:6379:6379 --name redis linuxize/redis

The -d option tells Docker to run the container in detached mode, the -p 127.0.0.1:6379:6379 option publishes port 6379 on the host loopback interface, and the --name redis option specifies the container name. The last argument linuxize/redis is the name of the image used to run the container. For more options, see our guide on the docker run command .

Warning
The Redis server in this example has no password and protected mode is disabled. Keep the port bound to 127.0.0.1. A port published as -p 6379:6379 listens on all interfaces. Docker adds its own firewall rules, and the port is reachable from the network even when UFW blocks it. If you set a password with requirepass, keep it in an .env file that is listed in .gitignore, not in the Dockerfile.

When the container starts, use the following command to list all running containers :

Terminal
docker container ls
output
CONTAINER ID   IMAGE            COMMAND                  CREATED         STATUS         PORTS                      NAMES
42723a119aab   linuxize/redis   "redis-server --prot…"   3 seconds ago   Up 3 seconds   127.0.0.1:6379->6379/tcp   redis

The PORTS column shows that port 6379 is published on 127.0.0.1 only.

To verify that everything works as it should, run redis-cli inside the container with docker exec :

Terminal
docker exec redis redis-cli ping
output
PONG

The Redis server responds with PONG, which means it is running and accepting connections.

Excluding Files with .dockerignore

BuildKit transfers only changed files between builds and skips files that the build does not use. A broad instruction such as COPY . . can still include large directories such as .git or node_modules, increasing transfer time and image size. Files such as .env can also end up in the image if a COPY instruction picks them up.

To exclude files and directories, create a .dockerignore file in the root of the build context:

.dockerignoretxt
.git
node_modules
*.log
.env

Each line is a pattern that matches files or directories relative to the context. The syntax is similar to the one of Git’s .gitignore file .

Multi-Stage Builds

A multi-stage build uses more than one FROM instruction in the same Dockerfile. Each FROM starts a new stage, and you can copy files from one stage into another. This way, you can compile an application in an image with the full toolchain and ship only the result.

The following example builds a small Go program. Create a new directory named hello with a main.go file:

main.gogo
package main

import "fmt"

func main() {
	fmt.Println("Hello from a multi-stage build")
}

In the same directory, create the following Dockerfile:

Dockerfiledockerfile
FROM golang:1.26 AS build
WORKDIR /src
COPY main.go .
RUN CGO_ENABLED=0 go build -o /hello main.go

FROM scratch
COPY --from=build /hello /hello
CMD ["/hello"]

The first stage starts from the golang:1.26 image and names the stage build. It copies main.go into /src and compiles a static binary. The second stage starts from scratch, an empty image, and copies only the compiled binary from the build stage.

Build the image and run it:

Terminal
docker build -t linuxize/hello .
docker run --rm linuxize/hello
output
Hello from a multi-stage build

List the images to compare the sizes:

Terminal
docker image ls
output
IMAGE                   ID             DISK USAGE   CONTENT SIZE   EXTRA
linuxize/hello:latest   85de01c2dd1e       3.67MB         1.33MB
linuxize/redis:latest   53296c6a1d2b        189MB         44.4MB   U

The linuxize/hello image is only 3.67MB. The Go compiler from the first stage is not part of the final image. The U in the EXTRA column means the image is in use by a container. For more examples, see the official multi-stage builds documentation.

Troubleshooting

Cannot connect to the Docker daemon or permission denied while trying to connect to the docker API
The first message means the Docker service is not running. Start it with sudo systemctl start docker. The second message means your user does not have permission to access the Docker socket. Add your user to the docker group with sudo usermod -aG docker $USER and log out and back in. Members of the docker group have root-level access to the host, so add only trusted users.

failed to resolve source metadata or pull access denied
The base image in FROM does not exist or the tag is incorrect, for example ubuntu:26.4 instead of ubuntu:26.04. Check the image name and tag on Docker Hub and update the FROM line accordingly.

'docker buildx build' requires 1 argument
The build context is missing from the command. Add the dot at the end to use the current directory: docker build -t name ..

Build succeeds but changes are not reflected
Changes to files used by COPY or ADD invalidate the affected cached layers automatically. An unchanged RUN instruction, such as one that installs packages, can still reuse an old result. Run docker build --no-cache -t name . if you need those commands to execute again. Recreate an existing container to use the rebuilt image.

failed to read dockerfile: open Dockerfile: no such file or directory
The build context does not contain a file named Dockerfile. Run the command from the directory containing the Dockerfile, or specify its path with -f. For example: docker build -f path/to/Dockerfile ..

FAQ

What is the difference between CMD and ENTRYPOINT?
ENTRYPOINT defines the main executable that always runs when the container starts. CMD provides default arguments that can be overridden at runtime. Use ENTRYPOINT for the command and CMD for its default flags, or use CMD alone for a fully overridable default command.

What is the difference between COPY and ADD?
Prefer COPY for copying local files and directories, since it does exactly what it says. Use ADD only when you need its extra features: fetching a remote URL or auto-extracting a local tar archive. Using COPY makes Dockerfiles easier to reason about. See our full comparison of Dockerfile COPY vs ADD for the details.

How do I reduce the size of a Docker image?
Use a smaller base image such as alpine instead of ubuntu. Combine RUN commands into a single layer and remove the package lists in the same step (as shown in the example above). For compiled applications, use a multi-stage build to keep only the runtime artifacts in the final image.

Conclusion

This tutorial covered the basics of using Dockerfiles to build Docker images and run containers from them. To learn more about recommended best practices, see Best practices for writing Dockerfiles , and to run several containers together, see our guide on Docker Compose .

Linuxize Weekly Newsletter

A quick weekly roundup of new tutorials, news, and tips.

About the authors

Dejan Panovski

Dejan Panovski

Dejan Panovski is the founder of Linuxize, an RHCSA-certified Linux system administrator and DevOps engineer based in Skopje, Macedonia. Author of 1000+ Linux tutorials with 20+ years of experience turning complex Linux tasks into clear, reliable guides.

View author page