4. Writing Your First Dockerfile | The Complete Docker Handbook.
Welcome to Article 4 of The Complete Docker Handbook.
In Article 3, we explored Docker images deeply. You learned about layers, tags, and how to manage images pulled from Docker Hub. But relying on pre-made images limits you. To containerize your specific applications, you need to create your own images.
That's where the Dockerfile comes in.
In this article, you will learn the syntax of a Dockerfile, build your first custom image, and understand the critical difference between build-time and run-time instructions.
What is a Dockerfile?
A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image.
Think of it as a recipe.
- Ingredients: Base image, code files, dependencies.
- Instructions: Install packages, copy files, set environment variables.
- Result: A Docker Image.
When you run docker build, Docker reads this file and executes the instructions step-by-step, creating a new layer for each instruction.
Key Dockerfile Instructions
While there are many commands, you only need to master a few core instructions to get started.
| Instruction | Description |
|---|---|
FROM | Sets the base image. Must be the first command (usually). |
WORKDIR | Sets the working directory for subsequent instructions. |
COPY | Copies files from your host machine into the image. |
RUN | Executes commands during the build process (e.g., installing packages). |
CMD | Sets the default command to run when the container starts. |
ENTRYPOINT | Configures the container to run as an executable. |
EXPOSE | Informs Docker that the container listens on specific ports (documentation only). |
Practical Example: Containerizing a Python App
Let's containerize a simple Python application. We will create a small script that runs a web server.
Step 1: Create the Application Files
Create a new folder named my-docker-app and create the following files inside it.
1. app.py
Python1from http.server import SimpleHTTPRequestHandler, HTTPServer
2
3class MyHandler(SimpleHTTPRequestHandler):
4 def do_GET(self):
5 self.send_response(200)
6 self.send_header('Content-type', 'text/html')
7 self.end_headers()
8 self.wfile.write(b'<h1>Hello from Docker!</h1>')
9
10if __name__ == '__main__':
11 server = HTTPServer(('0.0.0.0', 8000'), MyHandler)
12 print('Server running on port 8000...')
13 server.serve_forever()
2. requirements.txt
(Since we used standard libraries in this simple example, this can be empty, but it's good practice to have it)
Plain Text# No external dependencies for this simple example
Step 2: Create the Dockerfile
In the same folder, create a file named Dockerfile (no extension).
Plain Text1# 1. Base Image 2FROM python:3.9-slim 3 4# 2. Set Working Directory 5WORKDIR /app 6 7# 3. Copy requirements first (for caching benefits) 8COPY requirements.txt . 9 10# 4. Install dependencies 11RUN pip install --no-cache-dir -r requirements.txt 12 13# 5. Copy the rest of the application code 14COPY . . 15 16# 6. Command to run when container starts 17CMD ["python", "app.py"]
Step 3: Breakdown of the Dockerfile
FROM python:3.9-slim: We start with a lightweight version of Python 3.9. This is our foundation layer.WORKDIR /app: Creates a directory/appinside the image and sets it as the current directory. All subsequent commands run here.COPY requirements.txt .: Copies the requirements file from your host to the image's/appdirectory.- Pro Tip: We copy this before the rest of the code. Why? Because if your code changes but dependencies don't, Docker can use the cached layer for
pip install. This speeds up builds significantly.
- Pro Tip: We copy this before the rest of the code. Why? Because if your code changes but dependencies don't, Docker can use the cached layer for
RUN pip install...: Installs Python packages. This happens during the build.COPY . .: Copies everything else (likeapp.py) from the host to the image.CMD ["python", "app.py"]: This is the default command that runs when you start the container.
Step 4: Building the Image
Now, turn your Dockerfile into an image. Navigate to your my-docker-app folder in the terminal.
Bashdocker build -t my-python-app .
Breaking down the command:
docker build: The command to build an image.-t my-python-app: Tags the image with the name "my-python-app"..: The build context. The dot tells Docker to look for the Dockerfile and files in the current directory.
Watch the Output:
You will see Docker executing each step (Step 1/6 : FROM...). If you run the build command again, you'll notice it says CACHED. This is the power of layers!
Step 5: Running Your Container
Now that you have the image, run it just like you did with ubuntu in Article 2.
Bashdocker run -p 8080:8000 my-python-app
-p 8080:8000: Port Mapping. The app inside the container listens on port8000. This flag maps your host's port8080to the container's port8000.- Open your browser and go to
http://localhost:8080. You should see "Hello from Docker!"
Critical Concept: CMD vs. ENTRYPOINT
This is the most common point of confusion for beginners. Both define what happens when the container starts.
1. CMD (Command)
- Purpose: Provides defaults for an executing container.
- Behavior: Can be overridden easily. If you run
docker run my-image bash, thebashcommand replaces theCMD. - Use Case: When you want to allow the user to override the main command easily.
2. ENTRYPOINT
- Purpose: Configures the container to run as an executable.
- Behavior: Harder to override. If you add arguments to
docker run, they are passed as arguments to the ENTRYPOINT, not replacing it. - Use Case: When you want a specific executable to always run (e.g., a web server or a specific script).
Example Scenario
Dockerfile:
Plain Text1ENTRYPOINT ["python"] 2CMD ["app.py"]
Running docker run my-app:
- Executes:
python app.py
Running docker run my-app script.py:
- Executes:
python script.py(TheCMDis replaced by the argument, butENTRYPOINTstays).
Recommendation: For most general applications, use CMD. Use ENTRYPOINT for specialized tools or base images.
The .dockerignore File
Just like .gitignore, you should create a .dockerignore file in your project folder.
Why?
When you run docker build, Docker sends the entire directory (the context) to the Docker Daemon. If you have large folders like node_modules, .git, or __pycache__, this slows down the build and bloats your image.
Create a .dockerignore file:
Plain Text1__pycache__ 2*.pyc 3.git 4.env 5venv
This ensures these files are not copied into your image during the COPY . . step.
Troubleshooting Common Build Errors
Cannot find module/ImportError- Cause: You tried to run the app before installing dependencies.
- Fix: Ensure
RUN pip install...comes beforeCMD.
File not found- Cause: Your
WORKDIRis incorrect, or you are copying files to the wrong location. - Fix: Check your
COPYpaths andWORKDIR.
- Cause: Your
Permission denied- Cause: Trying to write to a read-only layer.
- Fix: Ensure you are writing to the working directory, not system folders like
/usr.
Summary Checklist
By the end of this article, you should be able to:
- Explain what a Dockerfile is.
- Write a Dockerfile using
FROM,WORKDIR,COPY,RUN, andCMD. - Build an image using
docker build. - Understand the difference between
CMDandENTRYPOINT. - Create a
.dockerignorefile to optimize builds.
What's Next?
You have successfully built a custom image! But is it a good image?
Beginner Dockerfiles often result in large, slow, and insecure images. In Article 5, we will focus on Optimizing Dockerfiles.
- How to reduce image size from 1GB to 50MB.
- Multi-stage builds.
- Security best practices (running as non-root).
Link: Read Article 5: Optimizing Dockerfiles (Best Practices)
Challenge: Try containerizing a simple Node.js or Go application using what you learned today. Share your Dockerfile in the comments!
Next Up: Optimizing Dockerfiles