benvin 39b2195f5a
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/docker Pipeline was successful
ci/woodpecker/push/pre-commit Pipeline was successful
Merge pull request 'Add valkey/redis driver with operator-secret auto-configuration' (#3) from benvin/valkey-support into main
Reviewed-on: #3
2026-08-23 17:02:13 +10:00

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 <cluster>-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:

initContainers:
  - name: wait-for-db
    image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/waitfordb:latest
    envFrom:
      - secretRef:
          name: mydb-app # CNPG <cluster>-app secret
    env:
      - name: WAITFORDB_TIMEOUT
        value: 5m

Wait for a valkey-operator instance:

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.

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

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:<tag> (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).

S
Description
A small Go tool, shipped as a container image, used as a Kubernetes initContainer to block an app from starting until its database is ready.
Readme 76 KiB
Languages
Go 95.1%
Makefile 4.1%
Dockerfile 0.8%