What a Dockerfile is and what it does
A Dockerfile is a text file that contains instructions for building a container image. Think of it as a recipe: you list the steps Docker should follow to set up your process, and Docker reads that file and builds an image that can run anywhere.
When you write a Dockerfile, you are telling Docker: start with this base system, install these programs, copy my process files here, and run this command when the container starts. Docker then executes those instructions in order and creates a packaged image. That image is what you actually run as a container.
The point of writing a Dockerfile instead of just running commands by hand is that the file is repeatable and shareable. You can give the same Dockerfile to someone else, they run it, and they get the exact same container. You can also version it, change it, and rebuild whenever you need to.
Key Takeaways
- A Dockerfile is a text file with instructions that Docker uses to build a container image, starting from a base image and adding your process on top.
- The most common Dockerfile commands are FROM (the base image), RUN (execute a command), COPY (copy files from your computer into the container), and CMD (the command to run when the container starts).
- You create a Dockerfile in a text editor, save it as a file named exactly "Dockerfile" with no extension, and run docker build in the same directory to create an image.
- The order of instructions in a Dockerfile matters because Docker caches layers; putting things that change often near the bottom makes rebuilds faster.
- You can test a Dockerfile by building it and running the resulting image with docker run to see if your process starts correctly.
The basic structure of a Dockerfile
Every Dockerfile starts with a FROM instruction that specifies the base image. This is the operating system and pre-installed software that your container will build on top of. Common base images include ubuntu:22.04 (a Linux distribution), python:3.11 (Python already installed), or node:18 (Node.js already installed).
After FROM, you add instructions that modify the image. The most common ones are:
- RUN — executes a command inside the container during the build. Use this to install packages, read files, or set up directories.
- COPY — copies files from your computer into the container. The syntax is COPY source destination.
- WORKDIR — sets the working directory inside the container, like cd on your computer.
- CMD — specifies the default command to run when the container starts. This is usually your process.
- EXPOSE — documents which port the process listens on (this does not actually open the port; it is informational).
A minimal Dockerfile might look like this:
FROM python:3.11 WORKDIR /app COPY . . RUN pip install -r requirements.txt CMD ["python", "app.py"]
This starts with Python 3.11, sets the working directory to /app, copies everything from your current folder into the container, installs Python packages from requirements.txt, and runs app.py when the container starts.
Writing your first Dockerfile
Start by creating a new file in your project directory. Open a text editor (Notepad, VS Code, or any plain-text editor) and name the file exactly Dockerfile with no extension and a capital D.
Write the FROM instruction first. If you are containerizing a Python process, use FROM python:3.11. If it is a Node.js app, use FROM node:18. If you are not sure what your process needs, start with a general Linux image like FROM ubuntu:22.04, though you will then need to install your runtime separately.
Next, set a working directory with WORKDIR. This is where your process files will live inside the container. Use something like WORKDIR /app.
Then copy your process files into the container. If your entire project should go in, use COPY . . (copy everything from the current directory into the working directory). If you only need certain files, be specific: COPY requirements.txt . or COPY src/ ./src/.
After that, use RUN to install dependencies or set up anything your process needs. For Python, this is usually RUN pip install -r requirements.txt. For Node.js, it is RUN npm install. For a compiled language, this is where you compile your code.
Finally, add CMD to specify what runs when the container starts. For Python, use CMD ["python", "app.py"]. For Node.js, use CMD ["node", "index.js"]. The syntax is a list in square brackets with the command and its arguments as separate items.
Building an image from your Dockerfile
Once you have written your Dockerfile, save it in your project directory. Open a terminal or command prompt, navigate to that directory, and run:
docker build -t myapp:1.0 .
The -t flag gives your image a name and tag. myapp is the name, and 1.0 is the tag (usually a version number). The dot at the end tells Docker to look for the Dockerfile in the current directory.
Docker will then execute each instruction in your Dockerfile in order. You will see output for each step. If any instruction fails, Docker stops and tells you which line caused the problem. Read the error message carefully — it usually tells you exactly what went wrong, like a missing file or a package that does not exist.
Building can take a few minutes the first time, especially if you are downloading a large base image or installing many packages. Docker caches each layer, so the second time you build, it will reuse layers that have not changed and only rebuild the ones that have.
Testing your image by running a container
After the build completes successfully, you can test your image by running it as a container:
docker run myapp:1.0
This starts a container from your image and runs the CMD instruction you specified. If your process is a web server, you will need to add a port mapping so you can access it from your computer:
docker run -p 8000:5000 myapp:1.0
The -p flag maps port 8000 on your computer to port 5000 inside the container. If your process listens on a different port, adjust the second number.
Watch the output. If your process starts without errors, your Dockerfile is working. If something goes wrong, you will see error messages that tell you what to fix. Common problems include missing files, wrong file paths, or packages that failed to install.
Organizing instructions for faster rebuilds
Docker builds images in layers. Each instruction in your Dockerfile creates a new layer, and Docker caches each layer. If you rebuild the image and only one instruction has changed, Docker reuses all the unchanged layers and only rebuilds from that point forward.
This means the order of your instructions affects rebuild speed. Put instructions that change often near the bottom and instructions that rarely change near the top. For example, your base image (FROM) almost never changes, so it goes first. Your process code (COPY) changes frequently, so it should go near the end.
A better structure looks like this:
FROM python:3.11 WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "app.py"]
This copies requirements.txt first and installs packages before copying the rest of your code. When you change your process code, Docker skips the pip install step and rebuilds only the final layers. If you had copied everything first, Docker would reinstall packages every time you changed a single line of code.
Common mistakes and how to avoid them
The most common mistake is forgetting to specify a working directory, which causes your files to scatter across the container's root filesystem. Always use WORKDIR early in your Dockerfile.
Another mistake is using absolute paths in COPY or RUN commands without thinking about where you are in the container. If you set WORKDIR /app, then COPY . . copies into /app, and RUN pip install -r requirements.txt looks for requirements.txt in /app. Be consistent about where files go.
A third mistake is making your base image too large. ubuntu:22.04 is about 77 MB, but python:3.11-slim is about 125 MB and includes Python. If you do not need a full Linux distribution, use a language-specific image. If you need something even smaller, look for alpine variants, which are much lighter but require more manual setup.
Finally, do not put secrets (API keys, passwords, database credentials) directly in your Dockerfile. Anyone who has the image can read them. Use environment variables or a secrets management tool instead.
Frequently Asked Questions
What is the difference between a Dockerfile and a Docker image?
A Dockerfile is the recipe (the text file with instructions). A Docker image is the result of following that recipe (the packaged process). You build an image from a Dockerfile, and you run a container from an image.
Do I need to install Docker on my computer to write a Dockerfile?
You need Docker installed to build and test your Dockerfile, but you can write the text file itself in any editor. However, you cannot verify that it works without Docker, so install it before you test.
Can I use a Dockerfile to containerize an process written in any language?
Yes. Choose a base image that has your language's runtime installed (Python, Node.js, Java, Go, etc.), copy your code in, install dependencies, and specify the command to run. The structure is the same regardless of language.
What does the dot at the end of docker build -t myapp:1.0 . mean?
The dot tells Docker where to find the Dockerfile and the files to copy into the image. It means "the current directory". You can replace it with a path to a different directory if your Dockerfile is elsewhere.
How do I update my image after I change my process code?
Run docker build -t myapp:1.0 . again. Docker will rebuild the image using your updated code. If you want to keep the old version, use a different tag: docker build -t myapp:1.1 .