What happens when you build a Docker image

Building a Docker image means taking your application code, its dependencies, and configuration, then packaging them into a single file that Docker can run anywhere. When you run the docker build command, Docker reads instructions from a file called a Dockerfile, executes them in order, and creates layers that stack on top of each other. The result is an image — a blueprint that you can use to spin up containers, which are the actual running instances of your application.

The image itself is not running anything. It sits on your computer or in a registry (a storage service for images) until you tell Docker to create a container from it. Think of the image as a recipe and the container as the meal you make from that recipe.

Key Takeaways

  • A Dockerfile is a text file with instructions that Docker follows to build an image, starting with a base image and adding layers for dependencies and code.
  • The docker build command reads your Dockerfile and creates an image, which you tag with a name and version so you can reference it later.
  • Each instruction in the Dockerfile creates a new layer in the image, and Docker caches these layers so rebuilds are faster if nothing changed.
  • You push images to a registry like Docker Hub so other machines or team members can pull and run them without rebuilding.

Creating a Dockerfile

A Dockerfile is a plain text file with no extension. You create it in the root directory of your project and name it exactly Dockerfile (capital D, no dot). Inside, you write instructions that tell Docker how to assemble your image.

The first instruction is almost always FROM, which specifies a base image to build on top of. For example, FROM python:3.11 starts with an official Python image that already has Python 3.11 installed. You could also use FROM ubuntu:22.04 to start with a bare Linux system and install everything yourself, but that takes longer and is more error-prone.

After FROM, you typically add WORKDIR to set the working directory inside the container — this is where your application code will live. Then you use COPY or ADD to bring files from your computer into the image, and RUN to execute commands like installing packages or compiling code. Finally, CMD or ENTRYPOINT tells Docker what command to run when the container starts.

A concrete example: Python application

Say you have a Python script called app.py that needs the Flask library. Your Dockerfile might look like this:

FROM python:3.11 WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY app.py . CMD ["python", "app.py"]

Line by line: start with Python 3.11, set the working directory to /app inside the container, copy your requirements.txt file into the image, install the packages listed in that file, copy your app.py script, and when the container runs, execute python app.py.

You would save this as a file named Dockerfile in the same directory as your app.py and requirements.txt. Then you run docker build -t my-app:1.0 . The -t flag tags the image with a name and version (my-app:1.0), and the . tells Docker to look for the Dockerfile in the current directory.

Understanding layers and caching

Each instruction in your Dockerfile creates a layer — a thin slice of the image. Docker stacks these layers on top of each other. The FROM instruction creates the base layer, COPY creates another, RUN creates another, and so on. This layered approach is what makes Docker efficient.

When you rebuild an image, Docker checks whether each layer has changed. If the Dockerfile instruction and the files it references are identical to the last build, Docker reuses the cached layer instead of rebuilding it. This means if you only changed your app.py file, Docker skips reinstalling Python and your dependencies — it just copies the new file and rebuilds from that point forward.

This is why the order of instructions matters. Put things that change rarely (like the base image and system dependencies) near the top, and things that change often (like your application code) near the bottom. If you put COPY app.py at the top and RUN pip install at the bottom, every change to app.py would force a full reinstall of dependencies.

Building the image with docker build

Open a terminal, navigate to the directory containing your Dockerfile, and run docker build -t image-name:tag . Replace image-name with something descriptive (like my-web-app) and tag with a version (like 1.0 or latest). The period at the end tells Docker to use the Dockerfile in the current directory.

Docker will print output showing each step: downloading the base image, running each instruction, and reporting the ID of each layer. If any instruction fails, the build stops and tells you which line caused the problem. Fix the Dockerfile and run the build command again.

Once the build completes, you can list your images with docker images and see your new image listed with the name and tag you gave it. The image is now stored on your computer and ready to run as a container.

Pushing your image to a registry

If you want to run your image on another machine or share it with your team, you push it to a registry — a central storage service for Docker images. Docker Hub is the most common public registry. You can also use private registries like Amazon ECR, Google Container Registry, or a self-hosted registry.

To push to Docker Hub, first create an account and log in from your terminal with docker login. Then tag your image with your Docker Hub username: docker tag my-app:1.0 username/my-app:1.0 Finally, push it: docker push username/my-app:1.0 The image is now on Docker Hub, and anyone with access can pull it and run it.

Common mistakes and how to avoid them

One frequent mistake is copying too much into the image. If you COPY . . to copy your entire project directory, you might accidentally include large files, temporary directories, or sensitive data like API keys. Create a .dockerignore file (similar to .gitignore) and list the files and folders Docker should skip: node_modules, .git, .env, __pycache__, and so on.

Another mistake is running your application as the root user. By default, Docker runs commands as root, which is a security risk if your container is compromised. Create a non-root user in your Dockerfile with RUN useradd -m appuser and then switch to it with USER appuser before your CMD instruction.

A third mistake is making images larger than necessary. Use a slim or alpine base image instead of a full one — FROM python:3.11-alpine is much smaller than FROM python:3.11 because it strips out unnecessary tools. Combine multiple RUN instructions into one with && to reduce the number of layers: RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* This installs packages and cleans up the package cache in a single layer.

Frequently Asked Questions

What is the difference between a Dockerfile and a Docker image?

A Dockerfile is a text file with instructions. A Docker image is what you get after running those instructions — it is a packaged, ready-to-run file. The Dockerfile is like source code; the image is like a compiled program.

Do I need to rebuild the image every time I change my code?

Yes, you need to rebuild the image if you want the container to run the new code. However, Docker caches layers, so if only your application code changed and nothing else, the rebuild is fast — Docker reuses the cached layers for the base image and dependencies.

Can I build a Docker image without a Dockerfile?

You can use docker commit to create an image from a running container, but this is not recommended for production. Dockerfiles are the standard because they are version-controlled, repeatable, and clear about what is inside the image.

What does the tag in docker build -t mean?

The tag is a name and version for your image, separated by a colon. my-app:1.0 means the image is called my-app and the version is 1.0. You can have multiple tags pointing to the same image, and if you do not specify a tag, Docker defaults to latest.

Why is my image so large?

Large base images, unnecessary dependencies, and uncleaned package caches are the usual culprits. Use alpine or slim variants of base images, remove packages you do not need, and clean up package managers after installing. You can also use multi-stage builds to copy only the final application into a fresh image, leaving behind build tools and temporary files.