Load Pydantic Settings from Docker Secrets
Docker and Swarm mount secrets as files under /run/secrets, not as environment variables — which is deliberately safer, because files on a tmpfs are not exposed by docker inspect the way env vars are. pydantic-settings reads this layout natively through secrets_dir, so you can wire mounted secrets into a validated model with one line. This extends the pydantic-settings fundamentals and the secrets management patterns.
Problem 1: passing secrets as environment variables
# ANTI-PATTERN — secret in an env var, visible to docker inspect
import os
db_password = os.environ["DB_PASSWORD"]
Anyone with docker inspect access to the container reads the value, and it is trivially leaked into a log line.
Problem 2: reading the secret file by hand
# ANTI-PATTERN — manual file read, no validation, no masking
with open("/run/secrets/db_password") as fh:
db_password = fh.read() # trailing newline, plain str, unvalidated
Hand-reading each file misses validation, leaves a trailing newline, and stores the credential as a plain str that can leak.
Secure implementation
Point secrets_dir at the mount and type the field as SecretStr; pydantic-settings reads and masks it.
# config.py
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
secrets_dir="/run/secrets", # Docker/Swarm mounts secrets here
extra="forbid",
)
db_password: SecretStr # read from /run/secrets/db_password, masked
api_key: SecretStr # read from /run/secrets/api_key
settings = Settings()
# compose.yaml
services:
app:
build: .
secrets:
- db_password
- api_key
secrets:
db_password:
file: ./secrets/db_password.txt # or external: true for Swarm-managed
api_key:
file: ./secrets/api_key.txt
Gotchas & version-specific behaviour
- pydantic-settings strips the trailing newline that Docker secret files often carry — a manual
open().read()would not. - Field names must match the secret file names (respecting
case_sensitive);db_passwordreads/run/secrets/db_password. secrets_diris a lower-priority source than environment variables, so an explicit env var of the same name still wins.- The secrets mount is a tmpfs — it never lands in the image and is gone when the container stops.
- Combine with a manager: in Swarm,
external: truesecrets can be rotated without rebuilding the image.
Production parity checklist
- Mount credentials as Docker/Swarm secrets, not environment variables.
- Type every secret field as
SecretStrand setextra="forbid". - Keep the local
./secrets/*.txtfiles gitignored; ship a.examplealongside. - Validate the model at startup so a missing secret fails the container immediately.
- For managed rotation, use Swarm external secrets or a secret manager.
Frequently asked questions
How does pydantic-settings read Docker secrets?
Set secrets_dir in SettingsConfigDict pointing at /run/secrets. pydantic-settings reads each field from a same-named file in that directory, so a secret mounted at /run/secrets/db_password populates the db_password field.
Why are Docker secrets safer than environment variables for credentials?
Environment variables are visible via docker inspect and easy to leak into logs. Docker secrets are mounted as in-memory tmpfs files readable only by the container, and never appear in the image or docker inspect output.
What precedence do file secrets have in pydantic-settings?
By default init arguments win, then environment variables, then the .env file, then the secrets_dir files. So a file secret is a lower-priority source than an explicit environment variable of the same name.
Key takeaways
Mount credentials as Docker secrets and read them with secrets_dir into SecretStr fields; the value stays on a tmpfs, out of docker inspect, and masked in every log line.