From 0cabcf743888ba822576e0a2bea616a50411dc05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sun, 14 Jun 2026 15:58:48 +0200 Subject: [PATCH] fix: separate DB_PASSWORD from DATABASE_URL to handle special chars Coolify overrides container_name, so the DB service is only reachable via its compose service name ("db"), not "worldcup_db". Also, passwords containing URL-special characters (#, ], =) break postgres URL parsing because the driver uses new URL() internally. - docker-compose.yml: DATABASE_URL now uses "db" hostname with no embedded password; DB_PASSWORD is passed as a separate env var - lib/db/index.ts: when DB_PASSWORD env var is set it is passed as a postgres driver option, bypassing URL parsing entirely - .env.example: documents production vs local dev env var usage; removes DATABASE_URL from the Coolify section (not needed there) Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 12 ++++++++++-- docker-compose.yml | 5 ++--- lib/db/index.ts | 7 ++++++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 8334b05..7558b7b 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,16 @@ -# Database +# ── Production (Coolify) ──────────────────────────────────────────────────── +# DB_PASSWORD is passed separately so special characters never need URL-encoding. +# DATABASE_URL is constructed inside docker-compose.yml and does NOT need to be +# set in Coolify — only DB_PASSWORD is required. DB_PASSWORD=changeme -DATABASE_URL=postgres://wc:changeme@worldcup_db:5432/worldcup # Traefik (set TRAEFIK_ENABLED=true when deploying behind Traefik) TRAEFIK_ENABLED=false TRAEFIK_HOST=worldcup.example.com NETWORK_NAME=traefik-network + +# ── Local development ──────────────────────────────────────────────────────── +# Set DATABASE_URL when running pnpm dev or pnpm sync on the host directly. +# The password can be plain-text here since it goes through the postgres driver, +# not URL parsing, when DB_PASSWORD is unset. +# DATABASE_URL=postgres://wc:wc@localhost:5432/worldcup diff --git a/docker-compose.yml b/docker-compose.yml index 7c0718b..956651f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,12 +2,12 @@ services: app: build: . restart: unless-stopped - container_name: worldcup depends_on: db: condition: service_healthy environment: - DATABASE_URL: postgres://wc:${DB_PASSWORD}@worldcup_db:5432/worldcup + DATABASE_URL: postgres://wc@db:5432/worldcup + DB_PASSWORD: ${DB_PASSWORD} NODE_ENV: production labels: - "traefik.enable=${TRAEFIK_ENABLED:-false}" @@ -27,7 +27,6 @@ services: db: image: postgres:16-alpine restart: unless-stopped - container_name: worldcup_db environment: POSTGRES_DB: worldcup POSTGRES_USER: wc diff --git a/lib/db/index.ts b/lib/db/index.ts index e43b958..94370e6 100644 --- a/lib/db/index.ts +++ b/lib/db/index.ts @@ -4,7 +4,12 @@ import * as schema from './schema' const connectionString = process.env.DATABASE_URL ?? 'postgres://wc:wc@localhost:5432/worldcup' -const client = postgres(connectionString, { max: 10 }) +// DB_PASSWORD is passed separately to avoid URL-encoding issues with special chars +// (e.g. #, ], = in passwords break URL parsing). Falls back to password in DATABASE_URL for local dev. +const client = postgres(connectionString, { + max: 10, + ...(process.env.DB_PASSWORD ? { password: process.env.DB_PASSWORD } : {}), +}) export const db = drizzle(client, { schema }) export * from './schema'