4. Writing Your First Dockerfile | The Complete Docker Handbook.

7 mins read
0 Like
50 Views

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

Python
1from 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 Text
1# 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

  1. FROM python:3.9-slim: We start with a lightweight version of Python 3.9. This is our foundation layer.
  2. WORKDIR /app: Creates a directory /app inside the image and sets it as the current directory. All subsequent commands run here.
  3. COPY requirements.txt .: Copies the requirements file from your host to the image's /app directory.
    • 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.
  4. RUN pip install...: Installs Python packages. This happens during the build.
  5. COPY . .: Copies everything else (like app.py) from the host to the image.
  6. 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.

Bash
docker 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.

Bash
docker run -p 8080:8000 my-python-app
  • -p 8080:8000: Port Mapping. The app inside the container listens on port 8000. This flag maps your host's port 8080 to the container's port 8000.
  • 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, the bash command replaces the CMD.
  • 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 Text
1ENTRYPOINT ["python"]
2CMD ["app.py"]

Running docker run my-app:

  • Executes: python app.py

Running docker run my-app script.py:

  • Executes: python script.py (The CMD is replaced by the argument, but ENTRYPOINT stays).

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 Text
1__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

  1. Cannot find module / ImportError

    • Cause: You tried to run the app before installing dependencies.
    • Fix: Ensure RUN pip install... comes before CMD.
  2. File not found

    • Cause: Your WORKDIR is incorrect, or you are copying files to the wrong location.
    • Fix: Check your COPY paths and WORKDIR.
  3. 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, and CMD.
  • Build an image using docker build.
  • Understand the difference between CMD and ENTRYPOINT.
  • Create a .dockerignore file 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

Share:

Comments

0
Join the conversation

Sign in to share your thoughts and connect with other readers

No comments yet

Be the first to share your thoughts!