Skip to content

Install with Docker

BetweenRows ships as a single Docker image at ghcr.io/getbetweenrows/betweenrows. The image contains both the data plane (pgwire proxy on port 5434) and the management plane (admin UI and REST API on port 5435) in one binary.

Minimum invocation

sh
docker run -d \
  -e BR_ADMIN_USER=admin \
  -e BR_ADMIN_PASSWORD=changeme \
  -p 5434:5434 -p 5435:5435 \
  -v betweenrows_data:/data \
  ghcr.io/getbetweenrows/betweenrows:0.16.2

TIP

Always pin the image tag to a specific version like :0.16.2. :latest will move you across release boundaries on every container restart — upgrade deliberately instead. See Upgrading for how to change versions safely.

Once the container is up, open http://localhost:5435 and log in with your admin credentials:

BetweenRows admin login screen served from the Docker container on port 5435

What the flags do

FlagPurpose
-e BR_ADMIN_USER=adminUsername for the initial admin account. Only used on first boot; ignored on subsequent boots. Change it before the first run if you prefer a different name — the username cannot be changed after creation.
-e BR_ADMIN_PASSWORD=changemePassword for the initial admin account. Required on first boot. Change it before running in any shared environment. You can change it later through the admin UI.
-p 5434:5434SQL proxy port. Connect your PostgreSQL clients here.
-p 5435:5435Admin UI and REST API port.
-v betweenrows_data:/dataPersistent volume. Stores the SQLite admin database, auto-generated encryption key, and JWT secret. Do not omit — without it, all data and keys are lost when the container restarts.

Production-grade invocation

For anything beyond a local demo, set the encryption key and JWT secret explicitly so they survive volume reset and can be rotated independently:

sh
docker run -d \
  --name betweenrows \
  --restart unless-stopped \
  -e BR_ADMIN_USER=admin \
  -e BR_ADMIN_PASSWORD="$(openssl rand -base64 24)" \
  -e BR_ENCRYPTION_KEY="$(openssl rand -hex 32)" \
  -e BR_ADMIN_JWT_SECRET="$(openssl rand -base64 32)" \
  -e BR_PROXY_BIND_ADDR=0.0.0.0:5434 \
  -e BR_ADMIN_BIND_ADDR=0.0.0.0:5435 \
  -e RUST_LOG=info \
  -p 5434:5434 -p 5435:5435 \
  -v /srv/betweenrows/data:/data \
  ghcr.io/getbetweenrows/betweenrows:0.16.2
  • BR_ENCRYPTION_KEY must be a 64-character hex string (32 bytes → AES-256-GCM key). If you change this value after secrets have been stored, existing secrets become unreadable.
  • BR_ADMIN_JWT_SECRET can be any non-empty string, but should be high-entropy. Tokens signed with the old value are rejected after rotation — all admins will need to re-authenticate.
  • Save both values somewhere secure before the first boot. If you lose them, you will need to wipe /data and re-create everything.

See the Configuration reference for the full list of environment variables.

Persistent data

The /data volume contains:

  • proxy_admin.db — the SQLite admin database (users, policies, datasources, audit logs, attribute definitions).
  • .betweenrows/encryption_key — the auto-generated AES-256-GCM key, if BR_ENCRYPTION_KEY was not set explicitly.
  • .betweenrows/jwt_secret — the auto-generated JWT signing secret, if BR_ADMIN_JWT_SECRET was not set explicitly.

TIP

Back up the whole /data directory regularly. See the Backups page for the recommended approach.

Verifying the install

  1. Check the container is running.

    sh
    docker ps --filter name=betweenrows
  2. Tail the logs to confirm startup.

    sh
    docker logs -f betweenrows

    Look for lines indicating the admin and proxy bind addresses and that migrations completed.

  3. Open the admin UI.

    Visit http://localhost:5435 and log in with your admin credentials.

  4. Run the Quickstart walkthrough.

    Follow the Quickstart from step 2 onward to add a data source, create a user, define a policy, and verify it with psql.

