Vikunja 2.7.0 cannot connect to Postgres if the DB password contains a literal `@` (pgx v5.11 DSN parsing change)

After upgrading from 2.6.0 to 2.7.0 (docker, vikunja/vikunja:latest, image pulled 2026-10-02), vikunja crash-loops on startup with:

level=INFO  msg="Running migrations…"
[xorm] [info]  PING DATABASE pgx
level=ERROR msg="Could not connect to db: failed to connect to `user=vikunja database=vikunja`: hostname resolving error: lookup <password-tail>@db: no such host"

Rolling back the image to 2.6.0 (which used lib/pq) connects fine, so it’s not a network/config issue. The container env is correct: VIKUNJA_DATABASE_HOST: db.

What’s happening

2.7.0 migrated from lib/pq to pgx v5.11.0. pgx 5.11 replaced url.Parse with its own hand-rolled URI parser in pgconn/parse_url.go, which follows libpq semantics: the first @
terminates the userinfo (it uses strings.IndexAny(p, "@/") instead of the last @, as net/url does).

Vikunja builds the DSN in pkg/db/db.go → getPostgreSQLConnectionString() like this:

connStr = fmt.Sprintf("postgres://%s:%s@%s:%s/%s%ssslmode=...",
    url.PathEscape(dbUser), url.PathEscape(dbPasswd), host, port, dbName, dbParam, dbSslMode, ...)

url.PathEscape intentionally does not escape @ (or :) — it’s legal in a path segment. So with a password like aa…X@-Dx! the DSN becomes

postgres://vikunja:aa…X@-Dx!@db:5432/vikunja?...

pgx 5.11 splits at the first @ (inside the password), treats …X@-Dx!@db as the netloc, and DNS-resolves a hostname that is literally part of the password.

Reproduction

Any 2.7.0 instance with a Postgres password containing a literal @ (and no vikunja restart needed to reproduce — a one-off container with the same env against a bogus
VIKUNJA_DATABASE_HOST shows the same mangled lookup). YAML quoting in the compose file doesn’t help — the quotes are consumed before the value reaches the container, and manual
percent-encoding doesn’t either (PathEscape re-escapes % → %25, so pgx decodes the password back to the literal %40).

Secondary issue: the DNS lookup error leaks a fragment of the password into the logs, and sanitizePostgresConnectionError() doesn’t catch it because the password never appears in
user:pass@ form in the message. Worth scrubbing before the fix ships.

Workaround: rotate the DB password to one without @ (ALTER USER in the live DB — the POSTGRES_PASSWORD env only applies at first initdb). Then 2.7.0 connects fine; I’ve verified
that.

Possible fixes (in order of preference, from my perspective):

  1. Build a keyword/value DSN (host=… user=… password=…) — libpq quoting rules are unambiguous, unlike URI userinfo.
  2. Keep the URI but percent-encode the userinfo correctly — e.g. escape @ and : on top of PathEscape, or construct it via url.UserPassword(user, pass) and build the URL, which
    percent-encodes userinfo properly.
  3. Pin pgx to < 5.11 until 2. is implemented.

Should be fixed in fix(db): percent-encode postgres credentials in the connection URL by tink-bot · Pull Request #4092 · go-vikunja/vikunja · GitHub, please check with the next unstable build (should be ready for deployment in ~30min, also on try).