fix: DB password — read from secrets file, bypasses Docker Compose interpolation

- Add secrets/db_password file support to docker-compose.yml (Docker secrets mount)
- config.py reads POSTGRES_PASSWORD_FILE if set, builds DATABASE_URL with proper URL encoding
- Remove inline DATABASE_URL construction from docker-compose.yml (was subject to $VAR interpolation)
- Any password with any characters now works — no escaping needed

Authored by: Jack Levy
This commit is contained in:
Jack Levy
2026-03-15 17:31:09 -04:00
parent 9f4c9c7a56
commit 8911351c99
4 changed files with 55 additions and 18 deletions

View File

@@ -1,4 +1,6 @@
import os
from functools import lru_cache
from urllib.parse import quote as urlquote
from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -19,9 +21,18 @@ class Settings(BaseSettings):
# Falls back to JWT_SECRET_KEY derivation if not set (not recommended for production)
ENCRYPTION_SECRET_KEY: str = ""
# Database
DATABASE_URL: str = "postgresql+asyncpg://congress:congress@postgres:5432/pocketveto"
SYNC_DATABASE_URL: str = "postgresql://congress:congress@postgres:5432/pocketveto"
# Database — built automatically from components (supports any characters in password)
POSTGRES_USER: str = "congress"
POSTGRES_PASSWORD: str = "congress"
POSTGRES_DB: str = "pocketveto"
POSTGRES_HOST: str = "postgres"
POSTGRES_PORT: int = 5432
# Path to a file containing the raw DB password (any chars, no escaping needed).
# When set, this takes priority over POSTGRES_PASSWORD.
POSTGRES_PASSWORD_FILE: str = ""
# Override these only if connecting to an external DB not managed by Docker Compose
DATABASE_URL: str = ""
SYNC_DATABASE_URL: str = ""
# Redis
REDIS_URL: str = "redis://redis:6379/0"
@@ -61,12 +72,22 @@ class Settings(BaseSettings):
PYTRENDS_ENABLED: bool = True
@model_validator(mode="after")
def check_secrets(self) -> "Settings":
def check_secrets_and_build_db_url(self) -> "Settings":
if self.JWT_SECRET_KEY == "change-me-in-production":
raise ValueError(
"JWT_SECRET_KEY must be set to a secure random value in .env. "
"Generate one with: python -c \"import secrets; print(secrets.token_hex(32))\""
)
if not self.DATABASE_URL:
if self.POSTGRES_PASSWORD_FILE and os.path.isfile(self.POSTGRES_PASSWORD_FILE):
with open(self.POSTGRES_PASSWORD_FILE) as f:
raw_pw = f.read().strip()
else:
raw_pw = self.POSTGRES_PASSWORD
pw = urlquote(raw_pw, safe="")
base = f"{self.POSTGRES_USER}:{pw}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
self.DATABASE_URL = f"postgresql+asyncpg://{base}"
self.SYNC_DATABASE_URL = f"postgresql://{base}"
return self
# SMTP (Email notifications)