mirror of
https://github.com/docker/docs.git
synced 2026-10-23 18:09:02 +03:00
Bare relative links in collapsed guides resolved to the guides landing page. Replace them with same-page anchors and remove one link to a deleted topic. Co-Authored-By: Codex <noreply@openai.com>
2263 lines
53 KiB
Markdown
2263 lines
53 KiB
Markdown
---
|
||
title: Python language-specific guide
|
||
linkTitle: Python
|
||
description: Containerize Python apps using Docker
|
||
keywords: Docker, getting started, Python, language
|
||
summary: |
|
||
This guide explains how to containerize Python applications using Docker.
|
||
aliases:
|
||
- /language/python/
|
||
- /guides/language/python/
|
||
- /language/python/build-images/
|
||
- /language/python/run-containers/
|
||
- /language/python/containerize/
|
||
- /language/python/develop/
|
||
- /language/python/configure-ci-cd/
|
||
- /guides/language/python/configure-ci-cd/
|
||
- /language/python/deploy/
|
||
- /guides/python/configure-github-actions/
|
||
- /guides/python/containerize/
|
||
- /guides/python/deploy/
|
||
- /guides/python/develop/
|
||
- /guides/python/lint-format-typing/
|
||
- /guides/python/secure-supply-chain/
|
||
params:
|
||
tags: [languages]
|
||
time: 20 minutes
|
||
---
|
||
|
||
> **Acknowledgment**
|
||
>
|
||
> This guide is a community contribution. Docker would like to thank
|
||
> [Esteban Maya](https://www.linkedin.com/in/esteban-x64/) and [Igor Aleksandrov](https://www.linkedin.com/in/igor-aleksandrov/) for their contribution
|
||
> to this guide.
|
||
|
||
The Python language-specific guide teaches you how to containerize a Python application using Docker. In this guide, you’ll learn how to:
|
||
|
||
- Containerize and run a Python application
|
||
- Set up a local environment to develop a Python application using containers
|
||
- Lint, format, typing and best practices
|
||
|
||
Start by containerizing an existing Python application.
|
||
|
||
## Containerize a Python application
|
||
|
||
### Prerequisites
|
||
|
||
- You have installed the latest version of [Docker Desktop](/get-started/get-docker.md).
|
||
|
||
### Overview
|
||
|
||
Containerizing your application means packaging it together with its
|
||
dependencies, configuration, and runtime into a single portable unit called a
|
||
container image. Running that image creates a container, an isolated process
|
||
that behaves the same on any machine, whether it's your laptop, a CI runner, or
|
||
a production server.
|
||
|
||
In this section, you'll containerize a simple
|
||
[FastAPI](https://fastapi.tiangolo.com) web application. You'll write a
|
||
`Dockerfile` that describes how to build the image, add a `compose.yaml` file
|
||
that defines how Docker runs your container, and then build and start the
|
||
application with one command.
|
||
|
||
You'll use [Docker Hardened Images](/dhi/) as the base. These are minimal,
|
||
secure Python images maintained by Docker.
|
||
|
||
### Create the application
|
||
|
||
The sample application is a minimal FastAPI service with a single endpoint
|
||
that returns a JSON greeting. Create the following files in a new
|
||
`python-docker-example` directory. To create all the files at once, switch to
|
||
the **Scaffold script** tab in the file browser and copy the shell command.
|
||
|
||
{{< files name="python-docker-example" >}}
|
||
|
||
{{< file path="app.py" status="new" >}}
|
||
|
||
```python
|
||
# A minimal FastAPI application.
|
||
# The root endpoint (GET /) returns a JSON "Hello World" response.
|
||
# See https://fastapi.tiangolo.com/ for the framework reference.
|
||
|
||
from fastapi import FastAPI
|
||
|
||
app = FastAPI()
|
||
|
||
|
||
@app.get("/")
|
||
async def root():
|
||
return {"message": "Hello World"}
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="requirements.txt" status="new" >}}
|
||
|
||
```text
|
||
# Python package dependencies for the application, pinned for reproducible builds.
|
||
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
|
||
|
||
fastapi==0.115.12
|
||
uvicorn==0.34.3
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path=".gitignore" status="new" >}}
|
||
|
||
```text
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< /files >}}
|
||
|
||
If you already have Python installed and want to verify the app works before
|
||
containerizing it, you can run it locally:
|
||
|
||
```console
|
||
$ python3 -m venv .venv
|
||
$ source .venv/bin/activate
|
||
$ pip install -r requirements.txt
|
||
$ uvicorn app:app --reload
|
||
```
|
||
|
||
> [!NOTE]
|
||
>
|
||
> On Windows, activate the virtual environment with `.venv\Scripts\activate`
|
||
> instead of `source .venv/bin/activate`.
|
||
|
||
If you don't have Python installed, skip ahead to the next section. The
|
||
remaining steps run the application in a container, with no local Python
|
||
required.
|
||
|
||
### Create the Docker assets
|
||
|
||
Sign in to the DHI registry so Docker can pull the Python base images during
|
||
the build. The available Python images are listed in the
|
||
[catalog](https://hub.docker.com/hardened-images/catalog/dhi/python).
|
||
|
||
```console
|
||
$ docker login dhi.io
|
||
```
|
||
|
||
Add the following three files to your `python-docker-example` directory. The
|
||
`Dockerfile` describes how to build the image, `compose.yaml` defines how
|
||
Docker runs the container, and `.dockerignore` keeps unwanted files out of the
|
||
build context.
|
||
|
||
> [!TIP]
|
||
>
|
||
> [Gordon](/ai/gordon/), Docker's AI assistant, can generate Docker assets for
|
||
> your project. Ask Gordon to create a Dockerfile, Compose file, and
|
||
> `.dockerignore` tailored to your application.
|
||
|
||
{{< files name="python-docker-example" >}}
|
||
|
||
{{< file path="app.py" >}}
|
||
|
||
```python
|
||
# A minimal FastAPI application.
|
||
# The root endpoint (GET /) returns a JSON "Hello World" response.
|
||
# See https://fastapi.tiangolo.com/ for the framework reference.
|
||
|
||
from fastapi import FastAPI
|
||
|
||
app = FastAPI()
|
||
|
||
|
||
@app.get("/")
|
||
async def root():
|
||
return {"message": "Hello World"}
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="requirements.txt" >}}
|
||
|
||
```text
|
||
# Python package dependencies for the application, pinned for reproducible builds.
|
||
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
|
||
|
||
fastapi==0.115.12
|
||
uvicorn==0.34.3
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="Dockerfile" status="new" >}}
|
||
|
||
```dockerfile
|
||
# syntax=docker/dockerfile:1
|
||
|
||
# Comments are provided throughout this file to help you get started.
|
||
# If you need more help, visit the Dockerfile reference guide at
|
||
# https://docs.docker.com/go/dockerfile-reference/
|
||
|
||
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
|
||
# For more information, see https://docs.docker.com/dhi/
|
||
|
||
# Use the dev image to build and install dependencies.
|
||
FROM dhi.io/python:3.12-dev AS builder
|
||
|
||
WORKDIR /app
|
||
|
||
RUN python3 -m venv /venv
|
||
ENV PATH="/venv/bin:$PATH"
|
||
|
||
# Download dependencies as a separate step to take advantage of Docker's caching.
|
||
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
|
||
# Leverage a bind mount to requirements.txt to avoid having to copy them into
|
||
# this layer.
|
||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||
--mount=type=bind,source=requirements.txt,target=requirements.txt \
|
||
pip install -r requirements.txt
|
||
|
||
# Use the minimal runtime image. It runs as nonroot by default.
|
||
FROM dhi.io/python:3.12
|
||
|
||
WORKDIR /app
|
||
|
||
COPY --from=builder /venv /venv
|
||
ENV PATH="/venv/bin:$PATH"
|
||
|
||
# Copy the source code into the container.
|
||
COPY . .
|
||
|
||
# Expose the port that the application listens on.
|
||
EXPOSE 8000
|
||
|
||
# Run the application.
|
||
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="compose.yaml" status="new" >}}
|
||
|
||
```yaml
|
||
# Comments are provided throughout this file to help you get started.
|
||
# If you need more help, visit the Docker Compose reference guide at
|
||
# https://docs.docker.com/go/compose-spec-reference/
|
||
|
||
# Here the instructions define your application as a service called "server".
|
||
# This service is built from the Dockerfile in the current directory.
|
||
# You can add other services your application may depend on here, such as a
|
||
# database or a cache. For examples, see the Awesome Compose repository:
|
||
# https://github.com/docker/awesome-compose
|
||
services:
|
||
server:
|
||
build:
|
||
context: .
|
||
ports:
|
||
- 8000:8000
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path=".dockerignore" status="new" >}}
|
||
|
||
```text
|
||
# Include any files or directories that you don't want to be copied to your
|
||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||
#
|
||
# For more help, visit the .dockerignore file reference guide at
|
||
# https://docs.docker.com/go/build-context-dockerignore/
|
||
|
||
**/.DS_Store
|
||
**/__pycache__
|
||
**/.venv
|
||
**/.classpath
|
||
**/.dockerignore
|
||
**/.env
|
||
**/.git
|
||
**/.gitignore
|
||
**/.project
|
||
**/.settings
|
||
**/.toolstarget
|
||
**/.vs
|
||
**/.vscode
|
||
**/*.*proj.user
|
||
**/*.dbmdl
|
||
**/*.jfm
|
||
**/bin
|
||
**/charts
|
||
**/docker-compose*
|
||
**/compose.y*ml
|
||
**/Dockerfile*
|
||
**/node_modules
|
||
**/npm-debug.log
|
||
**/obj
|
||
**/secrets.dev.yaml
|
||
**/values.dev.yaml
|
||
LICENSE
|
||
README.md
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path=".gitignore" >}}
|
||
|
||
```text
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< /files >}}
|
||
|
||
To learn more about each file, see the following:
|
||
|
||
- [Dockerfile](/reference/dockerfile.md)
|
||
- [.dockerignore](/reference/dockerfile.md#dockerignore-file)
|
||
- [compose.yaml](/reference/compose-file/_index.md)
|
||
|
||
### Run the application
|
||
|
||
Inside the `python-docker-example` directory, run the following command in a
|
||
terminal.
|
||
|
||
```console
|
||
$ docker compose up --build
|
||
```
|
||
|
||
Open a browser and view the application at [http://localhost:8000](http://localhost:8000). You should see a simple FastAPI application.
|
||
|
||
In the terminal, press `ctrl`+`c` to stop the application.
|
||
|
||
#### Run the application in the background
|
||
|
||
You can run the application detached from the terminal by adding the `-d`
|
||
option. Inside the `python-docker-example` directory, run the following command
|
||
in a terminal.
|
||
|
||
```console
|
||
$ docker compose up --build -d
|
||
```
|
||
|
||
Open a browser and view the application at [http://localhost:8000](http://localhost:8000).
|
||
|
||
To see the OpenAPI docs you can go to [http://localhost:8000/docs](http://localhost:8000/docs).
|
||
|
||
You should see a simple FastAPI application.
|
||
|
||
In the terminal, run the following command to stop the application.
|
||
|
||
```console
|
||
$ docker compose down
|
||
```
|
||
|
||
For more information about Compose commands, see the [Compose CLI
|
||
reference](/reference/cli/docker/compose/).
|
||
|
||
## Use containers for Python development
|
||
|
||
### Prerequisites
|
||
|
||
Complete [Containerize a Python application](#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.
|
||
|
||
> [!NOTE]
|
||
>
|
||
> The 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.
|
||
|
||
{{< files name="python-docker-example" >}}
|
||
|
||
{{< file path="app.py" status="modified" >}}
|
||
|
||
```python
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="config.py" status="new" >}}
|
||
|
||
```python
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="requirements.txt" status="modified" hl_lines="5-7" >}}
|
||
|
||
```text
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="Dockerfile" >}}
|
||
|
||
```dockerfile
|
||
# syntax=docker/dockerfile:1
|
||
|
||
# Comments are provided throughout this file to help you get started.
|
||
# If you need more help, visit the Dockerfile reference guide at
|
||
# https://docs.docker.com/go/dockerfile-reference/
|
||
|
||
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
|
||
# For more information, see https://docs.docker.com/dhi/
|
||
|
||
# Use the dev image to build and install dependencies.
|
||
FROM dhi.io/python:3.12-dev AS builder
|
||
|
||
WORKDIR /app
|
||
|
||
RUN python3 -m venv /venv
|
||
ENV PATH="/venv/bin:$PATH"
|
||
|
||
# Download dependencies as a separate step to take advantage of Docker's caching.
|
||
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
|
||
# Leverage a bind mount to requirements.txt to avoid having to copy them into
|
||
# this layer.
|
||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||
--mount=type=bind,source=requirements.txt,target=requirements.txt \
|
||
pip install -r requirements.txt
|
||
|
||
# Use the minimal runtime image. It runs as nonroot by default.
|
||
FROM dhi.io/python:3.12
|
||
|
||
WORKDIR /app
|
||
|
||
COPY --from=builder /venv /venv
|
||
ENV PATH="/venv/bin:$PATH"
|
||
|
||
# Copy the source code into the container.
|
||
COPY . .
|
||
|
||
# Expose the port that the application listens on.
|
||
EXPOSE 8000
|
||
|
||
# Run the application.
|
||
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="compose.yaml" >}}
|
||
|
||
```yaml
|
||
# Comments are provided throughout this file to help you get started.
|
||
# If you need more help, visit the Docker Compose reference guide at
|
||
# https://docs.docker.com/go/compose-spec-reference/
|
||
|
||
# Here the instructions define your application as a service called "server".
|
||
# This service is built from the Dockerfile in the current directory.
|
||
# You can add other services your application may depend on here, such as a
|
||
# database or a cache. For examples, see the Awesome Compose repository:
|
||
# https://github.com/docker/awesome-compose
|
||
services:
|
||
server:
|
||
build:
|
||
context: .
|
||
ports:
|
||
- 8000:8000
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path=".dockerignore" >}}
|
||
|
||
```text
|
||
# Include any files or directories that you don't want to be copied to your
|
||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||
#
|
||
# For more help, visit the .dockerignore file reference guide at
|
||
# https://docs.docker.com/go/build-context-dockerignore/
|
||
|
||
**/.DS_Store
|
||
**/__pycache__
|
||
**/.venv
|
||
**/.classpath
|
||
**/.dockerignore
|
||
**/.env
|
||
**/.git
|
||
**/.gitignore
|
||
**/.project
|
||
**/.settings
|
||
**/.toolstarget
|
||
**/.vs
|
||
**/.vscode
|
||
**/*.*proj.user
|
||
**/*.dbmdl
|
||
**/*.jfm
|
||
**/bin
|
||
**/charts
|
||
**/docker-compose*
|
||
**/compose.y*ml
|
||
**/Dockerfile*
|
||
**/node_modules
|
||
**/npm-debug.log
|
||
**/obj
|
||
**/secrets.dev.yaml
|
||
**/values.dev.yaml
|
||
LICENSE
|
||
README.md
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path=".gitignore" >}}
|
||
|
||
```text
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< /files >}}
|
||
|
||
### Update Docker assets
|
||
|
||
Replace `Dockerfile` and `compose.yaml` with the following.
|
||
|
||
{{< files name="python-docker-example" >}}
|
||
|
||
{{< file path="app.py" >}}
|
||
|
||
```python
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="config.py" >}}
|
||
|
||
```python
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="requirements.txt" >}}
|
||
|
||
```text
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="Dockerfile" status="modified" hl_lines="11,27-34,37,45" >}}
|
||
|
||
```dockerfile
|
||
# syntax=docker/dockerfile:1
|
||
|
||
# Comments are provided throughout this file to help you get started.
|
||
# If you need more help, visit the Dockerfile reference guide at
|
||
# https://docs.docker.com/go/dockerfile-reference/
|
||
|
||
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
|
||
# For more information, see https://docs.docker.com/dhi/
|
||
|
||
# Use the dev image to build and install dependencies.
|
||
# The builder stage is also used directly in development (see compose.yaml).
|
||
FROM dhi.io/python:3.12-dev AS builder
|
||
|
||
WORKDIR /app
|
||
|
||
RUN python3 -m venv /venv
|
||
ENV PATH="/venv/bin:$PATH"
|
||
|
||
# Download dependencies as a separate step to take advantage of Docker's caching.
|
||
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
|
||
# Leverage a bind mount to requirements.txt to avoid having to copy them
|
||
# into this layer.
|
||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||
--mount=type=bind,source=requirements.txt,target=requirements.txt \
|
||
pip install -r requirements.txt
|
||
|
||
# Copy the source code into the container.
|
||
COPY . .
|
||
|
||
# Expose the port that the application listens on.
|
||
EXPOSE 8000
|
||
|
||
# Run the application.
|
||
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
|
||
|
||
|
||
# Use the minimal runtime image for production. It runs as nonroot by default.
|
||
FROM dhi.io/python:3.12
|
||
|
||
WORKDIR /app
|
||
|
||
COPY --from=builder /venv /venv
|
||
ENV PATH="/venv/bin:$PATH"
|
||
|
||
COPY --from=builder /app .
|
||
|
||
EXPOSE 8000
|
||
|
||
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="compose.yaml" status="modified" hl_lines="8" >}}
|
||
|
||
```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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path=".dockerignore" >}}
|
||
|
||
```text
|
||
# Include any files or directories that you don't want to be copied to your
|
||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||
#
|
||
# For more help, visit the .dockerignore file reference guide at
|
||
# https://docs.docker.com/go/build-context-dockerignore/
|
||
|
||
**/.DS_Store
|
||
**/__pycache__
|
||
**/.venv
|
||
**/.classpath
|
||
**/.dockerignore
|
||
**/.env
|
||
**/.git
|
||
**/.gitignore
|
||
**/.project
|
||
**/.settings
|
||
**/.toolstarget
|
||
**/.vs
|
||
**/.vscode
|
||
**/*.*proj.user
|
||
**/*.dbmdl
|
||
**/*.jfm
|
||
**/bin
|
||
**/charts
|
||
**/docker-compose*
|
||
**/compose.y*ml
|
||
**/Dockerfile*
|
||
**/node_modules
|
||
**/npm-debug.log
|
||
**/obj
|
||
**/secrets.dev.yaml
|
||
**/values.dev.yaml
|
||
LICENSE
|
||
README.md
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path=".gitignore" >}}
|
||
|
||
```text
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< /files >}}
|
||
|
||
#### 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](/reference/cli/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.
|
||
|
||
{{< files name="python-docker-example" >}}
|
||
|
||
{{< file path="app.py" >}}
|
||
|
||
```python
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="config.py" >}}
|
||
|
||
```python
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="requirements.txt" >}}
|
||
|
||
```text
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="Dockerfile" >}}
|
||
|
||
```dockerfile
|
||
# syntax=docker/dockerfile:1
|
||
|
||
# Comments are provided throughout this file to help you get started.
|
||
# If you need more help, visit the Dockerfile reference guide at
|
||
# https://docs.docker.com/go/dockerfile-reference/
|
||
|
||
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
|
||
# For more information, see https://docs.docker.com/dhi/
|
||
|
||
# Use the dev image to build and install dependencies.
|
||
# The builder stage is also used directly in development (see compose.yaml).
|
||
FROM dhi.io/python:3.12-dev AS builder
|
||
|
||
WORKDIR /app
|
||
|
||
RUN python3 -m venv /venv
|
||
ENV PATH="/venv/bin:$PATH"
|
||
|
||
# Download dependencies as a separate step to take advantage of Docker's caching.
|
||
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
|
||
# Leverage a bind mount to requirements.txt to avoid having to copy them
|
||
# into this layer.
|
||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||
--mount=type=bind,source=requirements.txt,target=requirements.txt \
|
||
pip install -r requirements.txt
|
||
|
||
# Copy the source code into the container.
|
||
COPY . .
|
||
|
||
# Expose the port that the application listens on.
|
||
EXPOSE 8000
|
||
|
||
# Run the application.
|
||
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
|
||
|
||
|
||
# Use the minimal runtime image for production. It runs as nonroot by default.
|
||
FROM dhi.io/python:3.12
|
||
|
||
WORKDIR /app
|
||
|
||
COPY --from=builder /venv /venv
|
||
ENV PATH="/venv/bin:$PATH"
|
||
|
||
COPY --from=builder /app .
|
||
|
||
EXPOSE 8000
|
||
|
||
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="compose.yaml" status="modified" hl_lines="11-46" >}}
|
||
|
||
```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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="db/password.txt" status="new" >}}
|
||
|
||
```text
|
||
mysecretpassword
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path=".dockerignore" >}}
|
||
|
||
```text
|
||
# Include any files or directories that you don't want to be copied to your
|
||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||
#
|
||
# For more help, visit the .dockerignore file reference guide at
|
||
# https://docs.docker.com/go/build-context-dockerignore/
|
||
|
||
**/.DS_Store
|
||
**/__pycache__
|
||
**/.venv
|
||
**/.classpath
|
||
**/.dockerignore
|
||
**/.env
|
||
**/.git
|
||
**/.gitignore
|
||
**/.project
|
||
**/.settings
|
||
**/.toolstarget
|
||
**/.vs
|
||
**/.vscode
|
||
**/*.*proj.user
|
||
**/*.dbmdl
|
||
**/*.jfm
|
||
**/bin
|
||
**/charts
|
||
**/docker-compose*
|
||
**/compose.y*ml
|
||
**/Dockerfile*
|
||
**/node_modules
|
||
**/npm-debug.log
|
||
**/obj
|
||
**/secrets.dev.yaml
|
||
**/values.dev.yaml
|
||
LICENSE
|
||
README.md
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path=".gitignore" >}}
|
||
|
||
```text
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< /files >}}
|
||
|
||
> [!NOTE]
|
||
>
|
||
> To learn more about the instructions in the Compose file, see [Compose file
|
||
> reference](/reference/compose-file/).
|
||
|
||
Now, run the following `docker compose up` command to start your application.
|
||
|
||
```console
|
||
$ 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:
|
||
|
||
```console
|
||
$ 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:
|
||
|
||
```json
|
||
{
|
||
"age": 12,
|
||
"id": 1,
|
||
"name": "my hero",
|
||
"secret_name": "austing"
|
||
}
|
||
```
|
||
|
||
Now make a GET request:
|
||
|
||
```console
|
||
$ 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.
|
||
|
||
```json
|
||
{
|
||
"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](/manuals/compose/how-tos/file-watch.md).
|
||
|
||
Open your `compose.yaml` file in an IDE or text editor and add the highlighted
|
||
Compose Watch instructions.
|
||
|
||
{{< files name="python-docker-example" >}}
|
||
|
||
{{< file path="app.py" >}}
|
||
|
||
```python
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="config.py" >}}
|
||
|
||
```python
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="requirements.txt" >}}
|
||
|
||
```text
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="Dockerfile" >}}
|
||
|
||
```dockerfile
|
||
# syntax=docker/dockerfile:1
|
||
|
||
# Comments are provided throughout this file to help you get started.
|
||
# If you need more help, visit the Dockerfile reference guide at
|
||
# https://docs.docker.com/go/dockerfile-reference/
|
||
|
||
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
|
||
# For more information, see https://docs.docker.com/dhi/
|
||
|
||
# Use the dev image to build and install dependencies.
|
||
# The builder stage is also used directly in development (see compose.yaml).
|
||
FROM dhi.io/python:3.12-dev AS builder
|
||
|
||
WORKDIR /app
|
||
|
||
RUN python3 -m venv /venv
|
||
ENV PATH="/venv/bin:$PATH"
|
||
|
||
# Download dependencies as a separate step to take advantage of Docker's caching.
|
||
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
|
||
# Leverage a bind mount to requirements.txt to avoid having to copy them
|
||
# into this layer.
|
||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||
--mount=type=bind,source=requirements.txt,target=requirements.txt \
|
||
pip install -r requirements.txt
|
||
|
||
# Copy the source code into the container.
|
||
COPY . .
|
||
|
||
# Expose the port that the application listens on.
|
||
EXPOSE 8000
|
||
|
||
# Run the application.
|
||
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
|
||
|
||
|
||
# Use the minimal runtime image for production. It runs as nonroot by default.
|
||
FROM dhi.io/python:3.12
|
||
|
||
WORKDIR /app
|
||
|
||
COPY --from=builder /venv /venv
|
||
ENV PATH="/venv/bin:$PATH"
|
||
|
||
COPY --from=builder /app .
|
||
|
||
EXPOSE 8000
|
||
|
||
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="compose.yaml" status="modified" hl_lines="21-24" >}}
|
||
|
||
```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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path="db/password.txt" >}}
|
||
|
||
```text
|
||
mysecretpassword
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path=".dockerignore" >}}
|
||
|
||
```text
|
||
# Include any files or directories that you don't want to be copied to your
|
||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||
#
|
||
# For more help, visit the .dockerignore file reference guide at
|
||
# https://docs.docker.com/go/build-context-dockerignore/
|
||
|
||
**/.DS_Store
|
||
**/__pycache__
|
||
**/.venv
|
||
**/.classpath
|
||
**/.dockerignore
|
||
**/.env
|
||
**/.git
|
||
**/.gitignore
|
||
**/.project
|
||
**/.settings
|
||
**/.toolstarget
|
||
**/.vs
|
||
**/.vscode
|
||
**/*.*proj.user
|
||
**/*.dbmdl
|
||
**/*.jfm
|
||
**/bin
|
||
**/charts
|
||
**/docker-compose*
|
||
**/compose.y*ml
|
||
**/Dockerfile*
|
||
**/node_modules
|
||
**/npm-debug.log
|
||
**/obj
|
||
**/secrets.dev.yaml
|
||
**/values.dev.yaml
|
||
LICENSE
|
||
README.md
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< file path=".gitignore" >}}
|
||
|
||
```text
|
||
# 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
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< /files >}}
|
||
|
||
Run the following command to run your application with Compose Watch.
|
||
|
||
```console
|
||
$ docker compose watch
|
||
```
|
||
|
||
In a terminal, curl the application to get a response.
|
||
|
||
```console
|
||
$ 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.
|
||
|
||
```diff
|
||
- 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.
|
||
|
||
```console
|
||
$ curl http://localhost:8000
|
||
Hello, Docker!!!
|
||
```
|
||
|
||
Press `ctrl+c` in the terminal to stop your application.
|
||
|
||
## Linting, formatting, and type checking for Python
|
||
|
||
### Prerequisites
|
||
|
||
Complete [Develop your app](#use-containers-for-python-development). This topic requires a local Python
|
||
installation because the tools and Git hooks introduced here run on your
|
||
host. If you don't want to install Python locally, skip this topic. You can
|
||
also run the same checks in CI.
|
||
|
||
### Overview
|
||
|
||
Linting, formatting, and type checking are automated ways to catch bugs,
|
||
enforce style, and spot type errors before code runs. Running them on every
|
||
commit, in CI, and in your editor catches problems early when they're cheap
|
||
to fix.
|
||
|
||
In this section, you'll configure three tools for your Python application.
|
||
Ruff handles linting and formatting in a single fast pass. Pyright statically
|
||
checks your code for type errors. Pre-commit hooks run both of these
|
||
automatically before each Git commit so problems are caught locally before
|
||
they're committed.
|
||
|
||
### Linting and formatting with Ruff
|
||
|
||
Ruff is an extremely fast Python linter and formatter written in Rust. It replaces multiple tools like flake8, isort, and black with a single unified tool.
|
||
|
||
Create a `pyproject.toml` file in your `python-docker-example` directory:
|
||
|
||
{{< files name="python-docker-example" >}}
|
||
|
||
{{< file path="pyproject.toml" status="new" >}}
|
||
|
||
```toml
|
||
# Configuration for code-quality tools.
|
||
# - [tool.ruff]: linting and formatting (https://docs.astral.sh/ruff/)
|
||
# - [tool.pyright]: static type checking (https://microsoft.github.io/pyright/)
|
||
|
||
[tool.ruff]
|
||
target-version = "py312"
|
||
|
||
[tool.ruff.lint]
|
||
select = [
|
||
"E", # pycodestyle errors
|
||
"W", # pycodestyle warnings
|
||
"F", # pyflakes
|
||
"I", # isort
|
||
"B", # flake8-bugbear
|
||
"C4", # flake8-comprehensions
|
||
"UP", # pyupgrade
|
||
"ARG001", # unused arguments in functions
|
||
]
|
||
ignore = [
|
||
"E501", # line too long, handled by black
|
||
"B008", # do not perform function calls in argument defaults
|
||
"W191", # indentation contains tabs
|
||
"B904", # Allow raising exceptions without from e, for HTTPException
|
||
]
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< /files >}}
|
||
|
||
Install Ruff:
|
||
|
||
```console
|
||
$ pip install ruff
|
||
```
|
||
|
||
If you're using a virtual environment, make sure it is activated so the `ruff`
|
||
command is available.
|
||
|
||
Run these commands to check and format your code:
|
||
|
||
```console
|
||
# Check for errors
|
||
$ ruff check .
|
||
|
||
# Automatically fix fixable errors
|
||
$ ruff check --fix .
|
||
|
||
# Format code
|
||
$ ruff format .
|
||
```
|
||
|
||
### Type checking with Pyright
|
||
|
||
Pyright is a fast static type checker for Python that works well with modern Python features.
|
||
|
||
Update `pyproject.toml` to add the Pyright configuration at the bottom.
|
||
|
||
{{< files name="python-docker-example" >}}
|
||
|
||
{{< file path="pyproject.toml" status="modified" hl_lines="25-29" >}}
|
||
|
||
```toml
|
||
# Configuration for code-quality tools.
|
||
# - [tool.ruff]: linting and formatting (https://docs.astral.sh/ruff/)
|
||
# - [tool.pyright]: static type checking (https://microsoft.github.io/pyright/)
|
||
|
||
[tool.ruff]
|
||
target-version = "py312"
|
||
|
||
[tool.ruff.lint]
|
||
select = [
|
||
"E", # pycodestyle errors
|
||
"W", # pycodestyle warnings
|
||
"F", # pyflakes
|
||
"I", # isort
|
||
"B", # flake8-bugbear
|
||
"C4", # flake8-comprehensions
|
||
"UP", # pyupgrade
|
||
"ARG001", # unused arguments in functions
|
||
]
|
||
ignore = [
|
||
"E501", # line too long, handled by black
|
||
"B008", # do not perform function calls in argument defaults
|
||
"W191", # indentation contains tabs
|
||
"B904", # Allow raising exceptions without from e, for HTTPException
|
||
]
|
||
|
||
[tool.pyright]
|
||
typeCheckingMode = "strict"
|
||
pythonVersion = "3.12"
|
||
exclude = [".venv"]
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< /files >}}
|
||
|
||
Install Pyright and run it:
|
||
|
||
```console
|
||
$ pip install pyright
|
||
$ pyright
|
||
```
|
||
|
||
### Setting up pre-commit hooks
|
||
|
||
Pre-commit hooks run checks automatically before each commit on your local
|
||
machine. Create a `.pre-commit-config.yaml` file in your `python-docker-example`
|
||
directory to set up Ruff hooks:
|
||
|
||
{{< files name="python-docker-example" >}}
|
||
|
||
{{< file path=".pre-commit-config.yaml" status="new" >}}
|
||
|
||
```yaml
|
||
# Pre-commit hook configuration. Runs Ruff (lint + format) on every
|
||
# `git commit`. See https://pre-commit.com/
|
||
|
||
repos:
|
||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||
rev: v0.15.15
|
||
hooks:
|
||
- id: ruff
|
||
args: [--fix]
|
||
- id: ruff-format
|
||
```
|
||
|
||
{{< /file >}}
|
||
|
||
{{< /files >}}
|
||
|
||
To install and use:
|
||
|
||
```console
|
||
$ pip install pre-commit
|
||
$ pre-commit install
|
||
$ git commit -m "Test commit" # Automatically runs checks
|
||
```
|
||
|
||
### Summary
|
||
|
||
In this section, you learned how to:
|
||
|
||
- Configure and use Ruff for linting and formatting
|
||
- Set up Pyright for static type checking
|
||
- Automate checks with pre-commit hooks
|
||
|
||
These tools help maintain code quality and catch errors early in development.
|
||
|
||
Related information:
|
||
|
||
- [Ruff documentation](https://docs.astral.sh/ruff/)
|
||
- [Pyright documentation](https://microsoft.github.io/pyright/)
|
||
- [pre-commit framework](https://pre-commit.com/)
|
||
|
||
### Next steps
|
||
|
||
- Customize linting rules to match your team's style preferences
|
||
- Explore advanced type checking features
|