KAHIBARO
Discord Login Register

21.6. Building Images

Why Building Images Matters

When you build a Docker image, you are creating a repeatable, portable package of your backend application and everything it needs to run.

You can then:

This chapter focuses on the practical process of building images from a Dockerfile, verifying them, and avoiding common mistakes. You will use the concepts from the previous Docker chapters, but here the focus is specifically on image creation and good practices.


The `docker build` Command

The main command to create an image is:

bash
docker build -t my-backend-app:1.0 .

Let us break this down:

PartMeaning
docker buildBuild a Docker image from a Dockerfile.
-t my-backend-app:1.0Tag the image name is my-backend-app, tag is 1.0.
.Build context the current directory (files sent to the Docker daemon).

Important rule: The last argument of docker build is the build context directory, not the path to the Dockerfile. Docker sends everything in that directory (except ignored files) to the Docker daemon.
Keep your build context small to avoid slow builds and accidental leaks of secrets.

You can specify a different Dockerfile:

bash
docker build -f Dockerfile.prod -t my-backend-app:prod .

If your Dockerfile is in docker/Dockerfile, you can run:

bash
docker build -f docker/Dockerfile -t my-backend-app:1.0 .

Understanding the Build Context

Docker does not read files directly from your filesystem during build. Instead, it sends the context directory to the daemon.

If your project structure is:

text
my-backend/
  app/
    main.py
    requirements.txt
  docker/
    Dockerfile
  .git/
  .env
  README.md

Typical build command:

bash
cd my-backend
docker build -f docker/Dockerfile -t my-backend-app:1.0 .

Here:

Use a .dockerignore file in the context directory:

text
# .dockerignore
.git
.env
__pycache__
*.pyc
node_modules
tests

This keeps the image build:

A Simple Backend Image Example

Assume a basic Python backend using FastAPI:

text
my-backend/
  app/
    main.py
  requirements.txt
  Dockerfile

app/main.py:

python
from fastapi import FastAPI
app = FastAPI()
@app.get("/ping")
def ping():
    return {"message": "pong"}

requirements.txt:

text
fastapi
uvicorn[standard]

Dockerfile:

dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Build and run:

bash
docker build -t fastapi-backend:1.0 .
docker run -p 8000:8000 fastapi-backend:1.0

Test:

bash
curl http://localhost:8000/ping
# {"message":"pong"}

Tagging Images Correctly

Image names usually look like:

text
repository/name:tag

Examples:

Image nameMeaning
my-backend:latestLocal image named my-backend, tag latest.
my-backend:1.0.0Versioned image, useful for releases.
username/my-backend:devOften used for Docker Hub.
registry.example.com/api:prodImage in a private registry.

You can tag several versions for the same image:

bash
docker build -t my-backend:1.0.0 .
docker tag my-backend:1.0.0 my-backend:latest

Now docker run my-backend will use the latest tag.

Important rule: Do not rely on latest in production. Always use a specific tag, for example 1.0.0 or a commit hash, so that you know exactly which version is running.


Using Build Arguments

Sometimes you need parameters during build, without baking them into the image code.

In Dockerfile:

dockerfile
ARG APP_ENV=development
RUN echo "Building for environment: ${APP_ENV}"

Build with a custom value:

bash
docker build -t my-backend:staging --build-arg APP_ENV=staging .

Key points:

If you want runtime values, use environment variables when running containers, not ARG.


Layer Caching and Faster Builds

Each Dockerfile instruction creates a layer. Docker caches these layers and can reuse them if nothing changed.

Simplified example:

dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Build 1:

bash
docker build -t my-backend:1.0 .

All layers are built.

If you now only change app/main.py and build again:

If you modify requirements.txt, Docker must re-run the pip install step, which is slower.

Important rule: Put instructions that change rarely (like installing system packages, pip install) before instructions that change often (like copying source code). This maximizes cache reuse and speeds up builds.

To force a clean build without cache:

bash
docker build --no-cache -t my-backend:clean .

This is useful if you suspect caching issues or you updated system-level dependencies.


Multi-Stage Builds

Multi-stage builds let you:

This is very useful when building compiled code, but it is also useful for interpreted languages to drop build-only tools.

Example with a Python backend that uses a build step:

dockerfile
# Stage 1: build dependencies
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt
# Stage 2: runtime image
FROM python:3.12-slim
WORKDIR /app
# copy installed packages from builder image
COPY --from=builder /install /usr/local
COPY app ./app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Here:

