Back to Blog
Lesson 13 of the CI/CD: Continuous Integration from Scratch course
DevOpsJuly 13, 20264 min read

Introduction to Docker: Building Your First Container Image

Stop saying "it works on my machine." Learn how to use Docker to create reproducible environments and build your first container image today.

DockerCI/CDDevOpsContainersAutomation

Previously in this course, we mastered running tests in CI and ensured our pipeline triggers correctly on every push. While we now have automated feedback, we still rely on the host machine's environment—the specific version of Python, system libraries, and OS packages installed on the GitHub Runner.

This is where Docker changes the game. By introducing containerization, we decouple our application from the host environment, achieving true environment parity—the guarantee that the code running on your laptop is exactly what runs in production.

Why Containerization Matters

In traditional development, you might install Python 3.10 on your Mac, while your CI runner uses Ubuntu 22.04 with Python 3.8. These subtle differences lead to the infamous "it works on my machine" syndrome.

Docker solves this by packaging your application, its dependencies, and its runtime into a single, immutable unit called an image. When you run this image, it becomes a container—a process isolated from the host OS, ensuring that your app sees the exact same file system and libraries regardless of where it executes.

Anatomy of a Dockerfile

A Dockerfile is a text document containing the instructions to build an image. Think of it as a recipe.

Create a file named Dockerfile (no extension) in the root of your project:

Dockerfile
# 1. Start from a base image
FROM python:3.11-slim

# 2. Set the working directory inside the container
WORKDIR /app

# 3. Copy local files into the container
COPY . .

# 4. Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# 5. Define the command to run the app
CMD ["python", "app.py"]

Breaking down the instructions:

  • FROM: Sets the foundation. We use python:3.11-slim because it’s a lightweight version of Debian with Python pre-installed.
  • WORKDIR: Creates a directory inside the container. All subsequent commands happen here.
  • COPY: Moves files from your local machine into the container's virtual file system.
  • RUN: Executes commands during the build process (like installing packages).
  • CMD: Specifies what the container should do when it starts.

Hands-on Exercise: Build Your First Image

Ensure you have Docker Desktop installed. Open your terminal in your project root and execute the following:

  1. Build the image: docker build -t my-app . The -t flag tags your image with a name (e.g., my-app). The . tells Docker to look for the Dockerfile in the current directory.

  2. Verify the build: Run docker images. You should see my-app listed in the output.

  3. Run the container: docker run --rm my-app The --rm flag automatically removes the container once it stops, keeping your machine clean.

Common Pitfalls

  • Ignoring the .dockerignore file: Just like .gitignore, you should create a .dockerignore file. Without it, you might accidentally copy your local venv/ or .git/ folder into the image, making it unnecessarily huge.
  • Running as root: By default, containers run as the root user. For production-grade security, you should create a non-privileged user inside your Dockerfile.
  • Layer Bloat: Each instruction (RUN, COPY) creates a layer. If you change a file early in the file, every layer after it must be rebuilt. Order your instructions from least-to-most frequently changed (e.g., copy requirements.txt and install dependencies before copying your source code).

FAQ

Q: Is a container a Virtual Machine (VM)? A: No. VMs include a full guest operating system (heavy). Containers share the host's kernel, making them significantly faster and lighter.

Q: Can I use Docker for non-Python apps? A: Absolutely. Whether it's Node.js, Go, or Rust, the principle of packaging the runtime and dependencies remains the same.

Q: How do I know which base image to choose? A: Start with the official "slim" version of your language (e.g., node:18-slim or python:3.11-slim). They offer the best balance between size and utility.

Recap

We’ve moved beyond simple script execution to true environment packaging. By creating a Dockerfile and building a local image, you have ensured that your code's environment is now version-controlled and reproducible. This foundational step is critical for containerization basics and sets us up to move our CI environment into the cloud.

Up next: Dockerizing the Pipeline — we'll take this local image and teach GitHub Actions how to use it for our automated testing.

Similar Posts