Mantra Networking Mantra Networking

Docker: Dockerfile

Docker: Dockerfile
Created By: Lauren R. Garcia

Table of Contents

  • Overview
  • Typical Dockerfile Structure
  • Common Dockerfile Instructions
  • Best Practices
  • Example Minimal Dockerfile
  • File Naming and Placement
  • Differences: COPY vs ADD
  • Conclusion

Docker: Dockerfile Overview

What Is a Dockerfile?

A Dockerfile is a text document that contains a series of commands and instructions used to assemble a Docker image. Think of it as a recipe or blueprint for building a containerized environment — specifying everything from the base operating system, to the application code, dependencies, configuration, and runtime instructions.

Why Should You Know About Dockerfiles?

  • Consistency: Dockerfiles allow you to automate exactly how applications are packaged, configured, and run, eliminating the “it works on my machine” problem across different systems.
  • Portability: By describing all the steps required to set up your app, Dockerfiles make it possible to run your software in any environment that supports Docker — whether that's a developer laptop, a testing pipeline, or a cloud provider.
  • Automation: They enable repeatable, version-controlled builds. You can store your Dockerfile in source control, track changes, and be confident that anyone building the image gets the same result.
  • Security and Best Practices: By codifying things like non-root users, environmental variables, and dependency handling, you can enforce modern software security and operational standards.

How Does a Dockerfile Work?

  1. Reads Sequentially: Docker reads the Dockerfile from top to bottom, executing each instruction in order.
  2. Builds in Layers: Each command (like FROMCOPYRUN) results in a new layer in the image. Layers are cached, making rebuilds faster by only rebuilding changed parts.
  3. Produces an Image: When the build process is complete, you have a Docker image. This image is a snapshot — a ready-to-run, self-contained package.
  4. Containers Spawn from Images: You use this image to spawn containers. Each container is an isolated, reproducible environment based on your Dockerfile’s specifications.

In summary:
A Dockerfile is an essential tool for automating application delivery with Docker. It lets you define your environment once, then reliably recreate it anywhere, enabling modern DevOps workflows and cloud-native development.

Typical Dockerfile Structure

To build a functional Docker image, your Dockerfile should be organized with the following components in this order. Here is how you can structure your Dockerfile, step by step:

  1. Set the Base Image:
    Define which parent image your build will start from. This provides the OS and potentially language runtime.
    FROM python:3.12
  2. Configure the Working Directory:
    Set the working directory where subsequent commands and file copies will be executed.
    WORKDIR /usr/local/app
  3. Copy Dependency Files:
    Bring in requirement or dependency files to allow efficient layer caching and minimize rebuilds.
    COPY requirements.txt ./
  4. Install Dependencies:
    Run installation commands to prepare your container environment.
    RUN pip install --no-cache-dir -r requirements.txt
  5. Copy Application Source Code:
    Copy over your main app files after dependencies are satisfied.
    COPY src ./src
  6. Expose Ports:
    Indicate which network ports the container will listen on.
    EXPOSE 5000
  7. Set User (Optional for Security):
    Run the container as a non-root user for added security.
    RUN useradd app
    USER app
  8. Define the Startup Command:
    Specify the default command the container runs upon startup.
    CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]

Each of these steps forms a layer in your final Docker image, helping ensure it is efficient, maintainable, and secure.

Common Dockerfile Instructions

Dockerfiles use specific instructions to define how an image is built. Here are some frequently used instructions and what they do, step by step:

  • FROM
    Specifies the base image to start your build from. It sets the foundational environment.
    FROM ubuntu:22.04
  • WORKDIR
    Sets the working directory inside the image for subsequent commands.
    WORKDIR /app
  • COPY
    Copies files or directories from your local machine into the image.
    COPY . /app
  • RUN
    Executes commands during the build, commonly used to install packages or dependencies.
    RUN apt-get update && apt-get install -y curl
  • ENV
    Sets environment variables within the image for configuration.
    ENV NODE_ENV=production
  • EXPOSE
    Documents the port(s) the container will listen on during runtime.
    EXPOSE 80
  • USER
    Defines which user to run commands or the application as (improving security).
    USER appuser
  • CMD
    Sets the default command that will run when the container starts.
    CMD ["node", "server.js"]

By combining these instructions thoughtfully, you create repeatable, efficient, and secure Docker images.

Best Practices

Following these best practices will help you create Dockerfiles that are efficient, maintainable, and secure. Move through each step as you write or review your own Dockerfiles:

  1. Use Official and Minimal Base Images:
    Start with trusted and lightweight base images to reduce security risks and improve build speed.
  2. Leverage Layer Caching:
    Copy and install dependencies separately from application code so Docker caches unchanged layers, resulting in faster builds.
    COPY requirements.txt ./
    RUN pip install -r requirements.txt
    COPY . .
  3. Add a .dockerignore File:
    Prevent unnecessary files from being copied into the image by including a .dockerignore file. This reduces image size and build context.
    .dockerignore
    __pycache__/
    node_modules/
  4. Avoid Running as Root:
    Create and switch to a non-root user to limit permissions inside the container and improve security.
    RUN useradd appuser
    USER appuser
  5. Clean Up Intermediate Files:
    Remove temporary files and caches during the build process to keep images small and efficient.
    RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
  6. Document Exposed Ports and Metadata:
    Use EXPOSE and LABEL to clarify ports used and add descriptive metadata to your image.
    EXPOSE 8080
    LABEL maintainer="yourname@example.com"
  7. Prefer COPY over ADD:
    Use COPY to add files unless you need the special features of ADD (like tar extraction).
  8. Combine RUN Instructions When Possible:
    Group several commands in a single RUN instruction to reduce image layers.
    RUN apt-get update && apt-get install -y curl git
  9. Always Pin Dependency Versions:
    Specify exact versions of packages and dependencies to ensure predictable builds.
    RUN pip install flask==2.3.2

