# Docker

> Run malisper/pgrust or a local image build with the official postgres-shaped contract, connect with psql, and confirm first-boot init via entrypoint.

- Repository: malisper/pgrust
- GitHub: https://github.com/malisper/pgrust
- Human docs: https://grok-wiki.com/public/docs/malisper-pgrust-7e0c8e82bc21
- Complete Markdown: https://grok-wiki.com/public/docs/malisper-pgrust-7e0c8e82bc21/llms-full.txt

## Source Files

- `README.md`
- `Dockerfile`
- `docker/entrypoint.sh`
- `docker/README.md`
- `.dockerignore`

---

---
title: "Docker"
description: "Run malisper/pgrust or a local image build with the official postgres-shaped contract, connect with psql, and confirm first-boot init via entrypoint."
---

The root `Dockerfile` and `docker/entrypoint.sh` ship a **Postgres-shaped container**: same env vars, `PGDATA`, port **5432**, `gosu` step-down, and `/docker-entrypoint-initdb.d` first-boot scripts as the official `postgres` image, with the user-facing server and catalog bootstrap both provided by the pgrust binary at `/usr/local/bin/pgrust-postgres`. The only retained C tool is PostgreSQL 18 `psql` (init scripts and healthchecks). No C `postgres` backend and no C `initdb` are present in the final image.

## Image contract

| Surface | Value |
| --- | --- |
| Published image | `malisper/pgrust:v0.1` (pinned); `malisper/pgrust:latest` tracks the same release |
| Local tag (from clone) | `docker build -t pgrust .` |
| Binary (server + `--initdb`) | `/usr/local/bin/pgrust-postgres` |
| Client | `/usr/lib/postgresql/18/bin/psql` on `PATH` |
| Share / bootstrap templates | `/usr/share/postgresql/18` (`PG_SHAREDIR`) |
| Data volume | `/var/lib/postgresql/data` (`PGDATA`) |
| Socket dir | `/var/run/postgresql` |
| User | `postgres` uid/gid **999** |
| Port | **5432** (`EXPOSE`) |
| Stop signal | `SIGINT` (fast shutdown) |
| Entrypoint / default CMD | `docker-entrypoint.sh` / `["postgres"]` |

Official-compatible environment variables: `POSTGRES_PASSWORD`, `POSTGRES_USER`, `POSTGRES_DB`, `POSTGRES_INITDB_ARGS`, `POSTGRES_HOST_AUTH_METHOD`, `PGDATA`, plus Docker secrets via `*_FILE` (`file_env`).

## Quick run

<Tabs>
  <Tab title="Published image">
```bash
docker run -d --name pgrust \
  -e POSTGRES_PASSWORD=secret \
  -p 5432:5432 \
  malisper/pgrust:v0.1

until docker exec -e PGPASSWORD=secret pgrust \
  psql -h 127.0.0.1 -U postgres -c '\q' >/dev/null 2>&1; do
  sleep 1
done

docker exec -it -e PGPASSWORD=secret pgrust \
  psql -h 127.0.0.1 -U postgres
```
  </Tab>
  <Tab title="Local image build">
```bash
docker build -t pgrust .
docker run --rm \
  -e POSTGRES_PASSWORD=secret \
  -p 5432:5432 \
  pgrust
```
  </Tab>
</Tabs>

Connect from the host with any Postgres 18 client:

```bash
psql postgres://postgres:secret@localhost:5432/postgres \
  -c "select version(), 1 + 1 as two"
```

Or use the image-bundled `psql` via `docker exec` (as in the README one-liner).

<Warning>
pgrust is not production-ready. Prefer the pinned tag `malisper/pgrust:v0.1` for reproducible experiments.
</Warning>

## Image layout (three stages)

```text
Dockerfile stages
├── rustbuild  rust:1-bookworm
│   cargo build --release --locked --bin postgres
│   PGRUST_PGSHAREDIR → /usr/share/postgresql/18 (baked for tz)
│   → /opt/pgrust-postgres
├── pgtools    debian:bookworm-slim + PGDG
│   postgresql-18 (share tree only) + postgresql-client-18 (psql)
│   → /opt/pgbin/psql, share files, /opt/runlibs
└── final      debian:bookworm-slim
    pgrust-postgres + psql + share + gosu + runtime libs
    timezone → symlink to /usr/share/zoneinfo
```

| Stage | Role |
| --- | --- |
| `rustbuild` | Builds the pgrust `postgres` binary. Vendored `nodetags.h` means no PostgreSQL source tree is required—only Rust and build deps (`libicu-dev`, OpenSSL, LDAP, PAM). |
| `pgtools` | Installs PGDG `postgresql-18` / `postgresql-client-18`, stages **psql** and non-glibc shared libs, verifies share bootstrap files (`postgres.bki`, `system_*.sql`, samples, …). C server binaries and loadable modules are **not** copied forward. |
| `final` | Runtime: uid 999 `postgres`, `libicu`/`libxml2`/`tzdata`/`locales`/`gosu`, binary + share + `psql`, entrypoint, `VOLUME`, `EXPOSE 5432`. |

Build-time share path for the binary:

