# waitfordb A tiny, stateless Go tool that blocks until a database is ready, then exits `0`. It is designed to run as a Kubernetes **initContainer** so an app never starts before its database can serve queries, without hand-writing a `psql`-in-a-shell probe in every workload. Readiness means a trivial liveness query (`SELECT 1`, or `PING` for valkey/redis) succeeds under the given credentials and database — proving the **server is up, auth works, and the target database/role exist**. On timeout it exits non-zero so the pod fails fast and Kubernetes restarts it. `postgres` is the default driver; `mysql` and `valkey` (alias: `redis`) are also supported. Configuration is entirely via environment variables. ## Environment variables | Variable | Default | Description | | --------------------------- | ------------ | ------------------------------------------------------------------ | | `WAITFORDB_DRIVER` | `postgres` | Database driver: `postgres`, `mysql`, or `valkey` (alias: `redis`). | | `WAITFORDB_HOST` | `localhost` | Database host. Postgres falls back to `PGHOST`. | | `WAITFORDB_PORT` | `5432`/`3306`| Database port (driver default). Postgres falls back to `PGPORT`. | | `WAITFORDB_USER` | — | Username. Postgres falls back to `PGUSER`. Required (unless DSN). | | `WAITFORDB_PASSWORD` | — | Password. Postgres falls back to `PGPASSWORD`. | | `WAITFORDB_DATABASE` | — | Database name. Postgres falls back to `PGDATABASE`. Required (unless DSN). | | `WAITFORDB_SSLMODE` | driver default | Postgres sslmode (e.g. `disable`). Falls back to `PGSSLMODE`. | | `WAITFORDB_DSN` | — | Full driver-native connection string. Overrides all fields above. Valkey accepts `redis://`/`valkey://` URLs. | | `WAITFORDB_SECRET_FORMAT` | autodetected | Operator secret shape injected via `envFrom`: `cnpg` or `valkey`. See below. | | `WAITFORDB_TIMEOUT` | `0` (forever)| Total wait budget as a Go duration (e.g. `5m`). `0` waits forever. | | `WAITFORDB_INTERVAL` | `2s` | Gap between retries. | | `WAITFORDB_CONNECT_TIMEOUT` | `5s` | Per-attempt connect timeout, so a black-holed host cannot hang. | **Precedence for connection parameters:** `WAITFORDB_DSN` > `WAITFORDB_*` > `PG*` > operator-secret keys. The `PG*` (libpq) fallback applies to the **postgres** driver only, so the tool is a drop-in replacement anywhere those variables are already set. ## Operator secrets via `envFrom` waitfordb natively understands the secrets that CloudNativePG and valkey-operator generate, so an initContainer is just an `envFrom` plus (at most) a mode variable — no `secretKeyRef` plumbing per field. - **`cnpg`** — a CNPG `-app` secret (`host`/`port`/`dbname`/`user`/ `password` keys) fills the postgres connection parameters. - **`valkey`** — a valkey-operator generated secret where each key is a username and its value the password (e.g. the system-passwords secret with `_operator`/`_replication`). waitfordb authenticates as `_operator` when present, or as `WAITFORDB_USER` with the password taken from the key of the same name. The format is autodetected from the injected keys (CNPG keys → postgres, valkey/redis keys → valkey); set `WAITFORDB_SECRET_FORMAT` to pin it. Explicit `WAITFORDB_*` variables always win over secret keys. Wait for a CNPG cluster: ```yaml initContainers: - name: wait-for-db image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/waitfordb:latest envFrom: - secretRef: name: mydb-app # CNPG -app secret env: - name: WAITFORDB_TIMEOUT value: 5m ``` Wait for a valkey-operator instance: ```yaml initContainers: - name: wait-for-valkey image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/waitfordb:latest envFrom: - secretRef: name: internal-myapp-valkey-system-passwords env: - name: WAITFORDB_SECRET_FORMAT value: valkey # optional; autodetected from the _operator key - name: WAITFORDB_HOST value: myapp-valkey.myns.svc - name: WAITFORDB_TIMEOUT value: 5m ``` ### Exit codes | Code | Meaning | | ---- | ------------------------------------------- | | `0` | Database ready. | | `1` | Timed out (or interrupted by SIGTERM/SIGINT). | | `2` | Configuration error. | `waitfordb version` prints the version; `waitfordb help` prints usage. ### Example logs ``` 12:49:50 waitfordb v0.1.0: waiting for database (driver=postgres addr=db:5432 database=appdb user=appuser password=*** timeout=5m interval=2s connect_timeout=5s) 12:49:52 attempt 1: failed to connect to `user=appuser database=appdb`: dial tcp ... connection refused; retrying in 2s (elapsed 2s/5m) 12:49:56 database appdb ready after 4.2s (3 attempts) ``` The password is never printed — not in the startup line, not in any error line. ## Kubernetes initContainer example Drop this initContainer into a Deployment/StatefulSet. It reuses the same `Secret` the app consumes, so there is no second place to keep credentials. ```yaml initContainers: - name: wait-for-db image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/waitfordb:latest env: - name: WAITFORDB_HOST value: postgres-rw.default.svc.cluster.local - name: WAITFORDB_PORT value: "5432" - name: WAITFORDB_DATABASE value: appdb - name: WAITFORDB_SSLMODE value: disable - name: WAITFORDB_TIMEOUT value: 5m - name: WAITFORDB_USER valueFrom: secretKeyRef: name: app-db key: username - name: WAITFORDB_PASSWORD valueFrom: secretKeyRef: name: app-db key: password resources: requests: cpu: 10m memory: 16Mi limits: cpu: 100m memory: 64Mi securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true runAsNonRoot: true capabilities: drop: ["ALL"] ``` The libpq fallback means you can instead pass a single set of `PG*` variables (e.g. via `envFrom` a shared ConfigMap/Secret) with no `WAITFORDB_*` at all. ## Development ```sh make build # build ./dist/waitfordb make test # unit tests (fake driver + fake clock, deterministic) make test-integration # spins up postgres:16 via podman, asserts wait-then-succeed make docker # build the container image locally ``` Releases are cut by tagging: `make patch|minor|major` bumps the semver tag and pushes it, which triggers the Woodpecker pipeline to build and push `artifactapi.k8s.syd1.au.unkin.net/docker-internal/waitfordb:` (and `latest`). ## Adding a driver Implement `driver.Driver` (`Name()` + `Open(config.Config) (Pinger, error)`) and register it from an `init()`. Engines that plug into `database/sql` can reuse the shared `sqlPinger` (see `internal/driver/postgres.go` and `mysql.go`).