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

@@ -17,16 +17,17 @@ ENCRYPTION_SECRET_KEY=
# ─── PostgreSQL ───────────────────────────────────────────────────────────────
POSTGRES_USER=congress
# If your password contains special characters ($, &, #, etc.), wrap it in single quotes
# to prevent Docker Compose from interpreting them as variable substitutions.
# Example: POSTGRES_PASSWORD='p@$$w0rd&safe'
# Any password works — special characters ($, &, #, @, etc.) are all fine.
# Wrap in single quotes if your password contains spaces or leading/trailing chars
# you want preserved. The DATABASE_URL is built automatically by the backend
# with proper URL-encoding, so you never need to escape anything here.
POSTGRES_PASSWORD=change-me
POSTGRES_DB=pocketveto
# These are constructed automatically from the above in docker-compose.yml.
# Override here only if connecting to an external DB.
# DATABASE_URL=postgresql+asyncpg://congress:congress@postgres:5432/pocketveto
# SYNC_DATABASE_URL=postgresql://congress:congress@postgres:5432/pocketveto
# DATABASE_URL and SYNC_DATABASE_URL are built automatically from the values
# above. Set these only if connecting to an external database.
# DATABASE_URL=postgresql+asyncpg://congress:mypassword@postgres:5432/pocketveto
# SYNC_DATABASE_URL=postgresql://congress:mypassword@postgres:5432/pocketveto
# ─── Redis ────────────────────────────────────────────────────────────────────
REDIS_URL=redis://redis:6379/0

3
.gitignore vendored
View File

@@ -18,4 +18,7 @@ frontend/out/
# Docker — bind-mount data directories (created on first run)
postgres/
redis/
# Secrets — never commit these
secrets/
*.log

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)

View File

@@ -3,8 +3,10 @@ services:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER:-congress}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-congress}
POSTGRES_DB: ${POSTGRES_DB:-pocketveto}
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- ./postgres/data:/var/lib/postgresql/data
healthcheck:
@@ -36,9 +38,11 @@ services:
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"
env_file: .env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-congress}:${POSTGRES_PASSWORD:-congress}@postgres:5432/${POSTGRES_DB:-pocketveto}
- SYNC_DATABASE_URL=postgresql://${POSTGRES_USER:-congress}:${POSTGRES_PASSWORD:-congress}@postgres:5432/${POSTGRES_DB:-pocketveto}
- POSTGRES_HOST=postgres
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
- REDIS_URL=redis://redis:6379/0
secrets:
- db_password
depends_on:
postgres:
condition: service_healthy
@@ -54,9 +58,11 @@ services:
command: celery -A app.workers.celery_app worker --loglevel=info --concurrency=4 -Q polling,documents,llm,news
env_file: .env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-congress}:${POSTGRES_PASSWORD:-congress}@postgres:5432/${POSTGRES_DB:-pocketveto}
- SYNC_DATABASE_URL=postgresql://${POSTGRES_USER:-congress}:${POSTGRES_PASSWORD:-congress}@postgres:5432/${POSTGRES_DB:-pocketveto}
- POSTGRES_HOST=postgres
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
- REDIS_URL=redis://redis:6379/0
secrets:
- db_password
depends_on:
postgres:
condition: service_healthy
@@ -72,9 +78,11 @@ services:
command: celery -A app.workers.celery_app beat --loglevel=info --scheduler=redbeat.RedBeatScheduler
env_file: .env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-congress}:${POSTGRES_PASSWORD:-congress}@postgres:5432/${POSTGRES_DB:-pocketveto}
- SYNC_DATABASE_URL=postgresql://${POSTGRES_USER:-congress}:${POSTGRES_PASSWORD:-congress}@postgres:5432/${POSTGRES_DB:-pocketveto}
- POSTGRES_HOST=postgres
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
- REDIS_URL=redis://redis:6379/0
secrets:
- db_password
depends_on:
redis:
condition: service_healthy
@@ -108,3 +116,7 @@ services:
networks:
app_network:
driver: bridge
secrets:
db_password:
file: ./secrets/db_password