21.6. Building Images
Table of Contents
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:
- Run the same image on your laptop, a CI server, and in production.
- Share the image with your team or deploy it via Kubernetes or another orchestrator.
- Rebuild new versions of the image when you change your code.
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:
docker build -t my-backend-app:1.0 .Let us break this down:
| Part | Meaning |
|---|---|
docker build | Build a Docker image from a Dockerfile. |
-t my-backend-app:1.0 | Tag 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:
docker build -f Dockerfile.prod -t my-backend-app:prod .-f Dockerfile.prodtells Docker which file to use.- The final
.is still the context.
If your Dockerfile is in docker/Dockerfile, you can run:
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:
my-backend/
app/
main.py
requirements.txt
docker/
Dockerfile
.git/
.env
README.mdTypical build command:
cd my-backend
docker build -f docker/Dockerfile -t my-backend-app:1.0 .Here:
- Context is
my-backend. - Docker can copy
app/main.py,app/requirements.txtinto the image. - It will also send
.git,.envunless you exclude them.
Use a .dockerignore file in the context directory:
# .dockerignore
.git
.env
__pycache__
*.pyc
node_modules
testsThis keeps the image build:
- Faster, because less data is sent.
- Safer, because secrets (like
.envor SSH keys) are not sent and cannot be copied into the image.
A Simple Backend Image Example
Assume a basic Python backend using FastAPI:
my-backend/
app/
main.py
requirements.txt
Dockerfile
app/main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/ping")
def ping():
return {"message": "pong"}
requirements.txt:
fastapi
uvicorn[standard]
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:
docker build -t fastapi-backend:1.0 .
docker run -p 8000:8000 fastapi-backend:1.0Test:
curl http://localhost:8000/ping
# {"message":"pong"}Tagging Images Correctly
Image names usually look like:
repository/name:tagExamples:
| Image name | Meaning |
|---|---|
my-backend:latest | Local image named my-backend, tag latest. |
my-backend:1.0.0 | Versioned image, useful for releases. |
username/my-backend:dev | Often used for Docker Hub. |
registry.example.com/api:prod | Image in a private registry. |
You can tag several versions for the same image:
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:
ARG APP_ENV=development
RUN echo "Building for environment: ${APP_ENV}"Build with a custom value:
docker build -t my-backend:staging --build-arg APP_ENV=staging .Key points:
ARGvalues are only available during build.- They are not available at runtime inside the container.
- Do not use
ARGfor secrets. They can appear in the image history.
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:
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:
docker build -t my-backend:1.0 .All layers are built.
If you now only change app/main.py and build again:
FROM,WORKDIR,COPY requirements.txt,RUN pip installare cached.- Only
COPY appand layers after it are rebuilt. - The build is much faster.
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:
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:
- Use one image for building (with tools and compilers).
- Use a smaller image for running the final application.
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:
# 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:
builderstage installs dependencies.- Final image copies only the installed packages and source code.
- The final image does not contain build caches or extra tools from the builder stage.
Another example, static build for a small Go HTTP backend (even though this course uses Python, this demonstrates the idea clearly):
# 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:
- Smaller.
- Safer, fewer tools inside that an attacker could use.
- Faster to pull and deploy.
Inspecting, Listing, and Cleaning Images
List images:
docker imagesExample output:
| REPOSITORY | TAG | IMAGE ID | CREATED | SIZE |
|---|---|---|---|---|
| my-backend | 1.0.0 | 123abc456d | 2 minutes ago | 230MB |
| my-backend | latest | 123abc456d | 2 minutes ago | 230MB |
| python | 3.12-slim | 789ghi012j | 3 weeks ago | 150MB |
Inspect an image:
docker inspect my-backend:1.0.0This shows JSON with details:
- Environment variables.
- Entrypoint and command.
- Exposed ports.
- Layers.
Remove an image:
docker rmi my-backend:1.0.0If a container still uses this image, you must stop and remove the container first.
Remove dangling images (untagged images, often from intermediate builds):
docker image pruneClean more aggressively:
docker system pruneThis 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:
ENV DB_PASSWORD=supersecretpasswordor:
COPY .env .Anyone with the image can read these values.
Use runtime environment variables instead, for example:
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:
COPY . .This copies everything from the context, including:
- Tests.
- Local environment files.
.githistory.- Temporary files.
Better:
COPY app ./app
COPY requirements.txt .Be explicit about what you copy.
Using a Heavy Base Image
Compare:
FROM python:3.12versus:
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:
fastapi
uvicorn[standard]This may install different versions over time, leading to inconsistent builds.
Better:
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
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:
docker build -t my-backend:dev --build-arg APP_ENV=development .Build a prod image:
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:
Dockerfile.devDockerfile.prod
Then:
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.
- Run it locally
docker run --rm -p 8000:8000 my-backend:1.0.0
--rm removes the container after it stops.
- Check logs
You should see your application startup logs.
- Test endpoints
curl http://localhost:8000/ping- Check environment expectations
If your app expects DATABASE_URL, you can run:
docker run --rm -p 8000:8000 \
-e DATABASE_URL=postgresql://user:pass@db:5432/appdb \
my-backend:1.0.0- Confirm image size
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:
- [ ] Use a suitable base image, for example
python:3.12-slim. - [ ] Keep the build context small, use
.dockerignore. - [ ] Avoid
COPY . .if possible, copy only what you need. - [ ] Place rarely changing steps before frequently changing steps to benefit from caching.
- [ ] Use multi-stage builds for smaller, cleaner production images.
- [ ] Do not store secrets in the image or
Dockerfile. - [ ] Tag images meaningfully, for example
my-backend:1.0.0. - [ ] Pin dependency versions for reproducible builds.
- [ ] Test the image by running containers and calling endpoints.
- [ ] Clean unused images and cache when needed.
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
KAHIBARO