Another example, static build for a small Go HTTP backend (even though this course uses Python, this demonstrates the idea clearly):

dockerfile
# Stage 1: build binary
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY . .
RUN go build -o server ./cmd/server
# Stage 2: minimal runtime
FROM alpine:3.20
WORKDIR /app
COPY --from=builder /src/server .
EXPOSE 8080
CMD ["./server"]

Multi-stage builds help you keep production images:

Inspecting, Listing, and Cleaning Images

List images:

bash
docker images

Example output:

REPOSITORYTAGIMAGE IDCREATEDSIZE
my-backend1.0.0123abc456d2 minutes ago230MB
my-backendlatest123abc456d2 minutes ago230MB
python3.12-slim789ghi012j3 weeks ago150MB

Inspect an image:

bash
docker inspect my-backend:1.0.0

This shows JSON with details:

Remove an image:

bash
docker rmi my-backend:1.0.0

If a container still uses this image, you must stop and remove the container first.

Remove dangling images (untagged images, often from intermediate builds):

bash
docker image prune

Clean more aggressively:

bash
docker system prune

This removes unused containers, networks, dangling images, and build cache. Be careful on shared machines.


Common Pitfalls When Building Images

Including Secrets in the Image

Bad example:

dockerfile
ENV DB_PASSWORD=supersecretpassword

or:

dockerfile
COPY .env .

Anyone with the image can read these values.

Use runtime environment variables instead, for example:

bash
docker run -e DB_PASSWORD=supersecretpassword my-backend:1.0

and read them inside your app using your language tools (for example os.environ in Python).

Also ensure .env goes into .dockerignore.

Copying Too Much

Bad example:

dockerfile
COPY . .

This copies everything from the context, including:

Better:

dockerfile
COPY app ./app
COPY requirements.txt .

Be explicit about what you copy.

Using a Heavy Base Image

Compare:

dockerfile
FROM python:3.12

versus:

dockerfile
FROM python:3.12-slim

The -slim variant is much smaller and usually sufficient for backend applications.

If you need system packages, such as libpq-dev for PostgreSQL, you can still install them using apt-get.

Not Pinning Dependencies

requirements.txt:

text
fastapi
uvicorn[standard]

This may install different versions over time, leading to inconsistent builds.

Better:

text
fastapi==0.115.0
uvicorn[standard]==0.30.0

You can lock versions with tools like pip freeze or dependency managers.


Example: Development vs Production Build

Often you want slightly different images or configurations for development and production.

Single Dockerfile with ARG

dockerfile
FROM python:3.12-slim
ARG APP_ENV=production
ENV APP_ENV=${APP_ENV}
WORKDIR /app
COPY requirements.txt .
RUN if [ "$APP_ENV" = "development" ]; then \
        pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt; \
    else \
        pip install --no-cache-dir -r requirements.txt; \
    fi
COPY app ./app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Build a dev image:

bash
docker build -t my-backend:dev --build-arg APP_ENV=development .

Build a prod image:

bash
docker build -t my-backend:prod --build-arg APP_ENV=production .

Both images will behave differently depending on what is installed.

Separate Dockerfiles

Another approach is to have two files, for example:

Then:

bash
docker build -f Dockerfile.dev -t my-backend:dev .
docker build -f Dockerfile.prod -t my-backend:prod .

Choose the approach that keeps your configuration clear for your team.


Verifying and Testing a New Image

After building an image, always verify that it behaves as expected.

  1. Run it locally
bash
   docker run --rm -p 8000:8000 my-backend:1.0.0

--rm removes the container after it stops.

  1. Check logs

You should see your application startup logs.

  1. Test endpoints
bash
   curl http://localhost:8000/ping
  1. Check environment expectations

If your app expects DATABASE_URL, you can run:

bash
   docker run --rm -p 8000:8000 \
     -e DATABASE_URL=postgresql://user:pass@db:5432/appdb \
     my-backend:1.0.0
  1. Confirm image size
bash
   docker images my-backend

If the image is extremely large, inspect the Dockerfile to remove unnecessary files or use a smaller base image.


Summary Checklist for Building Backend Images

Use this checklist whenever you build a backend image:

With these practices, your backend Docker images will be reproducible, efficient, and ready for use in later chapters where you will integrate them with Docker Compose and deploy them to production.

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!