Writing a Dockerfile

intermediate
12 min

Writing a Dockerfile

So far you have run images that other people built. The real power of Docker appears when you package your own applications, and the recipe for that is the Dockerfile - a plain text file describing, step by step, how to assemble an image. In this lesson you will learn the essential instructions and build a working image for a small Python web app.

Your First Dockerfile

Create a folder containing a file named Dockerfile (no extension) with this content:

dockerfile
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 5000 CMD ["python", "app.py"]

Build and run it from that folder:

bash
docker build -t my-python-app . docker run -d -p 5000:5000 my-python-app

The trailing dot tells Docker to use the current directory as the build context - the set of files available to COPY instructions.

The Core Instructions

  • FROM sets the base image. Every Dockerfile starts here. Slim or alpine variants keep images small.
  • WORKDIR sets the working directory for subsequent instructions, creating it if needed. Prefer it over RUN cd.
  • COPY copies files from the build context into the image. COPY . . means "everything here into the current WORKDIR".
  • RUN executes a command at build time and bakes the result into a new layer - installing packages, compiling code.
  • EXPOSE documents which port the app listens on. It does not publish the port; -p still does that at run time.
  • CMD defines the default command executed when a container starts. Only the last CMD in a file takes effect.

RUN vs CMD vs ENTRYPOINT

These three confuse everyone at first:

  • RUN happens at build time and modifies the image.
  • CMD happens at run time and provides a default command that users can override: docker run my-python-app bash replaces the CMD entirely.
  • ENTRYPOINT also runs at run time, but arguments passed to docker run are appended to it instead of replacing it. A common pattern pairs them:
dockerfile
ENTRYPOINT ["python", "app.py"] CMD ["--port", "5000"]

Here the entrypoint is fixed and CMD supplies overridable default arguments.

Why COPY requirements.txt Comes First

Notice the Dockerfile copies requirements.txt and installs dependencies before copying the rest of the code. This is deliberate. Docker caches each layer, and a layer is rebuilt only when its inputs change. Application code changes constantly, but dependencies change rarely - so ordering the Dockerfile this way means day-to-day rebuilds skip the slow pip install step and finish in seconds. This is the single most valuable Dockerfile habit to learn.

The .dockerignore File

Just as git has .gitignore, Docker has .dockerignore. It keeps junk out of the build context, making builds faster and images cleaner:

.git __pycache__ *.pyc .env node_modules

Excluding secrets like .env files also prevents them from being accidentally baked into an image.

Other Useful Instructions

dockerfile
ENV APP_ENV=production # set an environment variable ARG VERSION=1.0 # build-time variable, not in the final container LABEL maintainer="you@example.com" USER appuser # run as a non-root user

ENV values persist in running containers; ARG values exist only during the build.

Key Takeaways

  • A Dockerfile is a step-by-step recipe; docker build -t name . turns it into an image.
  • FROM, WORKDIR, COPY, RUN, EXPOSE, and CMD cover ninety percent of real Dockerfiles.
  • RUN executes at build time; CMD and ENTRYPOINT define what happens at run time.
  • Copy dependency manifests before source code to exploit layer caching.
  • Use .dockerignore to keep secrets and clutter out of your images.

Next up: Building and Tagging Images, where you will master build contexts, tagging strategies, and pushing images to a registry.

Writing a Dockerfile - Docker | CodeYourCraft | CodeYourCraft