```dockerfile
ENV PGRUST_PGSHAREDIR=${PG_SHAREDIR}   # default /usr/share/postgresql/18
```

Debian packages use system tzdata (no `timezone/` under share). The final stage links:

```text
/usr/share/postgresql/18/timezone → /usr/share/zoneinfo
```

`.dockerignore` keeps the build context lean (drops `/target`, `/docs`, `/scripts`, `/audits`, …) while preserving crate sources the Cargo build needs.

## First-boot lifecycle

On `CMD`/`args` that run the server (`postgres` or leading `-` flags rewritten to `postgres …`), the entrypoint initializes only when `$PGDATA/PG_VERSION` is missing.

```mermaid
flowchart TD
  subgraph entry["docker-entrypoint.sh"]
    A["docker_setup_env"] --> B["docker_create_db_directories"]
    B --> C{"uid == 0?"}
    C -->|yes| D["gosu postgres re-exec"]
    C -->|no| E{"PG_VERSION exists?"}
    D --> E
    E -->|yes| Skip["Skip init message"]
    E -->|no| F["docker_verify_minimum_env"]
    F --> G["docker_init_database_dir\npgrust-postgres --initdb -L PG_SHAREDIR"]
    G --> H["pg_setup_hba_conf"]
    H --> I["docker_temp_server_start\nsocket-only pgrust"]
    I --> J["docker_setup_password ALTER ROLE"]
    J --> K["docker_setup_db CREATE DATABASE"]
    K --> L["docker_process_init_files\n/docker-entrypoint-initdb.d"]
    L --> M["docker_temp_server_stop SIGINT"]
    M --> N["Init complete message"]
  end
  Skip --> Run
  N --> Run["exec pgrust-postgres -D PGDATA + PGRUST_SERVER_OPTS + listen_addresses=*"]
```

Success signal on empty data dir (stdout):

```text
PostgreSQL init process complete; ready for start up.
```

Existing data dir:

```text
PostgreSQL Database directory appears to contain a database; Skipping initialization
```

### What diverges from docker-library/postgres

| Concern | Official image | pgrust image |
| --- | --- | --- |
| Catalog bootstrap | C `initdb` | `pgrust-postgres --initdb` (no `--pwfile`; password via `ALTER ROLE` after temp server start) |
| Temp server during init | `pg_ctl` | Background `pgrust-postgres` + poll with `psql` on unix socket |
| Final server argv | `postgres` on PATH | Rewrite to `/usr/local/bin/pgrust-postgres` + injected GUCs |
| Share location | Relative to bindir | Explicit `-L "$PG_SHAREDIR"` (binary lives under `/usr/local/bin`) |
| Host auth default | Password encryption discovery via `postgres -C` | Default **`trust`** for working TCP (password auth seam unported) |
| nss_wrapper | Used for username | Not needed; `--username` only |

## Injected runtime options

Applied to both the temporary init server and the final process (user CMD flags still append afterward):

| Setting | Value | Purpose |
| --- | --- | --- |
| `ulimit -s` | `65520` | Large C stack for pgrust frames |
| `RUST_MIN_STACK` | `33554432` (default if unset) | Rust thread stack |
| `io_method` | `sync` | Worker I/O method is unported |
| `max_stack_depth` | `60000` | Matches stack allowance |
| `unix_socket_directories` | `/var/run/postgresql` | Aligns with Debian `psql` default socket |
| `listen_addresses` (final only) | `*` | TCP on container networks |
| `listen_addresses` (temp init only) | empty | Socket-only, same as upstream |

Final exec shape:

```bash
exec "$PGRUST_BIN" -D "$PGDATA" \
  -c io_method=sync \
  -c max_stack_depth=60000 \
  -c unix_socket_directories=/var/run/postgresql \
  -c listen_addresses='*' \
  "$@"
```

Non-`postgres` commands (for example interactive `psql`, or help/version flags that should exit) are `exec`'d without rewrite.

## Environment variables

Defaults and first-boot behavior:

| Variable | Default | Notes |
| --- | --- | --- |
| `POSTGRES_PASSWORD` | (empty) | Required unless `POSTGRES_HOST_AUTH_METHOD=trust`. Stored in catalog via `ALTER ROLE` after init. Supports `POSTGRES_PASSWORD_FILE`. |
| `POSTGRES_USER` | `postgres` | Superuser for `--initdb --username`. Supports `*_FILE`. |
| `POSTGRES_DB` | same as `POSTGRES_USER` | Created after temp server starts if missing. Supports `*_FILE`. |
| `POSTGRES_INITDB_ARGS` | empty | Extra args to `--initdb`. **Space-separated** (`--opt value`), not `--opt=value`. Supports `*_FILE`. |
| `POSTGRES_INITDB_WALDIR` | unset | Creates dir and passes `--waldir` when set. |
| `POSTGRES_HOST_AUTH_METHOD` | **`trust`** (if unset at HBA setup) | Written as `host all all all <method>` in `pg_hba.conf`. |
| `PGDATA` | `/var/lib/postgresql/data` | Init skipped when `$PGDATA/PG_VERSION` is non-empty. |
| `PG_SHAREDIR` | `/usr/share/postgresql/18` | Passed as `-L` to `--initdb`. |
| `PGRUST_BIN` | `/usr/local/bin/pgrust-postgres` | Override path to the server binary. |
| `RUST_MIN_STACK` | `33554432` | Exported by the entrypoint. |
| `PGPORT` | `5432` | Temp server port during first-boot setup. |
| `PG_MAJOR` | `18` | Image major; used for old-data-layout checks. |

