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