Creating users from the shell

The proxy binary has a built-in user create subcommand for bootstrap — creating the first user, scripted provisioning, or rescuing a locked-out admin when the UI is unreachable. For everything else, use the admin UI.

sh
docker exec -it betweenrows proxy user create --username alice --password secret
FlagDescription
--username <name>Required. Must match [a-zA-Z0-9_.-], 3–50 characters, start with a letter.
--password <password>Required. Stored as an Argon2id hash.
--adminOptional. Creates the user with is_admin: true. Use this to create a rescue admin when you're locked out of the UI.

Password complexity is not enforced from the CLI

The shell path stores whatever password you pass without running the admin API's complexity check. A user created this way with a weak password will later fail to update their own password through the admin UI, which enforces complexity on edit.

Shell history caveat. The password appears in ~/.bash_history / ~/.zsh_history if entered directly. Prefer an env var or a secrets file:

sh
# From an env var
docker exec -it betweenrows proxy user create --username alice --password "$ALICE_PASSWORD"

# Generate a random password and display it once
PASSWORD=$(openssl rand -base64 24)
docker exec -it betweenrows proxy user create --username alice --password "$PASSWORD"
echo "Alice's password: $PASSWORD"

Rescue admin recipe. If you've forgotten the admin password and have no other admin accounts:

sh
docker exec -it betweenrows proxy user create \
  --username rescue \
  --password "$(openssl rand -base64 24)" \
  --admin

Log in as rescue, then reset the original admin password through the UI (or delete rescue after).

INFO

A forgot/reset password feature is on the roadmap. Until then, the shell rescue path is the only way to recover from a lost admin password.

Automation beyond the shell

Everything the admin UI does is backed by a REST API at http://localhost:5435/api/v1/. A full OpenAPI reference is planned; until it ships, the fastest way to discover the request and response shapes for any admin action is to perform the action in the UI and inspect the request in your browser's network tab. Every UI button maps 1:1 to a single REST call under /api/v1/.

Docker Compose

For reproducible local setups, a compose.yaml snippet:

yaml
services:
  betweenrows:
    image: ghcr.io/getbetweenrows/betweenrows:0.16.2
    container_name: betweenrows
    restart: unless-stopped
    ports:
      - "5434:5434"
      - "5435:5435"
    environment:
      BR_ADMIN_USER: admin
      BR_ADMIN_PASSWORD: ${BR_ADMIN_PASSWORD:?required}
      BR_ENCRYPTION_KEY: ${BR_ENCRYPTION_KEY:?required}
      BR_ADMIN_JWT_SECRET: ${BR_ADMIN_JWT_SECRET:?required}
      BR_PROXY_BIND_ADDR: 0.0.0.0:5434
      BR_ADMIN_BIND_ADDR: 0.0.0.0:5435
      RUST_LOG: info
    volumes:
      - betweenrows_data:/data

volumes:
  betweenrows_data:

Put the secrets in a .env file (not checked into git) and run docker compose up -d.

Behind a reverse proxy

In production, place the admin UI (port 5435) behind a reverse proxy (nginx, Caddy, Cloudflare Tunnel) for TLS, authentication, and rate limiting. The SQL proxy (port 5434) uses the PostgreSQL wire protocol and should be exposed directly or through a TCP load balancer — not an HTTP proxy.

Set BR_CORS_ALLOWED_ORIGINS if the admin UI is served from a different origin than the API:

sh
-e BR_CORS_ALLOWED_ORIGINS=https://admin.example.com

TIP

The admin API requires JWT authentication for all endpoints except /auth/login. A reverse proxy adds defense-in-depth: TLS termination, rate limiting on the login endpoint, and IP allowlisting if your admin team is on a known network.

Upgrading

See Upgrading — the short version is change the image tag and restart, back up /data first, read the changelog between your current and target versions before pulling.

Next steps