Python language-specific guide
This guide explains how to containerize Python applications using Docker.
Acknowledgment
This guide is a community contribution. Docker would like to thank Esteban Maya and Igor Aleksandrov for their contribution to this guide.
The Python language-specific guide teaches you how to containerize a Python application using Docker. In this guide, you’ll learn how to:
- Containerize and run a Python application
- Set up a local environment to develop a Python application using containers
- Lint, format, typing and best practices
Start by containerizing an existing Python application.
Containerize a Python application
Prerequisites
- You have installed the latest version of Docker Desktop.
Overview
Containerizing your application means packaging it together with its dependencies, configuration, and runtime into a single portable unit called a container image. Running that image creates a container, an isolated process that behaves the same on any machine, whether it's your laptop, a CI runner, or a production server.
In this section, you'll containerize a simple
FastAPI web application. You'll write a
Dockerfile that describes how to build the image, add a compose.yaml file
that defines how Docker runs your container, and then build and start the
application with one command.
You'll use Docker Hardened Images as the base. These are minimal, secure Python images maintained by Docker.
Create the application
The sample application is a minimal FastAPI service with a single endpoint
that returns a JSON greeting. Create the following files in a new
python-docker-example directory. To create all the files at once, switch to
the Scaffold script tab in the file browser and copy the shell command.
# A minimal FastAPI application.
# The root endpoint (GET /) returns a JSON "Hello World" response.
# See https://fastapi.tiangolo.com/ for the framework reference.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
uvicorn==0.34.3# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txtmkdir python-docker-example && cd python-docker-example
cat > app.py <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# A minimal FastAPI application.
# The root endpoint (GET /) returns a JSON "Hello World" response.
# See https://fastapi.tiangolo.com/ for the framework reference.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > requirements.txt <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
uvicorn==0.34.3
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > .gitignore <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txt
__DOCKER_DOCS_SCAFFOLD_EOF__# Write files as UTF-8 without BOM. Works on Windows PowerShell 5.1 and PowerShell 7+.
function WriteFile([string]$Path, [string]$Content) {
$full = Join-Path -Path (Get-Location).ProviderPath -ChildPath $Path
[System.IO.File]::WriteAllText($full, $Content, [System.Text.UTF8Encoding]::new($false))
}
New-Item -ItemType Directory -Force -Path python-docker-example | Out-Null
Set-Location python-docker-example
WriteFile 'app.py' @'
# A minimal FastAPI application.
# The root endpoint (GET /) returns a JSON "Hello World" response.
# See https://fastapi.tiangolo.com/ for the framework reference.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
'@
WriteFile 'requirements.txt' @'
# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
uvicorn==0.34.3
'@
WriteFile '.gitignore' @'
# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txt
'@If you already have Python installed and want to verify the app works before containerizing it, you can run it locally:
$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install -r requirements.txt
$ uvicorn app:app --reload
NoteOn Windows, activate the virtual environment with
.venv\Scripts\activateinstead ofsource .venv/bin/activate.
If you don't have Python installed, skip ahead to the next section. The remaining steps run the application in a container, with no local Python required.
Create the Docker assets
Sign in to the DHI registry so Docker can pull the Python base images during the build. The available Python images are listed in the catalog.
$ docker login dhi.io
Add the following three files to your python-docker-example directory. The
Dockerfile describes how to build the image, compose.yaml defines how
Docker runs the container, and .dockerignore keeps unwanted files out of the
build context.
TipGordon, Docker's AI assistant, can generate Docker assets for your project. Ask Gordon to create a Dockerfile, Compose file, and
.dockerignoretailored to your application.
# A minimal FastAPI application.
# The root endpoint (GET /) returns a JSON "Hello World" response.
# See https://fastapi.tiangolo.com/ for the framework reference.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
uvicorn==0.34.3# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them into
# this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Use the minimal runtime image. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker Compose reference guide at
# https://docs.docker.com/go/compose-spec-reference/
# Here the instructions define your application as a service called "server".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
services:
server:
build:
context: .
ports:
- 8000:8000# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txtmkdir python-docker-example && cd python-docker-example
cat > app.py <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# A minimal FastAPI application.
# The root endpoint (GET /) returns a JSON "Hello World" response.
# See https://fastapi.tiangolo.com/ for the framework reference.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > requirements.txt <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
uvicorn==0.34.3
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > Dockerfile <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them into
# this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Use the minimal runtime image. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > compose.yaml <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker Compose reference guide at
# https://docs.docker.com/go/compose-spec-reference/
# Here the instructions define your application as a service called "server".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
services:
server:
build:
context: .
ports:
- 8000:8000
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > .dockerignore <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > .gitignore <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txt
__DOCKER_DOCS_SCAFFOLD_EOF__# Write files as UTF-8 without BOM. Works on Windows PowerShell 5.1 and PowerShell 7+.
function WriteFile([string]$Path, [string]$Content) {
$full = Join-Path -Path (Get-Location).ProviderPath -ChildPath $Path
[System.IO.File]::WriteAllText($full, $Content, [System.Text.UTF8Encoding]::new($false))
}
New-Item -ItemType Directory -Force -Path python-docker-example | Out-Null
Set-Location python-docker-example
WriteFile 'app.py' @'
# A minimal FastAPI application.
# The root endpoint (GET /) returns a JSON "Hello World" response.
# See https://fastapi.tiangolo.com/ for the framework reference.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
'@
WriteFile 'requirements.txt' @'
# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
uvicorn==0.34.3
'@
WriteFile 'Dockerfile' @'
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them into
# this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Use the minimal runtime image. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
'@
WriteFile 'compose.yaml' @'
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker Compose reference guide at
# https://docs.docker.com/go/compose-spec-reference/
# Here the instructions define your application as a service called "server".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
services:
server:
build:
context: .
ports:
- 8000:8000
'@
WriteFile '.dockerignore' @'
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
'@
WriteFile '.gitignore' @'
# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txt
'@To learn more about each file, see the following:
Run the application
Inside the python-docker-example directory, run the following command in a
terminal.
$ docker compose up --build
Open a browser and view the application at http://localhost:8000. You should see a simple FastAPI application.
In the terminal, press ctrl+c to stop the application.
Run the application in the background
You can run the application detached from the terminal by adding the -d
option. Inside the python-docker-example directory, run the following command
in a terminal.
$ docker compose up --build -d
Open a browser and view the application at http://localhost:8000.
To see the OpenAPI docs you can go to http://localhost:8000/docs.
You should see a simple FastAPI application.
In the terminal, run the following command to stop the application.
$ docker compose down
For more information about Compose commands, see the Compose CLI reference.
Use containers for Python development
Prerequisites
Complete Containerize a Python application.
Overview
Once your application runs in a container, the next step is making the container loop part of your everyday development workflow. Code changes should show up quickly, and services your app depends on, like databases, should run right alongside it.
In this section, you'll extend the project from the previous topic by adding a
PostgreSQL database service to your compose.yaml, persisting the database
data in a named volume, and enabling Compose Watch so that changes you save in
your editor are picked up by the running container without a manual rebuild.
Update the application
You'll update your application to connect to a PostgreSQL database. Continue
working in your python-docker-example directory.
Replace app.py and requirements.txt, and add a new config.py file with the
following contents.
NoteThe application won't run yet after this step. It tries to connect to a PostgreSQL database that doesn't exist. The next two sections add the database service and the Docker configuration needed to run everything together.
# FastAPI application backed by a PostgreSQL database via SQLModel.
# The FastAPI lifespan handler creates database tables at startup.
# Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list).
# See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlmodel import Field, Session, SQLModel, create_engine, select
from config import settings
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
create_db_and_tables()
yield
app = FastAPI(lifespan=lifespan)
@app.get("/")
def hello() -> str:
return "Hello, Docker!"
@app.post("/heroes/")
def create_hero(hero: Hero) -> Hero:
with Session(engine) as session:
session.add(hero)
session.commit()
session.refresh(hero)
return hero
@app.get("/heroes/")
def read_heroes() -> Sequence[Hero]:
with Session(engine) as session:
heroes = session.exec(select(Hero)).all()
return heroes# Pydantic settings that read PostgreSQL connection details from the
# environment. Supports a password file (Docker secrets) via
# POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD.
# See https://docs.pydantic.dev/latest/concepts/pydantic_settings/
import os
from typing import Any
from pydantic import (
PostgresDsn,
computed_field,
field_validator,
model_validator,
)
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str | None = None
POSTGRES_PASSWORD_FILE: str | None = None
POSTGRES_DB: str
@model_validator(mode="before")
@classmethod
def check_postgres_password(cls, data: Any) -> Any:
"""Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set."""
if isinstance(data, dict):
password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore
password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore
if password_file is None and password is None:
raise ValueError(
"At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set."
)
return data # type: ignore
@field_validator("POSTGRES_PASSWORD_FILE", mode="before")
@classmethod
def read_password_from_file(cls, v: str | None) -> str | None:
if v is not None:
file_path = v
if os.path.exists(file_path):
with open(file_path) as file:
return file.read().strip()
raise ValueError(f"Password file {file_path} does not exist.")
return v
@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
url = MultiHostUrl.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD
if self.POSTGRES_PASSWORD
else self.POSTGRES_PASSWORD_FILE,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
return PostgresDsn(url)
settings = Settings() # type: ignore# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
sqlmodel==0.0.24
psycopg[binary]==3.2.9
pydantic-settings==2.9.1
uvicorn==0.34.3# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them into
# this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Use the minimal runtime image. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker Compose reference guide at
# https://docs.docker.com/go/compose-spec-reference/
# Here the instructions define your application as a service called "server".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
services:
server:
build:
context: .
ports:
- 8000:8000# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txtmkdir python-docker-example && cd python-docker-example
cat > app.py <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# FastAPI application backed by a PostgreSQL database via SQLModel.
# The FastAPI lifespan handler creates database tables at startup.
# Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list).
# See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlmodel import Field, Session, SQLModel, create_engine, select
from config import settings
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
create_db_and_tables()
yield
app = FastAPI(lifespan=lifespan)
@app.get("/")
def hello() -> str:
return "Hello, Docker!"
@app.post("/heroes/")
def create_hero(hero: Hero) -> Hero:
with Session(engine) as session:
session.add(hero)
session.commit()
session.refresh(hero)
return hero
@app.get("/heroes/")
def read_heroes() -> Sequence[Hero]:
with Session(engine) as session:
heroes = session.exec(select(Hero)).all()
return heroes
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > config.py <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Pydantic settings that read PostgreSQL connection details from the
# environment. Supports a password file (Docker secrets) via
# POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD.
# See https://docs.pydantic.dev/latest/concepts/pydantic_settings/
import os
from typing import Any
from pydantic import (
PostgresDsn,
computed_field,
field_validator,
model_validator,
)
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str | None = None
POSTGRES_PASSWORD_FILE: str | None = None
POSTGRES_DB: str
@model_validator(mode="before")
@classmethod
def check_postgres_password(cls, data: Any) -> Any:
"""Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set."""
if isinstance(data, dict):
password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore
password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore
if password_file is None and password is None:
raise ValueError(
"At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set."
)
return data # type: ignore
@field_validator("POSTGRES_PASSWORD_FILE", mode="before")
@classmethod
def read_password_from_file(cls, v: str | None) -> str | None:
if v is not None:
file_path = v
if os.path.exists(file_path):
with open(file_path) as file:
return file.read().strip()
raise ValueError(f"Password file {file_path} does not exist.")
return v
@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
url = MultiHostUrl.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD
if self.POSTGRES_PASSWORD
else self.POSTGRES_PASSWORD_FILE,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
return PostgresDsn(url)
settings = Settings() # type: ignore
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > requirements.txt <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
sqlmodel==0.0.24
psycopg[binary]==3.2.9
pydantic-settings==2.9.1
uvicorn==0.34.3
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > Dockerfile <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them into
# this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Use the minimal runtime image. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > compose.yaml <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker Compose reference guide at
# https://docs.docker.com/go/compose-spec-reference/
# Here the instructions define your application as a service called "server".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
services:
server:
build:
context: .
ports:
- 8000:8000
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > .dockerignore <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > .gitignore <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txt
__DOCKER_DOCS_SCAFFOLD_EOF__# Write files as UTF-8 without BOM. Works on Windows PowerShell 5.1 and PowerShell 7+.
function WriteFile([string]$Path, [string]$Content) {
$full = Join-Path -Path (Get-Location).ProviderPath -ChildPath $Path
[System.IO.File]::WriteAllText($full, $Content, [System.Text.UTF8Encoding]::new($false))
}
New-Item -ItemType Directory -Force -Path python-docker-example | Out-Null
Set-Location python-docker-example
WriteFile 'app.py' @'
# FastAPI application backed by a PostgreSQL database via SQLModel.
# The FastAPI lifespan handler creates database tables at startup.
# Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list).
# See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlmodel import Field, Session, SQLModel, create_engine, select
from config import settings
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
create_db_and_tables()
yield
app = FastAPI(lifespan=lifespan)
@app.get("/")
def hello() -> str:
return "Hello, Docker!"
@app.post("/heroes/")
def create_hero(hero: Hero) -> Hero:
with Session(engine) as session:
session.add(hero)
session.commit()
session.refresh(hero)
return hero
@app.get("/heroes/")
def read_heroes() -> Sequence[Hero]:
with Session(engine) as session:
heroes = session.exec(select(Hero)).all()
return heroes
'@
WriteFile 'config.py' @'
# Pydantic settings that read PostgreSQL connection details from the
# environment. Supports a password file (Docker secrets) via
# POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD.
# See https://docs.pydantic.dev/latest/concepts/pydantic_settings/
import os
from typing import Any
from pydantic import (
PostgresDsn,
computed_field,
field_validator,
model_validator,
)
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str | None = None
POSTGRES_PASSWORD_FILE: str | None = None
POSTGRES_DB: str
@model_validator(mode="before")
@classmethod
def check_postgres_password(cls, data: Any) -> Any:
"""Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set."""
if isinstance(data, dict):
password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore
password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore
if password_file is None and password is None:
raise ValueError(
"At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set."
)
return data # type: ignore
@field_validator("POSTGRES_PASSWORD_FILE", mode="before")
@classmethod
def read_password_from_file(cls, v: str | None) -> str | None:
if v is not None:
file_path = v
if os.path.exists(file_path):
with open(file_path) as file:
return file.read().strip()
raise ValueError(f"Password file {file_path} does not exist.")
return v
@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
url = MultiHostUrl.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD
if self.POSTGRES_PASSWORD
else self.POSTGRES_PASSWORD_FILE,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
return PostgresDsn(url)
settings = Settings() # type: ignore
'@
WriteFile 'requirements.txt' @'
# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
sqlmodel==0.0.24
psycopg[binary]==3.2.9
pydantic-settings==2.9.1
uvicorn==0.34.3
'@
WriteFile 'Dockerfile' @'
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them into
# this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Use the minimal runtime image. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
'@
WriteFile 'compose.yaml' @'
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker Compose reference guide at
# https://docs.docker.com/go/compose-spec-reference/
# Here the instructions define your application as a service called "server".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
services:
server:
build:
context: .
ports:
- 8000:8000
'@
WriteFile '.dockerignore' @'
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
'@
WriteFile '.gitignore' @'
# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txt
'@Update Docker assets
Replace Dockerfile and compose.yaml with the following.
# FastAPI application backed by a PostgreSQL database via SQLModel.
# The FastAPI lifespan handler creates database tables at startup.
# Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list).
# See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlmodel import Field, Session, SQLModel, create_engine, select
from config import settings
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
create_db_and_tables()
yield
app = FastAPI(lifespan=lifespan)
@app.get("/")
def hello() -> str:
return "Hello, Docker!"
@app.post("/heroes/")
def create_hero(hero: Hero) -> Hero:
with Session(engine) as session:
session.add(hero)
session.commit()
session.refresh(hero)
return hero
@app.get("/heroes/")
def read_heroes() -> Sequence[Hero]:
with Session(engine) as session:
heroes = session.exec(select(Hero)).all()
return heroes# Pydantic settings that read PostgreSQL connection details from the
# environment. Supports a password file (Docker secrets) via
# POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD.
# See https://docs.pydantic.dev/latest/concepts/pydantic_settings/
import os
from typing import Any
from pydantic import (
PostgresDsn,
computed_field,
field_validator,
model_validator,
)
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str | None = None
POSTGRES_PASSWORD_FILE: str | None = None
POSTGRES_DB: str
@model_validator(mode="before")
@classmethod
def check_postgres_password(cls, data: Any) -> Any:
"""Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set."""
if isinstance(data, dict):
password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore
password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore
if password_file is None and password is None:
raise ValueError(
"At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set."
)
return data # type: ignore
@field_validator("POSTGRES_PASSWORD_FILE", mode="before")
@classmethod
def read_password_from_file(cls, v: str | None) -> str | None:
if v is not None:
file_path = v
if os.path.exists(file_path):
with open(file_path) as file:
return file.read().strip()
raise ValueError(f"Password file {file_path} does not exist.")
return v
@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
url = MultiHostUrl.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD
if self.POSTGRES_PASSWORD
else self.POSTGRES_PASSWORD_FILE,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
return PostgresDsn(url)
settings = Settings() # type: ignore# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
sqlmodel==0.0.24
psycopg[binary]==3.2.9
pydantic-settings==2.9.1
uvicorn==0.34.3# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
# The builder stage is also used directly in development (see compose.yaml).
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them
# into this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
# Use the minimal runtime image for production. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
COPY --from=builder /app .
EXPOSE 8000
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]services:
# Application service. The `target: builder` line builds the development
# image (includes a shell and tools); the production stage of the
# Dockerfile is unused in development.
server:
build:
context: .
target: builder
ports:
- 8000:8000# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txtmkdir python-docker-example && cd python-docker-example
cat > app.py <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# FastAPI application backed by a PostgreSQL database via SQLModel.
# The FastAPI lifespan handler creates database tables at startup.
# Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list).
# See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlmodel import Field, Session, SQLModel, create_engine, select
from config import settings
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
create_db_and_tables()
yield
app = FastAPI(lifespan=lifespan)
@app.get("/")
def hello() -> str:
return "Hello, Docker!"
@app.post("/heroes/")
def create_hero(hero: Hero) -> Hero:
with Session(engine) as session:
session.add(hero)
session.commit()
session.refresh(hero)
return hero
@app.get("/heroes/")
def read_heroes() -> Sequence[Hero]:
with Session(engine) as session:
heroes = session.exec(select(Hero)).all()
return heroes
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > config.py <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Pydantic settings that read PostgreSQL connection details from the
# environment. Supports a password file (Docker secrets) via
# POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD.
# See https://docs.pydantic.dev/latest/concepts/pydantic_settings/
import os
from typing import Any
from pydantic import (
PostgresDsn,
computed_field,
field_validator,
model_validator,
)
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str | None = None
POSTGRES_PASSWORD_FILE: str | None = None
POSTGRES_DB: str
@model_validator(mode="before")
@classmethod
def check_postgres_password(cls, data: Any) -> Any:
"""Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set."""
if isinstance(data, dict):
password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore
password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore
if password_file is None and password is None:
raise ValueError(
"At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set."
)
return data # type: ignore
@field_validator("POSTGRES_PASSWORD_FILE", mode="before")
@classmethod
def read_password_from_file(cls, v: str | None) -> str | None:
if v is not None:
file_path = v
if os.path.exists(file_path):
with open(file_path) as file:
return file.read().strip()
raise ValueError(f"Password file {file_path} does not exist.")
return v
@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
url = MultiHostUrl.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD
if self.POSTGRES_PASSWORD
else self.POSTGRES_PASSWORD_FILE,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
return PostgresDsn(url)
settings = Settings() # type: ignore
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > requirements.txt <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
sqlmodel==0.0.24
psycopg[binary]==3.2.9
pydantic-settings==2.9.1
uvicorn==0.34.3
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > Dockerfile <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
# The builder stage is also used directly in development (see compose.yaml).
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them
# into this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
# Use the minimal runtime image for production. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
COPY --from=builder /app .
EXPOSE 8000
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > compose.yaml <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
services:
# Application service. The `target: builder` line builds the development
# image (includes a shell and tools); the production stage of the
# Dockerfile is unused in development.
server:
build:
context: .
target: builder
ports:
- 8000:8000
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > .dockerignore <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > .gitignore <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txt
__DOCKER_DOCS_SCAFFOLD_EOF__# Write files as UTF-8 without BOM. Works on Windows PowerShell 5.1 and PowerShell 7+.
function WriteFile([string]$Path, [string]$Content) {
$full = Join-Path -Path (Get-Location).ProviderPath -ChildPath $Path
[System.IO.File]::WriteAllText($full, $Content, [System.Text.UTF8Encoding]::new($false))
}
New-Item -ItemType Directory -Force -Path python-docker-example | Out-Null
Set-Location python-docker-example
WriteFile 'app.py' @'
# FastAPI application backed by a PostgreSQL database via SQLModel.
# The FastAPI lifespan handler creates database tables at startup.
# Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list).
# See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlmodel import Field, Session, SQLModel, create_engine, select
from config import settings
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
create_db_and_tables()
yield
app = FastAPI(lifespan=lifespan)
@app.get("/")
def hello() -> str:
return "Hello, Docker!"
@app.post("/heroes/")
def create_hero(hero: Hero) -> Hero:
with Session(engine) as session:
session.add(hero)
session.commit()
session.refresh(hero)
return hero
@app.get("/heroes/")
def read_heroes() -> Sequence[Hero]:
with Session(engine) as session:
heroes = session.exec(select(Hero)).all()
return heroes
'@
WriteFile 'config.py' @'
# Pydantic settings that read PostgreSQL connection details from the
# environment. Supports a password file (Docker secrets) via
# POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD.
# See https://docs.pydantic.dev/latest/concepts/pydantic_settings/
import os
from typing import Any
from pydantic import (
PostgresDsn,
computed_field,
field_validator,
model_validator,
)
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str | None = None
POSTGRES_PASSWORD_FILE: str | None = None
POSTGRES_DB: str
@model_validator(mode="before")
@classmethod
def check_postgres_password(cls, data: Any) -> Any:
"""Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set."""
if isinstance(data, dict):
password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore
password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore
if password_file is None and password is None:
raise ValueError(
"At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set."
)
return data # type: ignore
@field_validator("POSTGRES_PASSWORD_FILE", mode="before")
@classmethod
def read_password_from_file(cls, v: str | None) -> str | None:
if v is not None:
file_path = v
if os.path.exists(file_path):
with open(file_path) as file:
return file.read().strip()
raise ValueError(f"Password file {file_path} does not exist.")
return v
@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
url = MultiHostUrl.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD
if self.POSTGRES_PASSWORD
else self.POSTGRES_PASSWORD_FILE,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
return PostgresDsn(url)
settings = Settings() # type: ignore
'@
WriteFile 'requirements.txt' @'
# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
sqlmodel==0.0.24
psycopg[binary]==3.2.9
pydantic-settings==2.9.1
uvicorn==0.34.3
'@
WriteFile 'Dockerfile' @'
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
# The builder stage is also used directly in development (see compose.yaml).
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them
# into this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
# Use the minimal runtime image for production. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
COPY --from=builder /app .
EXPOSE 8000
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
'@
WriteFile 'compose.yaml' @'
services:
# Application service. The `target: builder` line builds the development
# image (includes a shell and tools); the production stage of the
# Dockerfile is unused in development.
server:
build:
context: .
target: builder
ports:
- 8000:8000
'@
WriteFile '.dockerignore' @'
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
'@
WriteFile '.gitignore' @'
# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txt
'@About these changes
The Dockerfile builder stage now includes COPY . . and a CMD
instruction, which makes it directly runnable. This lets Compose target the
builder stage during development without rebuilding the production stage. The
production stage at the bottom is unchanged and still produces a minimal,
nonroot runtime image for shipping.
In compose.yaml, the new target: builder line tells Compose to build and
run the builder stage of the Dockerfile during development. Unlike the minimal
production image, the development image includes a shell and additional tools
that make debugging easier. If you need a shell in a running production
container, use
Docker Debug instead.
Add a local database and persist data
You can use containers to set up local services, like a database. In this
section, you'll update the compose.yaml file to define a database service
and a volume to persist data, and add a db/password.txt file that holds the
database password.
# FastAPI application backed by a PostgreSQL database via SQLModel.
# The FastAPI lifespan handler creates database tables at startup.
# Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list).
# See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlmodel import Field, Session, SQLModel, create_engine, select
from config import settings
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
create_db_and_tables()
yield
app = FastAPI(lifespan=lifespan)
@app.get("/")
def hello() -> str:
return "Hello, Docker!"
@app.post("/heroes/")
def create_hero(hero: Hero) -> Hero:
with Session(engine) as session:
session.add(hero)
session.commit()
session.refresh(hero)
return hero
@app.get("/heroes/")
def read_heroes() -> Sequence[Hero]:
with Session(engine) as session:
heroes = session.exec(select(Hero)).all()
return heroes# Pydantic settings that read PostgreSQL connection details from the
# environment. Supports a password file (Docker secrets) via
# POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD.
# See https://docs.pydantic.dev/latest/concepts/pydantic_settings/
import os
from typing import Any
from pydantic import (
PostgresDsn,
computed_field,
field_validator,
model_validator,
)
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str | None = None
POSTGRES_PASSWORD_FILE: str | None = None
POSTGRES_DB: str
@model_validator(mode="before")
@classmethod
def check_postgres_password(cls, data: Any) -> Any:
"""Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set."""
if isinstance(data, dict):
password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore
password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore
if password_file is None and password is None:
raise ValueError(
"At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set."
)
return data # type: ignore
@field_validator("POSTGRES_PASSWORD_FILE", mode="before")
@classmethod
def read_password_from_file(cls, v: str | None) -> str | None:
if v is not None:
file_path = v
if os.path.exists(file_path):
with open(file_path) as file:
return file.read().strip()
raise ValueError(f"Password file {file_path} does not exist.")
return v
@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
url = MultiHostUrl.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD
if self.POSTGRES_PASSWORD
else self.POSTGRES_PASSWORD_FILE,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
return PostgresDsn(url)
settings = Settings() # type: ignore# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
sqlmodel==0.0.24
psycopg[binary]==3.2.9
pydantic-settings==2.9.1
uvicorn==0.34.3# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
# The builder stage is also used directly in development (see compose.yaml).
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them
# into this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
# Use the minimal runtime image for production. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
COPY --from=builder /app .
EXPOSE 8000
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]services:
# Application service. The `target: builder` line builds the development
# image (includes a shell and tools); the production stage of the
# Dockerfile is unused in development.
server:
build:
context: .
target: builder
ports:
- 8000:8000
environment:
- POSTGRES_SERVER=db
- POSTGRES_USER=postgres
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
depends_on:
db:
condition: service_healthy
secrets:
- db-password
# Database service. Reads the password from a Docker secret mounted at
# /run/secrets/db-password. Compose waits for the healthcheck to pass
# before starting the server, via the server's depends_on.
db:
image: dhi.io/postgres:18
restart: always
user: postgres
secrets:
- db-password
volumes:
- db-data:/var/lib/postgresql
environment:
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
expose:
- 5432
healthcheck:
test: ["CMD", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:
secrets:
db-password:
file: db/password.txtmysecretpassword# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txtmkdir -p python-docker-example/db && cd python-docker-example
cat > app.py <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# FastAPI application backed by a PostgreSQL database via SQLModel.
# The FastAPI lifespan handler creates database tables at startup.
# Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list).
# See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlmodel import Field, Session, SQLModel, create_engine, select
from config import settings
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
create_db_and_tables()
yield
app = FastAPI(lifespan=lifespan)
@app.get("/")
def hello() -> str:
return "Hello, Docker!"
@app.post("/heroes/")
def create_hero(hero: Hero) -> Hero:
with Session(engine) as session:
session.add(hero)
session.commit()
session.refresh(hero)
return hero
@app.get("/heroes/")
def read_heroes() -> Sequence[Hero]:
with Session(engine) as session:
heroes = session.exec(select(Hero)).all()
return heroes
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > config.py <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Pydantic settings that read PostgreSQL connection details from the
# environment. Supports a password file (Docker secrets) via
# POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD.
# See https://docs.pydantic.dev/latest/concepts/pydantic_settings/
import os
from typing import Any
from pydantic import (
PostgresDsn,
computed_field,
field_validator,
model_validator,
)
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str | None = None
POSTGRES_PASSWORD_FILE: str | None = None
POSTGRES_DB: str
@model_validator(mode="before")
@classmethod
def check_postgres_password(cls, data: Any) -> Any:
"""Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set."""
if isinstance(data, dict):
password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore
password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore
if password_file is None and password is None:
raise ValueError(
"At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set."
)
return data # type: ignore
@field_validator("POSTGRES_PASSWORD_FILE", mode="before")
@classmethod
def read_password_from_file(cls, v: str | None) -> str | None:
if v is not None:
file_path = v
if os.path.exists(file_path):
with open(file_path) as file:
return file.read().strip()
raise ValueError(f"Password file {file_path} does not exist.")
return v
@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
url = MultiHostUrl.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD
if self.POSTGRES_PASSWORD
else self.POSTGRES_PASSWORD_FILE,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
return PostgresDsn(url)
settings = Settings() # type: ignore
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > requirements.txt <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
sqlmodel==0.0.24
psycopg[binary]==3.2.9
pydantic-settings==2.9.1
uvicorn==0.34.3
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > Dockerfile <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
# The builder stage is also used directly in development (see compose.yaml).
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them
# into this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
# Use the minimal runtime image for production. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
COPY --from=builder /app .
EXPOSE 8000
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > compose.yaml <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
services:
# Application service. The `target: builder` line builds the development
# image (includes a shell and tools); the production stage of the
# Dockerfile is unused in development.
server:
build:
context: .
target: builder
ports:
- 8000:8000
environment:
- POSTGRES_SERVER=db
- POSTGRES_USER=postgres
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
depends_on:
db:
condition: service_healthy
secrets:
- db-password
# Database service. Reads the password from a Docker secret mounted at
# /run/secrets/db-password. Compose waits for the healthcheck to pass
# before starting the server, via the server's depends_on.
db:
image: dhi.io/postgres:18
restart: always
user: postgres
secrets:
- db-password
volumes:
- db-data:/var/lib/postgresql
environment:
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
expose:
- 5432
healthcheck:
test: ["CMD", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:
secrets:
db-password:
file: db/password.txt
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > db/password.txt <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
mysecretpassword
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > .dockerignore <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > .gitignore <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txt
__DOCKER_DOCS_SCAFFOLD_EOF__# Write files as UTF-8 without BOM. Works on Windows PowerShell 5.1 and PowerShell 7+.
function WriteFile([string]$Path, [string]$Content) {
$full = Join-Path -Path (Get-Location).ProviderPath -ChildPath $Path
[System.IO.File]::WriteAllText($full, $Content, [System.Text.UTF8Encoding]::new($false))
}
New-Item -ItemType Directory -Force -Path python-docker-example | Out-Null
New-Item -ItemType Directory -Force -Path python-docker-example/db | Out-Null
Set-Location python-docker-example
WriteFile 'app.py' @'
# FastAPI application backed by a PostgreSQL database via SQLModel.
# The FastAPI lifespan handler creates database tables at startup.
# Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list).
# See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlmodel import Field, Session, SQLModel, create_engine, select
from config import settings
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
create_db_and_tables()
yield
app = FastAPI(lifespan=lifespan)
@app.get("/")
def hello() -> str:
return "Hello, Docker!"
@app.post("/heroes/")
def create_hero(hero: Hero) -> Hero:
with Session(engine) as session:
session.add(hero)
session.commit()
session.refresh(hero)
return hero
@app.get("/heroes/")
def read_heroes() -> Sequence[Hero]:
with Session(engine) as session:
heroes = session.exec(select(Hero)).all()
return heroes
'@
WriteFile 'config.py' @'
# Pydantic settings that read PostgreSQL connection details from the
# environment. Supports a password file (Docker secrets) via
# POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD.
# See https://docs.pydantic.dev/latest/concepts/pydantic_settings/
import os
from typing import Any
from pydantic import (
PostgresDsn,
computed_field,
field_validator,
model_validator,
)
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str | None = None
POSTGRES_PASSWORD_FILE: str | None = None
POSTGRES_DB: str
@model_validator(mode="before")
@classmethod
def check_postgres_password(cls, data: Any) -> Any:
"""Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set."""
if isinstance(data, dict):
password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore
password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore
if password_file is None and password is None:
raise ValueError(
"At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set."
)
return data # type: ignore
@field_validator("POSTGRES_PASSWORD_FILE", mode="before")
@classmethod
def read_password_from_file(cls, v: str | None) -> str | None:
if v is not None:
file_path = v
if os.path.exists(file_path):
with open(file_path) as file:
return file.read().strip()
raise ValueError(f"Password file {file_path} does not exist.")
return v
@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
url = MultiHostUrl.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD
if self.POSTGRES_PASSWORD
else self.POSTGRES_PASSWORD_FILE,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
return PostgresDsn(url)
settings = Settings() # type: ignore
'@
WriteFile 'requirements.txt' @'
# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
sqlmodel==0.0.24
psycopg[binary]==3.2.9
pydantic-settings==2.9.1
uvicorn==0.34.3
'@
WriteFile 'Dockerfile' @'
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
# The builder stage is also used directly in development (see compose.yaml).
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them
# into this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
# Use the minimal runtime image for production. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
COPY --from=builder /app .
EXPOSE 8000
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
'@
WriteFile 'compose.yaml' @'
services:
# Application service. The `target: builder` line builds the development
# image (includes a shell and tools); the production stage of the
# Dockerfile is unused in development.
server:
build:
context: .
target: builder
ports:
- 8000:8000
environment:
- POSTGRES_SERVER=db
- POSTGRES_USER=postgres
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
depends_on:
db:
condition: service_healthy
secrets:
- db-password
# Database service. Reads the password from a Docker secret mounted at
# /run/secrets/db-password. Compose waits for the healthcheck to pass
# before starting the server, via the server's depends_on.
db:
image: dhi.io/postgres:18
restart: always
user: postgres
secrets:
- db-password
volumes:
- db-data:/var/lib/postgresql
environment:
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
expose:
- 5432
healthcheck:
test: ["CMD", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:
secrets:
db-password:
file: db/password.txt
'@
WriteFile 'db/password.txt' @'
mysecretpassword
'@
WriteFile '.dockerignore' @'
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
'@
WriteFile '.gitignore' @'
# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txt
'@NoteTo learn more about the instructions in the Compose file, see Compose file reference.
Now, run the following docker compose up command to start your application.
$ docker compose up --build
Now test your API endpoint. Open a new terminal then make a request to the server using the curl commands:
Create an object with a POST request:
$ curl -X 'POST' \
'http://localhost:8000/heroes/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"id": 1,
"name": "my hero",
"secret_name": "austing",
"age": 12
}'
You should receive the following response:
{
"age": 12,
"id": 1,
"name": "my hero",
"secret_name": "austing"
}Now make a GET request:
$ curl -X 'GET' \
'http://localhost:8000/heroes/' \
-H 'accept: application/json'
You should receive the same response as above because it's the only object in the database.
{
"age": 12,
"id": 1,
"name": "my hero",
"secret_name": "austing"
}Press ctrl+c in the terminal to stop your application.
Automatically update services
Use Compose Watch to automatically update your running Compose services as you edit and save your code. For more details about Compose Watch, see Use Compose Watch.
Open your compose.yaml file in an IDE or text editor and add the highlighted
Compose Watch instructions.
# FastAPI application backed by a PostgreSQL database via SQLModel.
# The FastAPI lifespan handler creates database tables at startup.
# Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list).
# See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlmodel import Field, Session, SQLModel, create_engine, select
from config import settings
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
create_db_and_tables()
yield
app = FastAPI(lifespan=lifespan)
@app.get("/")
def hello() -> str:
return "Hello, Docker!"
@app.post("/heroes/")
def create_hero(hero: Hero) -> Hero:
with Session(engine) as session:
session.add(hero)
session.commit()
session.refresh(hero)
return hero
@app.get("/heroes/")
def read_heroes() -> Sequence[Hero]:
with Session(engine) as session:
heroes = session.exec(select(Hero)).all()
return heroes# Pydantic settings that read PostgreSQL connection details from the
# environment. Supports a password file (Docker secrets) via
# POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD.
# See https://docs.pydantic.dev/latest/concepts/pydantic_settings/
import os
from typing import Any
from pydantic import (
PostgresDsn,
computed_field,
field_validator,
model_validator,
)
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str | None = None
POSTGRES_PASSWORD_FILE: str | None = None
POSTGRES_DB: str
@model_validator(mode="before")
@classmethod
def check_postgres_password(cls, data: Any) -> Any:
"""Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set."""
if isinstance(data, dict):
password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore
password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore
if password_file is None and password is None:
raise ValueError(
"At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set."
)
return data # type: ignore
@field_validator("POSTGRES_PASSWORD_FILE", mode="before")
@classmethod
def read_password_from_file(cls, v: str | None) -> str | None:
if v is not None:
file_path = v
if os.path.exists(file_path):
with open(file_path) as file:
return file.read().strip()
raise ValueError(f"Password file {file_path} does not exist.")
return v
@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
url = MultiHostUrl.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD
if self.POSTGRES_PASSWORD
else self.POSTGRES_PASSWORD_FILE,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
return PostgresDsn(url)
settings = Settings() # type: ignore# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
sqlmodel==0.0.24
psycopg[binary]==3.2.9
pydantic-settings==2.9.1
uvicorn==0.34.3# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
# The builder stage is also used directly in development (see compose.yaml).
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them
# into this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
# Use the minimal runtime image for production. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
COPY --from=builder /app .
EXPOSE 8000
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]services:
# Application service. The `target: builder` line builds the development
# image (includes a shell and tools); the production stage of the
# Dockerfile is unused in development.
server:
build:
context: .
target: builder
ports:
- 8000:8000
environment:
- POSTGRES_SERVER=db
- POSTGRES_USER=postgres
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
depends_on:
db:
condition: service_healthy
secrets:
- db-password
develop:
watch:
- action: rebuild
path: .
db:
image: dhi.io/postgres:18
restart: always
user: postgres
secrets:
- db-password
volumes:
- db-data:/var/lib/postgresql
environment:
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
expose:
- 5432
healthcheck:
test: ["CMD", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:
secrets:
db-password:
file: db/password.txtmysecretpassword# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txtmkdir -p python-docker-example/db && cd python-docker-example
cat > app.py <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# FastAPI application backed by a PostgreSQL database via SQLModel.
# The FastAPI lifespan handler creates database tables at startup.
# Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list).
# See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlmodel import Field, Session, SQLModel, create_engine, select
from config import settings
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
create_db_and_tables()
yield
app = FastAPI(lifespan=lifespan)
@app.get("/")
def hello() -> str:
return "Hello, Docker!"
@app.post("/heroes/")
def create_hero(hero: Hero) -> Hero:
with Session(engine) as session:
session.add(hero)
session.commit()
session.refresh(hero)
return hero
@app.get("/heroes/")
def read_heroes() -> Sequence[Hero]:
with Session(engine) as session:
heroes = session.exec(select(Hero)).all()
return heroes
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > config.py <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Pydantic settings that read PostgreSQL connection details from the
# environment. Supports a password file (Docker secrets) via
# POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD.
# See https://docs.pydantic.dev/latest/concepts/pydantic_settings/
import os
from typing import Any
from pydantic import (
PostgresDsn,
computed_field,
field_validator,
model_validator,
)
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str | None = None
POSTGRES_PASSWORD_FILE: str | None = None
POSTGRES_DB: str
@model_validator(mode="before")
@classmethod
def check_postgres_password(cls, data: Any) -> Any:
"""Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set."""
if isinstance(data, dict):
password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore
password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore
if password_file is None and password is None:
raise ValueError(
"At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set."
)
return data # type: ignore
@field_validator("POSTGRES_PASSWORD_FILE", mode="before")
@classmethod
def read_password_from_file(cls, v: str | None) -> str | None:
if v is not None:
file_path = v
if os.path.exists(file_path):
with open(file_path) as file:
return file.read().strip()
raise ValueError(f"Password file {file_path} does not exist.")
return v
@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
url = MultiHostUrl.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD
if self.POSTGRES_PASSWORD
else self.POSTGRES_PASSWORD_FILE,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
return PostgresDsn(url)
settings = Settings() # type: ignore
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > requirements.txt <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
sqlmodel==0.0.24
psycopg[binary]==3.2.9
pydantic-settings==2.9.1
uvicorn==0.34.3
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > Dockerfile <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
# The builder stage is also used directly in development (see compose.yaml).
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them
# into this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
# Use the minimal runtime image for production. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
COPY --from=builder /app .
EXPOSE 8000
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > compose.yaml <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
services:
# Application service. The `target: builder` line builds the development
# image (includes a shell and tools); the production stage of the
# Dockerfile is unused in development.
server:
build:
context: .
target: builder
ports:
- 8000:8000
environment:
- POSTGRES_SERVER=db
- POSTGRES_USER=postgres
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
depends_on:
db:
condition: service_healthy
secrets:
- db-password
develop:
watch:
- action: rebuild
path: .
db:
image: dhi.io/postgres:18
restart: always
user: postgres
secrets:
- db-password
volumes:
- db-data:/var/lib/postgresql
environment:
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
expose:
- 5432
healthcheck:
test: ["CMD", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:
secrets:
db-password:
file: db/password.txt
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > db/password.txt <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
mysecretpassword
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > .dockerignore <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
__DOCKER_DOCS_SCAFFOLD_EOF__
cat > .gitignore <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txt
__DOCKER_DOCS_SCAFFOLD_EOF__# Write files as UTF-8 without BOM. Works on Windows PowerShell 5.1 and PowerShell 7+.
function WriteFile([string]$Path, [string]$Content) {
$full = Join-Path -Path (Get-Location).ProviderPath -ChildPath $Path
[System.IO.File]::WriteAllText($full, $Content, [System.Text.UTF8Encoding]::new($false))
}
New-Item -ItemType Directory -Force -Path python-docker-example | Out-Null
New-Item -ItemType Directory -Force -Path python-docker-example/db | Out-Null
Set-Location python-docker-example
WriteFile 'app.py' @'
# FastAPI application backed by a PostgreSQL database via SQLModel.
# The FastAPI lifespan handler creates database tables at startup.
# Endpoints: GET / (greeting), POST /heroes/ (create), GET /heroes/ (list).
# See https://fastapi.tiangolo.com/ and https://sqlmodel.tiangolo.com/
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlmodel import Field, Session, SQLModel, create_engine, select
from config import settings
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
create_db_and_tables()
yield
app = FastAPI(lifespan=lifespan)
@app.get("/")
def hello() -> str:
return "Hello, Docker!"
@app.post("/heroes/")
def create_hero(hero: Hero) -> Hero:
with Session(engine) as session:
session.add(hero)
session.commit()
session.refresh(hero)
return hero
@app.get("/heroes/")
def read_heroes() -> Sequence[Hero]:
with Session(engine) as session:
heroes = session.exec(select(Hero)).all()
return heroes
'@
WriteFile 'config.py' @'
# Pydantic settings that read PostgreSQL connection details from the
# environment. Supports a password file (Docker secrets) via
# POSTGRES_PASSWORD_FILE in addition to POSTGRES_PASSWORD.
# See https://docs.pydantic.dev/latest/concepts/pydantic_settings/
import os
from typing import Any
from pydantic import (
PostgresDsn,
computed_field,
field_validator,
model_validator,
)
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str | None = None
POSTGRES_PASSWORD_FILE: str | None = None
POSTGRES_DB: str
@model_validator(mode="before")
@classmethod
def check_postgres_password(cls, data: Any) -> Any:
"""Validate that either POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE is set."""
if isinstance(data, dict):
password_file: str | None = data.get("POSTGRES_PASSWORD_FILE") # type: ignore
password: str | None = data.get("POSTGRES_PASSWORD") # type: ignore
if password_file is None and password is None:
raise ValueError(
"At least one of POSTGRES_PASSWORD_FILE and POSTGRES_PASSWORD must be set."
)
return data # type: ignore
@field_validator("POSTGRES_PASSWORD_FILE", mode="before")
@classmethod
def read_password_from_file(cls, v: str | None) -> str | None:
if v is not None:
file_path = v
if os.path.exists(file_path):
with open(file_path) as file:
return file.read().strip()
raise ValueError(f"Password file {file_path} does not exist.")
return v
@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
url = MultiHostUrl.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD
if self.POSTGRES_PASSWORD
else self.POSTGRES_PASSWORD_FILE,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
return PostgresDsn(url)
settings = Settings() # type: ignore
'@
WriteFile 'requirements.txt' @'
# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
sqlmodel==0.0.24
psycopg[binary]==3.2.9
pydantic-settings==2.9.1
uvicorn==0.34.3
'@
WriteFile 'Dockerfile' @'
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
# Use the dev image to build and install dependencies.
# The builder stage is also used directly in development (see compose.yaml).
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them
# into this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
# Use the minimal runtime image for production. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
COPY --from=builder /app .
EXPOSE 8000
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
'@
WriteFile 'compose.yaml' @'
services:
# Application service. The `target: builder` line builds the development
# image (includes a shell and tools); the production stage of the
# Dockerfile is unused in development.
server:
build:
context: .
target: builder
ports:
- 8000:8000
environment:
- POSTGRES_SERVER=db
- POSTGRES_USER=postgres
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
depends_on:
db:
condition: service_healthy
secrets:
- db-password
develop:
watch:
- action: rebuild
path: .
db:
image: dhi.io/postgres:18
restart: always
user: postgres
secrets:
- db-password
volumes:
- db-data:/var/lib/postgresql
environment:
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
expose:
- 5432
healthcheck:
test: ["CMD", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:
secrets:
db-password:
file: db/password.txt
'@
WriteFile 'db/password.txt' @'
mysecretpassword
'@
WriteFile '.dockerignore' @'
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
'@
WriteFile '.gitignore' @'
# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txt
'@Run the following command to run your application with Compose Watch.
$ docker compose watch
In a terminal, curl the application to get a response.
$ curl http://localhost:8000
Hello, Docker!
Any changes to the application's source files on your local machine will now be immediately reflected in the running container.
Open python-docker-example/app.py in an IDE or text editor and update the Hello, Docker! string by adding a few more exclamation marks.
- return 'Hello, Docker!'
+ return 'Hello, Docker!!!'
Save the changes to app.py and then wait a few seconds for the application to rebuild. Curl the application again and verify that the updated text appears.
$ curl http://localhost:8000
Hello, Docker!!!
Press ctrl+c in the terminal to stop your application.
Linting, formatting, and type checking for Python
Prerequisites
Complete Develop your app. This topic requires a local Python installation because the tools and Git hooks introduced here run on your host. If you don't want to install Python locally, skip this topic. The same checks run in CI in the next topic.
Overview
Linting, formatting, and type checking are automated ways to catch bugs, enforce style, and spot type errors before code runs. Running them on every commit, in CI, and in your editor catches problems early when they're cheap to fix.
In this section, you'll configure three tools for your Python application. Ruff handles linting and formatting in a single fast pass. Pyright statically checks your code for type errors. Pre-commit hooks run both of these automatically before each Git commit so problems are caught locally before they're committed.
Linting and formatting with Ruff
Ruff is an extremely fast Python linter and formatter written in Rust. It replaces multiple tools like flake8, isort, and black with a single unified tool.
Create a pyproject.toml file in your python-docker-example directory:
# Configuration for code-quality tools.
# - [tool.ruff]: linting and formatting (https://docs.astral.sh/ruff/)
# - [tool.pyright]: static type checking (https://microsoft.github.io/pyright/)
[tool.ruff]
target-version = "py312"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG001", # unused arguments in functions
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"W191", # indentation contains tabs
"B904", # Allow raising exceptions without from e, for HTTPException
]mkdir python-docker-example && cd python-docker-example
cat > pyproject.toml <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Configuration for code-quality tools.
# - [tool.ruff]: linting and formatting (https://docs.astral.sh/ruff/)
# - [tool.pyright]: static type checking (https://microsoft.github.io/pyright/)
[tool.ruff]
target-version = "py312"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG001", # unused arguments in functions
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"W191", # indentation contains tabs
"B904", # Allow raising exceptions without from e, for HTTPException
]
__DOCKER_DOCS_SCAFFOLD_EOF__# Write files as UTF-8 without BOM. Works on Windows PowerShell 5.1 and PowerShell 7+.
function WriteFile([string]$Path, [string]$Content) {
$full = Join-Path -Path (Get-Location).ProviderPath -ChildPath $Path
[System.IO.File]::WriteAllText($full, $Content, [System.Text.UTF8Encoding]::new($false))
}
New-Item -ItemType Directory -Force -Path python-docker-example | Out-Null
Set-Location python-docker-example
WriteFile 'pyproject.toml' @'
# Configuration for code-quality tools.
# - [tool.ruff]: linting and formatting (https://docs.astral.sh/ruff/)
# - [tool.pyright]: static type checking (https://microsoft.github.io/pyright/)
[tool.ruff]
target-version = "py312"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG001", # unused arguments in functions
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"W191", # indentation contains tabs
"B904", # Allow raising exceptions without from e, for HTTPException
]
'@Install Ruff:
$ pip install ruff
If you're using a virtual environment, make sure it is activated so the ruff
command is available.
Run these commands to check and format your code:
# Check for errors
$ ruff check .
# Automatically fix fixable errors
$ ruff check --fix .
# Format code
$ ruff format .
Type checking with Pyright
Pyright is a fast static type checker for Python that works well with modern Python features.
Update pyproject.toml to add the Pyright configuration at the bottom.
# Configuration for code-quality tools.
# - [tool.ruff]: linting and formatting (https://docs.astral.sh/ruff/)
# - [tool.pyright]: static type checking (https://microsoft.github.io/pyright/)
[tool.ruff]
target-version = "py312"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG001", # unused arguments in functions
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"W191", # indentation contains tabs
"B904", # Allow raising exceptions without from e, for HTTPException
]
[tool.pyright]
typeCheckingMode = "strict"
pythonVersion = "3.12"
exclude = [".venv"]mkdir python-docker-example && cd python-docker-example
cat > pyproject.toml <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Configuration for code-quality tools.
# - [tool.ruff]: linting and formatting (https://docs.astral.sh/ruff/)
# - [tool.pyright]: static type checking (https://microsoft.github.io/pyright/)
[tool.ruff]
target-version = "py312"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG001", # unused arguments in functions
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"W191", # indentation contains tabs
"B904", # Allow raising exceptions without from e, for HTTPException
]
[tool.pyright]
typeCheckingMode = "strict"
pythonVersion = "3.12"
exclude = [".venv"]
__DOCKER_DOCS_SCAFFOLD_EOF__# Write files as UTF-8 without BOM. Works on Windows PowerShell 5.1 and PowerShell 7+.
function WriteFile([string]$Path, [string]$Content) {
$full = Join-Path -Path (Get-Location).ProviderPath -ChildPath $Path
[System.IO.File]::WriteAllText($full, $Content, [System.Text.UTF8Encoding]::new($false))
}
New-Item -ItemType Directory -Force -Path python-docker-example | Out-Null
Set-Location python-docker-example
WriteFile 'pyproject.toml' @'
# Configuration for code-quality tools.
# - [tool.ruff]: linting and formatting (https://docs.astral.sh/ruff/)
# - [tool.pyright]: static type checking (https://microsoft.github.io/pyright/)
[tool.ruff]
target-version = "py312"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG001", # unused arguments in functions
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"W191", # indentation contains tabs
"B904", # Allow raising exceptions without from e, for HTTPException
]
[tool.pyright]
typeCheckingMode = "strict"
pythonVersion = "3.12"
exclude = [".venv"]
'@Install Pyright and run it:
$ pip install pyright
$ pyright
Setting up pre-commit hooks
Pre-commit hooks run checks automatically before each commit on your local
machine. Create a .pre-commit-config.yaml file in your python-docker-example
directory to set up Ruff hooks:
# Pre-commit hook configuration. Runs Ruff (lint + format) on every
# `git commit`. See https://pre-commit.com/
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.15
hooks:
- id: ruff
args: [--fix]
- id: ruff-formatmkdir python-docker-example && cd python-docker-example
cat > .pre-commit-config.yaml <<'__DOCKER_DOCS_SCAFFOLD_EOF__'
# Pre-commit hook configuration. Runs Ruff (lint + format) on every
# `git commit`. See https://pre-commit.com/
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.15
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
__DOCKER_DOCS_SCAFFOLD_EOF__# Write files as UTF-8 without BOM. Works on Windows PowerShell 5.1 and PowerShell 7+.
function WriteFile([string]$Path, [string]$Content) {
$full = Join-Path -Path (Get-Location).ProviderPath -ChildPath $Path
[System.IO.File]::WriteAllText($full, $Content, [System.Text.UTF8Encoding]::new($false))
}
New-Item -ItemType Directory -Force -Path python-docker-example | Out-Null
Set-Location python-docker-example
WriteFile '.pre-commit-config.yaml' @'
# Pre-commit hook configuration. Runs Ruff (lint + format) on every
# `git commit`. See https://pre-commit.com/
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.15
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
'@To install and use:
$ pip install pre-commit
$ pre-commit install
$ git commit -m "Test commit" # Automatically runs checks
Summary
In this section, you learned how to:
- Configure and use Ruff for linting and formatting
- Set up Pyright for static type checking
- Automate checks with pre-commit hooks
These tools help maintain code quality and catch errors early in development.
Related information:
Next steps
- Customize linting rules to match your team's style preferences
- Explore advanced type checking features