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://en.claudeai.life/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://en.claudeai.life/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://en.claudeai.life/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://en.claudeai.life/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://en.claudeai.life/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://en.claudeai.life/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://en.claudeai.life/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://en.claudeai.life/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://en.claudeai.life/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://en.claudeai.life/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://en.claudeai.life/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://en.claudeai.life/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://en.claudeai.life/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://en.claudeai.life/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://en.claudeai.life/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://en.claudeai.life/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://en.claudeai.life/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://en.claudeai.life/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://en.claudeai.life/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://en.claudeai.life/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://en.claudeai.life/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://en.claudeai.life/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://en.claudeai.life/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://en.claudeai.life/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://en.claudeai.life/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://en.claudeai.life/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://en.claudeai.life/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://en.claudeai.life/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://en.claudeai.life/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://en.claudeai.life/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://en.claudeai.life/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://en.claudeai.life/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://en.claudeai.life/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://en.claudeai.life/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://en.claudeai.life/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://en.claudeai.life/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://en.claudeai.life/go/dockerfile-reference/
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://en.claudeai.life/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://en.claudeai.life/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.
Summary
In this section, you took a look at setting up your Compose file to add a local database and persist data. You also learned how to use Compose Watch to automatically rebuild and run your container when you update your code.
Related information:
Next steps
In the next section, you'll learn how you can set up linting, formatting, and type checking to follow the best practices in Python apps.