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:
15
.env.example
15
.env.example
@@ -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
3
.gitignore
vendored
@@ -18,4 +18,7 @@ frontend/out/
|
||||
# Docker — bind-mount data directories (created on first run)
|
||||
postgres/
|
||||
redis/
|
||||
|
||||
# Secrets — never commit these
|
||||
secrets/
|
||||
*.log
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user