Key idea
Same checklist as the Node recipe. For Python, the build stage installs the dependencies into a virtual environment, one folder the runtime stage copies across whole. The app runs under gunicorn, a production server, as a user you create.
Get the app
git clone https://github.com/computesphere-samples/learn.git
cd learn/labs/containerize-python
A small Flask app with / and /healthz. Flask and gunicorn are pinned in requirements.txt. Already cloned it for the Node recipe? Run cd ../containerize-python instead.
.dockerignore
.venv
__pycache__
*.pyc
.env*
.git
Dockerfile
.dockerignore
A local .venv was built for your machine, and .env files hold secrets (lesson 3.4.4).
Dockerfile
# Stage 1: install the dependencies into a virtual environment.
FROM python:3.13-slim AS build
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Stage 2: a clean Python with the finished environment copied in.
FROM python:3.13-slim
RUN useradd --create-home --uid 10001 app
COPY --from=build /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH" PYTHONUNBUFFERED=1 PORT=3000
WORKDIR /app
COPY app.py ./
USER app
EXPOSE 3000
CMD ["sh", "-c", "exec gunicorn --bind 0.0.0.0:${PORT} app:app"]
What each choice does:
- The virtual environment at
/opt/venvholds every installed package, so oneCOPY --from=buildmoves them all. Putting it first onPATHmeansgunicornis found. --no-cache-dirstops pip keeping its download cache in the layer.- Both stages use the same base, so packages built in stage 1 run in stage 2.
useraddcreates userapp, andUSER appruns as it (lesson 3.4.3). The slim image has no such user ready-made.PYTHONUNBUFFERED=1makes log lines appear straight away indocker logs.- The shell form of
CMDlets${PORT}be filled in when the container starts, andexechands over to gunicorn so it receives stop signals.
Build, run and check
docker build -t containerize-python:1.0.0 .
docker run -d --rm --name containerize-python -p 3000:3000 containerize-python:1.0.0
curl -i localhost:3000/healthz
HTTP/1.1 200 OK
Server: gunicorn
Content-Type: application/json
...
{"status":"ok"}
Then confirm the user, read the logs, and stop it:
docker image inspect containerize-python:1.0.0 --format '{{.Config.User}}'
docker logs containerize-python
docker stop containerize-python
The first prints app, and the logs include Listening at: http://0.0.0.0:3000. The image comes out at about 230 MB, most of it Python itself.
Check yourself