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) 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 is also supported. Configuration is entirely via environment variables.

Environment variables

Variable Default Description
WAITFORDB_DRIVER postgres Database driver: postgres or mysql.
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.
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*. The PG* (libpq) fallback applies to the postgres driver only, so the tool is a drop-in replacement anywhere those variables are already set.

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%