### Supported vs rejected initdb args

pgrust `--initdb` accepts a subset (for example `-D`/`--pgdata`, `-U`/`--username`, `-L`, `-E`/`--encoding`, locale flags, `--wal-segsize`). Unsupported flags in `POSTGRES_INITDB_ARGS` (for example `-A`/`--auth`, `--data-checksums`) **error** at first boot.

## Init scripts

Directory: `/docker-entrypoint-initdb.d` (created empty in the image). Processed only on first boot, after password and database setup, against a socket-only temp server.

| Pattern | Action |
| --- | --- |
| `*.sh` (executable) | Run |
| `*.sh` (not executable) | Source |
| `*.sql` | `psql -f` |
| `*.sql.gz` / `*.sql.xz` / `*.sql.zst` | Decompress pipe into `psql` |
| other | Ignored (logged) |

`psql` runs with `ON_ERROR_STOP=1`, `--username "$POSTGRES_USER"`, optional `--dbname "$POSTGRES_DB"`, and `PGHOST`/`PGHOSTADDR` cleared so the unix socket is used.

Example bind-mount:

```bash
docker run --rm \
  -e POSTGRES_PASSWORD=secret \
  -v "$PWD/initdb.d:/docker-entrypoint-initdb.d:ro" \
  -p 5432:5432 \
  pgrust
```

## Authentication (TCP)

<Warning>
Password authentication over TCP is not usable yet. The `fetch_role_password` seam is unported. A `host … scram-sha-256` or `md5` line causes `FATAL` **"seam not installed: fetch_role_password"** even with the correct password. Local/`trust` paths work (init scripts and unix-socket checks succeed).
</Warning>

Consequences for the Docker contract:

1. Default host method is **`trust`** so TCP from other containers or the host works without seam errors.
2. The superuser password is still written into the catalog when `POSTGRES_PASSWORD` is set; it is simply not enforced on `host` lines until password auth is ported.
3. Setting `POSTGRES_HOST_AUTH_METHOD` to a password method is honored in `pg_hba.conf` but the entrypoint prints a large warning that TCP logins will fail.
4. Empty `POSTGRES_PASSWORD` without `POSTGRES_HOST_AUTH_METHOD=trust` still errors at first boot (same guard as the official image).

For local experiments, prefer:

```bash
-e POSTGRES_PASSWORD=secret
# leave POSTGRES_HOST_AUTH_METHOD unset → trust host line
```

## Verify readiness

1. **Wait for first-boot completion** — look for `PostgreSQL init process complete; ready for start up.` in container logs, or poll with `psql` as in the quick-run loop.
2. **Query** — `select version(), 1 + 1 as two` should return a Postgres 18-shaped version string and `2`.
3. **Skip path** — restart the same named volume; logs should show *Skipping initialization* and the prior data should remain.

## Common failures

| Symptom | Cause | Mitigation |
| --- | --- | --- |
| Init errors: password not specified | Empty `POSTGRES_PASSWORD` and auth method not `trust` | Set `-e POSTGRES_PASSWORD=…` or `POSTGRES_HOST_AUTH_METHOD=trust` |
| TCP `FATAL` seam / `fetch_role_password` | Password host auth method | Use default `trust` for host lines |
| Initdb option error | Unsupported `POSTGRES_INITDB_ARGS` | Stick to supported flags; see [initdb options reference](/initdb-reference) |
| Temp server timeout | Share/tz path wrong or binary crash during bootstrap | Confirm image has `/usr/share/postgresql/18` and `timezone` symlink; check logs |
| Stack / boot refusal without Docker entrypoint | Missing stack/`io_method` when running the binary bare | Prefer the image entrypoint, or apply the same `ulimit`/`RUST_MIN_STACK`/`io_method=sync` settings as [Run the server](/run-server) |
| Old data layout error (18+ mount layout) | Existing volume with unexpected `PG_VERSION` paths | Match official 18+ volume guidance; see entrypoint `docker_error_old_databases` |

## Related pages

<CardGroup>
  <Card title="Docker environment reference" href="/docker-reference">
    Full env surface: POSTGRES_*, PGDATA, init scripts, injected server opts.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    Non-Docker local build, --initdb, and connect path.
  </Card>
  <Card title="Run the server" href="/run-server">
    Postmaster flags, io_method=sync, stack settings, readiness.
  </Card>
  <Card title="Initialize a cluster" href="/init-cluster">
    postgres --initdb phases and success signals outside Docker.
  </Card>
  <Card title="Compatibility and status" href="/compatibility">
    Non-production status, extension boundaries, regression-oracle model.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    io_method, stack, share/tz, auth, and client failures.
  </Card>
</CardGroup>
