diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 6c2313b..d071308 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -51,16 +51,23 @@ Create a file named `.env` in the **project root** (same folder as POSTGRES_DB=wpsuite POSTGRES_USER=wpsuite POSTGRES_PASSWORD= - -# Must match the POSTGRES_* values above. Host is the compose service name "db". -DATABASE_URL=postgresql+psycopg://wpsuite:@db:5432/wpsuite ``` +That's it — the API now builds its own connection string from these three +values and **encodes the password automatically**, so a password with special +characters (`@ ! # : /` …) works without any manual escaping. `DATABASE_URL` +is **optional** and only needed if you want to point the API at some other +database; if you do set it, you must URL-encode the password yourself, and it's +ignored whenever the three `POSTGRES_*` values are present. + Generate a strong password with `openssl rand -base64 32`. -These are the only credentials in the system: the `db` container initialises -Postgres from `POSTGRES_*`, and the `api` container connects with the matching -`DATABASE_URL`. Neither value appears in the compose file or in git. +> **Portainer note:** for a Git-based stack these go in the stack's +> **Environment variables** section (Portainer doesn't read a local `.env`). +> Set `POSTGRES_DB` / `POSTGRES_USER` / `POSTGRES_PASSWORD` there. + +These are the only credentials in the system, and they never appear in the +compose file or in git. ## 3. Point your reverse proxy at the nginx container diff --git a/docker-compose.yml b/docker-compose.yml index def674d..a5f0070 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,14 @@ services: build: . container_name: wp_api environment: - DATABASE_URL: ${DATABASE_URL} + # Preferred: the API builds its own connection string from these and + # encodes the password automatically (no manual URL-encoding needed). + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_HOST: db + # Optional full-URL override (must be URL-encoded if used). + DATABASE_URL: ${DATABASE_URL:-} restart: unless-stopped depends_on: db: diff --git a/server/db.py b/server/db.py index b8cae2f..a47c283 100644 --- a/server/db.py +++ b/server/db.py @@ -1,28 +1,49 @@ """Database engine and session setup. -The connection string comes from the DATABASE_URL environment variable, e.g. - postgresql+psycopg://wpsuite:secret@db-host:5432/wpsuite +Connection precedence: + 1. POSTGRES_USER + POSTGRES_PASSWORD + POSTGRES_DB (preferred) — the URL is + built with SQLAlchemy's URL.create(), which encodes the password for you, + so passwords with special characters (@ ! # : / …) need NO manual escaping. + Host/port default to POSTGRES_HOST=db / POSTGRES_PORT=5432. + 2. DATABASE_URL — a full SQLAlchemy URL, if you'd rather supply one directly + (you must URL-encode any special characters in the password yourself). + 3. Neither set → a local SQLite file, so the API runs anywhere without Postgres. -If unset, it falls back to a local SQLite file so the API can be run and tested -on any machine without Postgres. The schema is identical either way (SQLAlchemy -handles the dialect differences). +The schema is identical either way (SQLAlchemy handles dialect differences). """ import os -from sqlalchemy import create_engine +from sqlalchemy import create_engine, URL from sqlalchemy.orm import sessionmaker, DeclarativeBase -# Load a local .env if present (dev convenience). In production the DATABASE_URL -# normally comes from the systemd unit's Environment / EnvironmentFile instead. +# Load a local .env if present (dev convenience). try: from dotenv import load_dotenv load_dotenv() except Exception: pass -DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./wpsuite.db") -# SQLite needs this flag to be used from FastAPI's threadpool; Postgres ignores it. -connect_args = {"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {} +def _resolve_url(): + user = os.getenv("POSTGRES_USER") + pw = os.getenv("POSTGRES_PASSWORD") + dbname = os.getenv("POSTGRES_DB") + if user and pw and dbname: + # Build from components — password is encoded automatically. + return URL.create( + "postgresql+psycopg", + username=user, password=pw, + host=os.getenv("POSTGRES_HOST", "db"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=dbname, + ) + return os.getenv("DATABASE_URL") or "sqlite:///./wpsuite.db" + + +DATABASE_URL = _resolve_url() + +# SQLite needs this flag from FastAPI's threadpool; Postgres ignores it. +_is_sqlite = isinstance(DATABASE_URL, str) and DATABASE_URL.startswith("sqlite") +connect_args = {"check_same_thread": False} if _is_sqlite else {} engine = create_engine(DATABASE_URL, connect_args=connect_args, pool_pre_ping=True, future=True) SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)