PostgreSQL compose file: POSTGRES_PASSWORD is read from docker/.env, and pasted unescaped into a URL #582

Closed
opened 2026-10-01 10:47:49 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the PostgreSQL compose file against the install instructions and parsing database URLs with django-environ.

Related to #219.

Checked again on 2026-10-01 against main at 0a5cad587: still present.

${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} is Compose interpolation. Compose resolves it from the shell and from the .env in the project directory, which with -f docker/compose.postgres.yml and no --project-directory is docker/; the env_file: ../.env lines only pass variables into containers and play no part in interpolation. The documented install fills .env at the repository root and runs the command from there, so the PostgreSQL install refuses to start with a message pointing at the file the operator already filled. Separately, the password is pasted raw into POSTULO_DATABASE_URL, so some ordinary password characters break the URL.

  • Where: docker/compose.postgres.yml:28 and :56 (postgres://postulo:${POSTGRES_PASSWORD:?…}@db:5432/postulo), :91 (the db service); the wiki's Installing-Postulo.md:57-61 ("Set POSTGRES_PASSWORD in .env first"); .env.example, which has no POSTGRES_PASSWORD line at all.
  • What happens:
    1. An operator copies .env.example to .env at the checkout root, adds POSTGRES_PASSWORD, and runs docker compose -f docker/compose.postgres.yml up -d from the checkout. Compose stops with "required variable POSTGRES_PASSWORD is missing a value: set POSTGRES_PASSWORD in .env". The workaround (--env-file .env, or exporting the variable) is written nowhere.
    2. With the variable supplied, a password containing / or ? (common in openssl rand -base64 output) makes django-environ fail at start-up with ValueError: Port could not be cast to integer value, and a %xx sequence is percent-decoded, so Postulo connects with a different password from the one the db service was given. (@, : and # happen to parse correctly.)
  • Why it matters: the PostgreSQL install path, which the wiki recommends to anybody already running PostgreSQL, fails on its first command, and a randomly generated password can fail it again later.
  • Fix: stop interpolating the secret. Give the db service env_file: ../.env so POSTGRES_PASSWORD reaches PostgreSQL from the file, and let Postulo assemble its database settings from separate variables (for example POSTULO_DATABASE_PASSWORD read beside POSTULO_DATABASE_URL without a password), so no character needs escaping. Failing that, add --env-file .env to every documented compose.postgres.yml command and say that the password must not need URL-encoding. Add POSTGRES_PASSWORD to .env.example, commented, with that note. Nothing in CI or scripts/check-image.sh ever runs this compose file; add a CI step that runs docker compose -f docker/compose.postgres.yml config from the repository root with a root .env.

Done when

  • A CI step (or a check in scripts/check-image.sh) runs docker compose -f docker/compose.postgres.yml config from the repository root with only a root .env holding POSTGRES_PASSWORD, and succeeds; it fails today.
  • A test in tests/test_settings.py (or wherever database settings are tested) configures a password containing /, ? and %41 and asserts that the resulting DATABASES["default"]["PASSWORD"] is exactly that string.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the PostgreSQL compose file against the install instructions and parsing database URLs with `django-environ`. Related to #219. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. `${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}` is Compose interpolation. Compose resolves it from the shell and from the `.env` in the project directory, which with `-f docker/compose.postgres.yml` and no `--project-directory` is `docker/`; the `env_file: ../.env` lines only pass variables into containers and play no part in interpolation. The documented install fills `.env` at the repository root and runs the command from there, so the PostgreSQL install refuses to start with a message pointing at the file the operator already filled. Separately, the password is pasted raw into `POSTULO_DATABASE_URL`, so some ordinary password characters break the URL. - **Where:** `docker/compose.postgres.yml:28` and `:56` (`postgres://postulo:${POSTGRES_PASSWORD:?…}@db:5432/postulo`), `:91` (the `db` service); the wiki's `Installing-Postulo.md:57-61` ("Set `POSTGRES_PASSWORD` in `.env` first"); `.env.example`, which has no `POSTGRES_PASSWORD` line at all. - **What happens:** 1. An operator copies `.env.example` to `.env` at the checkout root, adds `POSTGRES_PASSWORD`, and runs `docker compose -f docker/compose.postgres.yml up -d` from the checkout. Compose stops with "required variable POSTGRES_PASSWORD is missing a value: set POSTGRES_PASSWORD in .env". The workaround (`--env-file .env`, or exporting the variable) is written nowhere. 2. With the variable supplied, a password containing `/` or `?` (common in `openssl rand -base64` output) makes `django-environ` fail at start-up with `ValueError: Port could not be cast to integer value`, and a `%xx` sequence is percent-decoded, so Postulo connects with a different password from the one the `db` service was given. (`@`, `:` and `#` happen to parse correctly.) - **Why it matters:** the PostgreSQL install path, which the wiki recommends to anybody already running PostgreSQL, fails on its first command, and a randomly generated password can fail it again later. - **Fix:** stop interpolating the secret. Give the `db` service `env_file: ../.env` so `POSTGRES_PASSWORD` reaches PostgreSQL from the file, and let Postulo assemble its database settings from separate variables (for example `POSTULO_DATABASE_PASSWORD` read beside `POSTULO_DATABASE_URL` without a password), so no character needs escaping. Failing that, add `--env-file .env` to every documented `compose.postgres.yml` command and say that the password must not need URL-encoding. Add `POSTGRES_PASSWORD` to `.env.example`, commented, with that note. Nothing in CI or `scripts/check-image.sh` ever runs this compose file; add a CI step that runs `docker compose -f docker/compose.postgres.yml config` from the repository root with a root `.env`. ## Done when - A CI step (or a check in `scripts/check-image.sh`) runs `docker compose -f docker/compose.postgres.yml config` from the repository root with only a root `.env` holding `POSTGRES_PASSWORD`, and succeeds; it fails today. - A test in `tests/test_settings.py` (or wherever database settings are tested) configures a password containing `/`, `?` and `%41` and asserts that the resulting `DATABASES["default"]["PASSWORD"]` is exactly that string.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:47:49 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#582
No description provided.