Consistently following these steps helps prevent common issues, keeps images streamlined, and supports secure, repeatable deployments.

Example Minimal Dockerfile

Here is a simple and minimal Dockerfile example that you can use as a starting point for lightweight containers. Follow these steps to create one:

  1. Specify the base image:
    Use a small base image like Alpine Linux for minimal size.
    FROM alpine:latest
  2. Set a default command:
    Define a default command that runs when the container starts.
    CMD ["sh"]

This Dockerfile runs a minimal shell environment inside an Alpine container, resulting in a very small image.

File Naming and Placement

To ensure your Docker build works as expected, it's important to follow these steps when naming and placing your Dockerfile:

  1. Name the File Correctly:
    The file must be named exactly Dockerfile—with no file extension like .txt or .docker.
    Dockerfile ✅
    Dockerfile.txt ❌
    Dockerfile.docker ❌
  2. Use the Root of Your Project:
    Place the Dockerfile in the root directory of your project, alongside your main source code and configuration files.
    This gives Docker access to the entire context of your application during build.
    /
    ├── Dockerfile
    ├── app.py
    ├── requirements.txt
    └── src/
        └── ...
  3. Include a .dockerignore File:
    To control what gets sent to the Docker daemon during build, create a .dockerignore file in the same folder.
    This helps reduce build time and prevents accidental inclusion of sensitive or unnecessary files.
    .dockerignore contents:
    __pycache__/
    *.log
    node_modules/
    .env
        
  4. Alternative File Names (Advanced):
    If you need to use a different file name, you can specify it with the -f flag when building the image:
    docker build -f Dockerfile.dev -t my-app .
    But by default, Docker looks for a file called Dockerfile.

Keeping your Dockerfile properly named and placed ensures consistent builds and easier project upkeep.

Differences: COPY vs ADD

Both COPY and ADD are Dockerfile instructions used to transfer files into your image, but they have important differences. Here’s a breakdown to help you decide which to use:

  1. Basic Functionality:
    COPY copies files and directories from your local build context into the image.
    ADD can do everything COPY does, plus it has some additional features.
  2. Remote File Support:
    ADD supports copying files from remote URLs.
    COPY does not support URLs and only copies local files.
  3. Tar Archive Extraction:
    When ADD copies a local tar file (e.g., .tar, .tar.gz), it will automatically extract its contents into the image.
    COPY copies the archive as-is without extracting.
  4. Layer Impact and Transparency:
    Since ADD has more complex behaviors, it is sometimes less transparent and can result in unexpected builds.
    COPY is simpler and preferred when you just need to add local files.
  5. Best Practice:
    Use COPY for most cases where you need to copy local files or directories.
    Use ADD only if you need to:
    • Automatically extract a local tar archive during image build.
    • Download and add a remote file by specifying a URL.
  6. Example:
    COPY app /app — Copies the local app directory into the image.
    ADD https://example.com/package.tar.gz /tmp/ — Downloads and adds the remote tar.gz file.

By choosing between these instructions carefully, you can keep your builds predictable and efficient.

Conclusion

Throughout this blog post, we explored the foundational elements of working with Dockerfiles — the blueprint for creating Docker container images. You’ve seen not only how Dockerfiles are structured, but also the best ways to use them effectively in your development and deployment workflows.

Here’s a quick recap of what we covered:

  • A Dockerfile is a step-by-step set of instructions to build a container image.
  • The structure typically starts with setting a base image, defining a working directory, copying files, installing dependencies, and setting commands to run the app.
  • Instructions like FROMCOPYRUNCMD, and EXPOSE serve specific roles in shaping your image.
  • Following practices like pinning versions, using .dockerignore, and avoiding running as root improves security and consistency.
  • A minimal Dockerfile can start with just a couple of lines using something like the Alpine image.
  • Naming the Dockerfile correctly and placing it in the project root ensures Docker works as expected.
  • When deciding between COPY and ADD, go with COPY unless you need features like remote downloads or tar extraction.

By now, you should feel confident writing optimized, readable, and reusable Dockerfiles for your own projects. Whether you’re building production-grade applications or lightweight test environments, the steps we've covered will help make your images smaller, more secure, and easier to maintain.

Thanks for following along! If you enjoyed this walkthrough, stay tuned for future posts on Docker Compose, multi-stage builds, and container orchestration. Happy shipping! 🐳