Skip to content

beginner ~45 min updated 2026-06-01

Docker Build and Push

Write a multi-stage Dockerfile for a small Python API, build a multi-architecture image with docker buildx, tag it correctly, and push it to Docker Hub with build cache enabled.

Objective

Build a multi-stage, multi-architecture Docker image with buildx and publish it to Docker Hub with semantic tags. You will learn why multi-stage builds shrink images, how layer caching works, and how one build can serve both amd64 and arm64 machines.

Prerequisites

  • Docker Engine 24 or newer with the buildx plugin (docker buildx version works)
  • A free Docker Hub account
  • Basic terminal familiarity
  • Python 3 installed locally (only needed to understand the sample app)

Architecture

A small Flask API is packaged with a two-stage Dockerfile: the builder stage installs dependencies into a virtualenv, and the slim runtime stage copies only that virtualenv plus the app code. Buildx uses a container driver to cross-compile for amd64 and arm64 and pushes a single multi-arch manifest list to Docker Hub.

 Dockerfile (multi-stage)
+---------------------------+
| stage 1: builder          |
|  python:3.12 + pip install|
+------------+--------------+
             | copy /opt/venv
+------------v--------------+        docker buildx build
| stage 2: runtime          | ---->  --platform amd64,arm64
|  python:3.12-slim + app   |              |
+---------------------------+              v
                              docker.io/USER/flask-lab:1.0.0
                              (manifest list: amd64 + arm64)

Steps

1. Create the sample application

mkdir docker-lab && cd docker-lab
# app.py
from flask import Flask, jsonify
import platform

app = Flask(__name__)

@app.get("/")
def index():
    return jsonify(status="ok", arch=platform.machine())

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
# requirements.txt
flask==3.0.3

2. Write the multi-stage Dockerfile

# Dockerfile
# ---- build stage ----
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN python -m venv /opt/venv && \
    /opt/venv/bin/pip install --no-cache-dir -r requirements.txt

# ---- runtime stage ----
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
COPY app.py .
ENV PATH="/opt/venv/bin:$PATH"
EXPOSE 5000
USER nobody
CMD ["python", "app.py"]

Add a .dockerignore:

# .dockerignore
__pycache__/
*.pyc
.git

3. Build and test locally

docker build -t flask-lab:dev .
docker run -d --name flask-lab -p 5000:5000 flask-lab:dev
curl -s http://localhost:5000
docker rm -f flask-lab
docker images flask-lab

4. Log in and create a buildx builder

docker login
docker buildx create --name lab-builder --driver docker-container --use
docker buildx inspect --bootstrap

5. Build multi-arch and push

Replace YOUR_USER with your Docker Hub username:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag YOUR_USER/flask-lab:1.0.0 \
  --tag YOUR_USER/flask-lab:latest \
  --cache-to type=inline \
  --push .

6. Verify the manifest list

docker buildx imagetools inspect YOUR_USER/flask-lab:1.0.0
docker pull YOUR_USER/flask-lab:1.0.0
docker run --rm -d -p 5000:5000 --name verify YOUR_USER/flask-lab:1.0.0
curl -s http://localhost:5000
docker rm -f verify

Expected output

$ curl -s http://localhost:5000
{"arch":"x86_64","status":"ok"}

$ docker images flask-lab
REPOSITORY   TAG   IMAGE ID       SIZE
flask-lab    dev   3f9a1b2c4d5e   142MB

$ docker buildx imagetools inspect YOUR_USER/flask-lab:1.0.0
Name:      docker.io/YOUR_USER/flask-lab:1.0.0
MediaType: application/vnd.oci.image.index.v1+json

Manifests:
  Platform:  linux/amd64
  Platform:  linux/arm64

Troubleshooting

  • denied: requested access to the resource is denied on push: you tagged the image without your Docker Hub namespace, or docker login expired. Tag as YOUR_USER/flask-lab and log in again.
  • multiple platforms feature is currently not supported for docker driver: you are using the default docker driver. Create a container driver builder: docker buildx create --driver docker-container --use.
  • arm64 build is extremely slow: QEMU emulation is doing the work. That is normal for cross-builds; install emulators explicitly with docker run --privileged --rm tonistiigi/binfmt --install all if the arm64 stage errors out.
  • COPY failed: file not found: the file is excluded by .dockerignore or you are building from the wrong context directory. Run the build from inside docker-lab/ and check the ignore file.
  • Port 5000 already in use: another process (on macOS often AirPlay Receiver) owns it. Map a different host port: -p 5050:5000.

Cleanup

docker rm -f flask-lab verify 2>/dev/null || true
docker rmi flask-lab:dev YOUR_USER/flask-lab:1.0.0 YOUR_USER/flask-lab:latest 2>/dev/null || true
docker buildx rm lab-builder
docker logout
cd .. && rm -rf docker-lab
# Optionally delete the repository in the Docker Hub web UI.