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 ───────────────────────────────────────────────────────────────
|
# ─── PostgreSQL ───────────────────────────────────────────────────────────────
|
||||||
POSTGRES_USER=congress
|
POSTGRES_USER=congress
|
||||||
# If your password contains special characters ($, &, #, etc.), wrap it in single quotes
|
# Any password works — special characters ($, &, #, @, etc.) are all fine.
|
||||||
# to prevent Docker Compose from interpreting them as variable substitutions.
|
# Wrap in single quotes if your password contains spaces or leading/trailing chars
|
||||||
# Example: POSTGRES_PASSWORD='p@$$w0rd&safe'
|
# 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_PASSWORD=change-me
|
||||||
POSTGRES_DB=pocketveto
|
POSTGRES_DB=pocketveto
|
||||||
|
|
||||||
# These are constructed automatically from the above in docker-compose.yml.
|
# DATABASE_URL and SYNC_DATABASE_URL are built automatically from the values
|
||||||
# Override here only if connecting to an external DB.
|
# above. Set these only if connecting to an external database.
|
||||||
# DATABASE_URL=postgresql+asyncpg://congress:congress@postgres:5432/pocketveto
|
# DATABASE_URL=postgresql+asyncpg://congress:mypassword@postgres:5432/pocketveto
|
||||||
# SYNC_DATABASE_URL=postgresql://congress:congress@postgres:5432/pocketveto
|
# SYNC_DATABASE_URL=postgresql://congress:mypassword@postgres:5432/pocketveto
|
||||||
|
|
||||||
# ─── Redis ────────────────────────────────────────────────────────────────────
|
# ─── Redis ────────────────────────────────────────────────────────────────────
|
||||||
REDIS_URL=redis://redis:6379/0
|
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)
|
# Docker — bind-mount data directories (created on first run)
|
||||||
postgres/
|
postgres/
|
||||||
redis/
|
redis/
|
||||||
|
|
||||||
|
# Secrets — never commit these
|
||||||
|
secrets/
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import os
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
from urllib.parse import quote as urlquote
|
||||||
from pydantic import model_validator
|
from pydantic import model_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
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)
|
# Falls back to JWT_SECRET_KEY derivation if not set (not recommended for production)
|
||||||
ENCRYPTION_SECRET_KEY: str = ""
|
ENCRYPTION_SECRET_KEY: str = ""
|
||||||
|
|
||||||
# Database
|
# Database — built automatically from components (supports any characters in password)
|
||||||
DATABASE_URL: str = "postgresql+asyncpg://congress:congress@postgres:5432/pocketveto"
|
POSTGRES_USER: str = "congress"
|
||||||
SYNC_DATABASE_URL: str = "postgresql://congress:congress@postgres:5432/pocketveto"
|
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
|
||||||
REDIS_URL: str = "redis://redis:6379/0"
|
REDIS_URL: str = "redis://redis:6379/0"
|
||||||
@@ -61,12 +72,22 @@ class Settings(BaseSettings):
|
|||||||
PYTRENDS_ENABLED: bool = True
|
PYTRENDS_ENABLED: bool = True
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@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":
|
if self.JWT_SECRET_KEY == "change-me-in-production":
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"JWT_SECRET_KEY must be set to a secure random value in .env. "
|
"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))\""
|
"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
|
return self
|
||||||
|
|
||||||
# SMTP (Email notifications)
|
# SMTP (Email notifications)
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ services:
|
|||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-congress}
|
POSTGRES_USER: ${POSTGRES_USER:-congress}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-congress}
|
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-pocketveto}
|
POSTGRES_DB: ${POSTGRES_DB:-pocketveto}
|
||||||
|
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
|
||||||
|
secrets:
|
||||||
|
- db_password
|
||||||
volumes:
|
volumes:
|
||||||
- ./postgres/data:/var/lib/postgresql/data
|
- ./postgres/data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -36,9 +38,11 @@ services:
|
|||||||
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"
|
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-congress}:${POSTGRES_PASSWORD:-congress}@postgres:5432/${POSTGRES_DB:-pocketveto}
|
- POSTGRES_HOST=postgres
|
||||||
- SYNC_DATABASE_URL=postgresql://${POSTGRES_USER:-congress}:${POSTGRES_PASSWORD:-congress}@postgres:5432/${POSTGRES_DB:-pocketveto}
|
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
|
||||||
- REDIS_URL=redis://redis:6379/0
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
secrets:
|
||||||
|
- db_password
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
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
|
command: celery -A app.workers.celery_app worker --loglevel=info --concurrency=4 -Q polling,documents,llm,news
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-congress}:${POSTGRES_PASSWORD:-congress}@postgres:5432/${POSTGRES_DB:-pocketveto}
|
- POSTGRES_HOST=postgres
|
||||||
- SYNC_DATABASE_URL=postgresql://${POSTGRES_USER:-congress}:${POSTGRES_PASSWORD:-congress}@postgres:5432/${POSTGRES_DB:-pocketveto}
|
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
|
||||||
- REDIS_URL=redis://redis:6379/0
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
secrets:
|
||||||
|
- db_password
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -72,9 +78,11 @@ services:
|
|||||||
command: celery -A app.workers.celery_app beat --loglevel=info --scheduler=redbeat.RedBeatScheduler
|
command: celery -A app.workers.celery_app beat --loglevel=info --scheduler=redbeat.RedBeatScheduler
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-congress}:${POSTGRES_PASSWORD:-congress}@postgres:5432/${POSTGRES_DB:-pocketveto}
|
- POSTGRES_HOST=postgres
|
||||||
- SYNC_DATABASE_URL=postgresql://${POSTGRES_USER:-congress}:${POSTGRES_PASSWORD:-congress}@postgres:5432/${POSTGRES_DB:-pocketveto}
|
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
|
||||||
- REDIS_URL=redis://redis:6379/0
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
secrets:
|
||||||
|
- db_password
|
||||||
depends_on:
|
depends_on:
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -108,3 +116,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
app_network:
|
app_network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
db_password:
|
||||||
|
file: ./secrets/db_password
|
||||||
|
|||||||
Reference in New Issue
Block a user