Dockerfile in practice: from your first image to docker build
Until now, all containers in this series were created from ready-made Docker Hub images — ubuntu, nginx. It’s time to learn how to build your own image, using a Dockerfile.
What is a Dockerfile
A Dockerfile is a text file where we put instructions for Docker to build a container image. By convention, instructions are always written in uppercase (FROM, RUN, CMD…).
How instructions become layers
Each Dockerfile instruction generates a new layer added to the previous ones. Think of it this way:
Base image (ex: ubuntu) → read-only layer
RUN apt-get install nginx → read-only layer
COPY config-nginx → read-only layer
────────────────────────────────
Running container → read-write layer (on top of everything)
The layers generated during the build (from FROM, RUN, COPY…) are read-only and stored inside the image — they are shared among all containers created from it. Only when a container is created does Docker add a writeable (RW) layer on top, exclusive to that container: that’s where files created or modified during execution end up, as we saw with echo in part 2 of this series.
Writing your first Dockerfile
FROM ubuntu:18.04
RUN apt-get update && apt-get install nginx -y
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
FROM ubuntu:18.04— defines the base image. It must be the first instruction in the Dockerfile.RUN apt-get update && apt-get install nginx -y— executes a command during the build, installing Nginx inside the image. EachRUNgenerates (and “commits”) a new layer.EXPOSE 80— documents which port the container will listen on. This doesn’t publish the port by itself: we still need the-pindocker run, as we saw in part 2.CMD ["nginx", "-g", "daemon off;"]— unlikeRUN,CMDdoes not execute during the build. It defines the default command executed when the container starts.
Building the image
docker image build -t meu-nginx:1.0 .
The -t gives the image a name and tag (my-nginx:1.0); the . at the end indicates that the Dockerfile and build context are in the current directory.
And to run a container from it:
docker container run -d --name meu-nginx -p 8080:80 meu-nginx:1.0
A detail that prevents giant images
If each RUN generates a permanent layer, package cache installed in one RUN will continue to occupy space in the image even if you delete it in a subsequent RUN — the previous layer, with the cache still inside, remains there. That’s why it’s common to chain installation and cleanup in the same RUN:
RUN apt-get update && apt-get install nginx -y && rm -rf /var/lib/apt/lists/*
rm -rfremoves files and directories:-rmakes the removal recursive (deletes folders and everything inside them) and-fforces removal without asking for confirmation./var/lib/apt/lists/*is the path whereaptstores the list of available packages for installation.
Summarizing the command: “delete everything inside the folder where apt stores the package list” — after installing what was needed, this cache is no longer useful inside the final image.
Conclusion
With FROM, RUN, EXPOSE, and CMD, it’s already possible to package a simple application. But the Dockerfile has many more instructions — ENTRYPOINT, ENV, COPY, VOLUME, USER, and others — which we will explore in the next part of the series.
References
- Docker Docs — Dockerfile reference — official reference for all Dockerfile instructions.
- Docker Docs — docker build — options for the build command.
- Docker Docs — Dockerfile best practices — official recommendations, including reducing the number of layers.
- LINUXtips — Demystifying Docker — course used as the basis for my studies and these notes.