FFDB Docs

Install with Docker Compose

Copy a complete Docker Compose stack, start FFDB, and finish first-run setup in the portal.

Run the complete stack with Compose

This guide starts a complete FFDB single-host evaluation directly from a copyable Docker Compose file and a protected environment file.

It gives Docker users the actual service definition, required values, health checks, volumes, and lifecycle commands instead of making a local source tree or installer script the only visible path.

Use the direct Compose path for local evaluation or a trusted private network. Use the signed external-provider release profile before admitting internet traffic or production data.

Requirements for Install with Docker Compose

  • Prerequisite — Docker Engine 27 or newer with Compose v2, OpenSSL, sufficient host resources, and the documented loopback ports.
  • Prerequisite — For later production: operator-managed PostgreSQL, S3-compatible storage, email delivery, DNS, TLS, and off-host backups.
  • Required value — Independent PostgreSQL, MinIO, encryption, backup, HMAC, and bootstrap secrets plus a unique node UUID.
  • Required value — The exact local public origin, database URL, object-storage origins, and release-matched FFDB image version.

Before you start

This page is the direct Docker path. Copy compose.yaml and .env into an empty directory, generate the required infrastructure secrets, and run docker compose up. You do not clone the FFDB repository, install Rust or Node.js, or execute a source build.

The example is a complete single-host installation for local evaluation and private networks. It includes PostgreSQL, MinIO, Mailpit, the FFDB API and workers, and the compiled web gateway. The first browser user still completes owner and instance onboarding; no Stripe account or billing mode is selected in advance.

  • Docker Engine 27 or newer with Docker Compose v2.
  • At least 4 CPU cores, 8 GB of memory, and 20 GB of free disk for a useful evaluation.
  • OpenSSL for generating independent secrets and a browser on the same trusted machine.
  • Loopback ports 5173, 9000, 9001, 8025, and 1025 available.

1. Copy compose.yaml

Create an empty directory named ffdb, save this complete file as compose.yaml, and keep its seven named volumes. Both FFDB images are pinned to the server release version; use the same version for every FFDB image during an upgrade.

compose.yamlyaml
name: ffdb

services:
  postgres:
    image: postgres:17.5-alpine
    environment:
      - POSTGRES_DB
      - POSTGRES_USER
      - POSTGRES_PASSWORD
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
      interval: 3s
      timeout: 3s
      retries: 30
    volumes:
      - postgres-data:/var/lib/postgresql/data
    restart: unless-stopped

  minio:
    image: minio/minio:RELEASE.2025-04-22T22-12-26Z
    command: server /data --console-address ":9001"
    environment:
      - MINIO_ROOT_USER
      - MINIO_ROOT_PASSWORD
      - MINIO_API_CORS_ALLOW_ORIGIN
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 3s
      timeout: 3s
      retries: 30
    ports:
      - "127.0.0.1:9000:9000"
      - "127.0.0.1:9001:9001"
    volumes:
      - minio-data:/data
    restart: unless-stopped

  minio-bootstrap:
    image: minio/mc:RELEASE.2025-04-16T18-13-26Z
    depends_on:
      minio:
        condition: service_healthy
    environment:
      - MINIO_ROOT_USER
      - MINIO_ROOT_PASSWORD
    entrypoint: ["/bin/sh", "-ec"]
    command:
      - >-
        mc alias set local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" &&
        mc mb --ignore-existing local/ffdb &&
        mc anonymous set none local/ffdb
    restart: "no"

  mailpit:
    image: axllent/mailpit:v1.27.8
    environment:
      MP_DATABASE: /data/mailpit.db
    healthcheck:
      test: ["CMD", "/mailpit", "readyz"]
      interval: 3s
      timeout: 3s
      retries: 30
    ports:
      - "127.0.0.1:1025:1025"
      - "127.0.0.1:8025:8025"
    volumes:
      - mailpit-data:/data
    restart: unless-stopped

  volume-init:
    image: alpine:3.22.1
    user: "0:0"
    command:
      - /bin/sh
      - -ec
      - chown -R 10001:10001 /var/lib/ffdb/projects /var/lib/ffdb/backups /var/lib/ffdb/metrics /var/lib/ffdb/sync
    read_only: true
    volumes:
      - project-data:/var/lib/ffdb/projects
      - backup-data:/var/lib/ffdb/backups
      - metrics-data:/var/lib/ffdb/metrics
      - sync-data:/var/lib/ffdb/sync
    restart: "no"

  api:
    image: ghcr.io/forever-frameworks-llc/ffdb-runtime:0.3.0
    environment:
      - FFDB_ENVIRONMENT
      - FFDB_HTTP_BIND
      - FFDB_PUBLIC_BASE_URL
      - FFDB_CORS_ALLOWED_ORIGINS
      - FFDB_DATABASE_URL
      - FFDB_NODE_ID
      - FFDB_NODE_NAME
      - FFDB_DATABASE_ROOT
      - FFDB_BACKUP_ROOT
      - FFDB_METRICS_ROOT
      - FFDB_DATABASE_WORKER
      - FFDB_MASTER_KEY
      - FFDB_BACKUP_MASTER_KEY
      - FFDB_CURSOR_HMAC_KEY
      - FFDB_BOOTSTRAP_TOKEN
      - FFDB_S3_ENDPOINT
      - FFDB_S3_PUBLIC_ENDPOINT
      - FFDB_S3_REGION
      - FFDB_S3_BUCKET
      - FFDB_S3_ACCESS_KEY_ID
      - FFDB_S3_SECRET_ACCESS_KEY
      - FFDB_EMAIL_TRANSPORT
      - FFDB_SMTP_HOST
      - FFDB_SMTP_PORT
      - FFDB_EMAIL_FROM
      - RUST_LOG
    depends_on:
      postgres:
        condition: service_healthy
      minio-bootstrap:
        condition: service_completed_successfully
      mailpit:
        condition: service_healthy
      volume-init:
        condition: service_completed_successfully
    healthcheck:
      test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:8080/readyz"]
      interval: 5s
      timeout: 3s
      retries: 30
      start_period: 5s
    expose: ["8080"]
    read_only: true
    tmpfs:
      - /tmp:rw,noexec,nosuid,nodev,size=64m
    volumes:
      - project-data:/var/lib/ffdb/projects
      - backup-data:/var/lib/ffdb/backups
      - metrics-data:/var/lib/ffdb/metrics
    cap_drop: ["ALL"]
    security_opt: ["no-new-privileges:true"]
    restart: unless-stopped

  sync-worker:
    image: ghcr.io/forever-frameworks-llc/ffdb-runtime:0.3.0
    command: ["/usr/local/bin/ffdb-sync-worker"]
    environment:
      - FFDB_SYNC_STATE_DIR
      - FFDB_SYNC_MAINTENANCE_INTERVAL_SECONDS
      - FFDB_SYNC_STALE_TEMPORARY_SECONDS
      - RUST_LOG
    depends_on:
      api:
        condition: service_healthy
      volume-init:
        condition: service_completed_successfully
    read_only: true
    tmpfs:
      - /tmp:rw,noexec,nosuid,nodev,size=16m
    volumes:
      - sync-data:/var/lib/ffdb/sync
    cap_drop: ["ALL"]
    security_opt: ["no-new-privileges:true"]
    restart: unless-stopped

  gateway:
    image: ghcr.io/forever-frameworks-llc/ffdb-gateway:0.3.0
    environment:
      - FFDB_S3_PUBLIC_ORIGIN
    depends_on:
      api:
        condition: service_healthy
    ports:
      - "127.0.0.1:5173:8080"
    read_only: true
    tmpfs:
      - /tmp:rw,noexec,nosuid,nodev,size=16m,mode=1777
    cap_drop: ["ALL"]
    security_opt: ["no-new-privileges:true"]
    restart: unless-stopped

volumes:
  postgres-data:
  minio-data:
  mailpit-data:
  project-data:
  backup-data:
  metrics-data:
  sync-data:

2. Create .env and replace every placeholder

Save the template as .env beside compose.yaml, set its mode to 0600, and replace every replace-with value. PostgreSQL, MinIO, the database encryption key, backup key, cursor HMAC key, and bootstrap token must all be independent.

Use openssl rand -hex 24 for the PostgreSQL and MinIO passwords, openssl rand -base64 32 for each encryption key, and openssl rand -hex 32 for the HMAC key and bootstrap token. Copy the PostgreSQL password into FFDB_DATABASE_URL and the MinIO password into FFDB_S3_SECRET_ACCESS_KEY. Generate a unique UUID for FFDB_NODE_ID.

.envenv
POSTGRES_DB=ffdb
POSTGRES_USER=ffdb
POSTGRES_PASSWORD=replace-with-openssl-rand-hex-24

MINIO_ROOT_USER=ffdb-local
MINIO_ROOT_PASSWORD=replace-with-another-openssl-rand-hex-24
MINIO_API_CORS_ALLOW_ORIGIN=http://127.0.0.1:5173,http://localhost:5173

FFDB_ENVIRONMENT=development
FFDB_HTTP_BIND=0.0.0.0:8080
FFDB_PUBLIC_BASE_URL=http://127.0.0.1:5173
FFDB_CORS_ALLOWED_ORIGINS=http://127.0.0.1:5173,http://localhost:5173
FFDB_DATABASE_URL=postgres://ffdb:replace-with-the-postgres-password@postgres:5432/ffdb
FFDB_NODE_ID=019fc39c-ddbd-7d12-9849-e4ee35310132
FFDB_NODE_NAME=ffdb-single-host-01
FFDB_DATABASE_ROOT=/var/lib/ffdb/projects
FFDB_BACKUP_ROOT=/var/lib/ffdb/backups
FFDB_METRICS_ROOT=/var/lib/ffdb/metrics
FFDB_DATABASE_WORKER=/usr/local/bin/ffdb-database-worker
FFDB_MASTER_KEY=replace-with-openssl-rand-base64-32
FFDB_BACKUP_MASTER_KEY=replace-with-an-independent-base64-key
FFDB_CURSOR_HMAC_KEY=replace-with-openssl-rand-hex-32
FFDB_BOOTSTRAP_TOKEN=replace-with-an-independent-openssl-rand-hex-32

FFDB_S3_ENDPOINT=http://minio:9000
FFDB_S3_PUBLIC_ENDPOINT=http://127.0.0.1:9000
FFDB_S3_PUBLIC_ORIGIN=http://127.0.0.1:9000
FFDB_S3_REGION=us-east-1
FFDB_S3_BUCKET=ffdb
FFDB_S3_ACCESS_KEY_ID=ffdb-local
FFDB_S3_SECRET_ACCESS_KEY=replace-with-the-minio-password

FFDB_EMAIL_TRANSPORT=smtp
FFDB_SMTP_HOST=mailpit
FFDB_SMTP_PORT=1025
FFDB_EMAIL_FROM=FFDB <noreply@localhost.test>
FFDB_SYNC_STATE_DIR=/var/lib/ffdb/sync
FFDB_SYNC_MAINTENANCE_INTERVAL_SECONDS=60
FFDB_SYNC_STALE_TEMPORARY_SECONDS=3600
RUST_LOG=ffdb=info,tower_http=info
Generate each value independentlysh
umask 077
openssl rand -hex 24       # PostgreSQL password
openssl rand -hex 24       # MinIO password
openssl rand -base64 32    # FFDB_MASTER_KEY
openssl rand -base64 32    # FFDB_BACKUP_MASTER_KEY
openssl rand -hex 32       # FFDB_CURSOR_HMAC_KEY
openssl rand -hex 32       # FFDB_BOOTSTRAP_TOKEN
uuidgen | tr '[:upper:]' '[:lower:]'  # FFDB_NODE_ID
chmod 600 .env

3. Start and verify the stack

Compose pulls the released images, starts dependencies in order, and waits for their health checks. The public entry point is the gateway on 127.0.0.1:5173. It serves compiled assets and proxies API traffic to private Axum port 8080; it is not a Vite development server.

Terminalsh
docker compose config --quiet
docker compose pull
docker compose up --detach --wait

docker compose ps
curl --fail http://127.0.0.1:5173/healthz
curl --fail http://127.0.0.1:5173/readyz
curl --fail http://127.0.0.1:5173/openapi.json >/dev/null

4. Create the first owner and choose the instance type

Open http://127.0.0.1:5173/app/. Paste FFDB_BOOTSTRAP_TOKEN from the protected .env file, create the first owner, and choose private, team, platform billing with your Stripe keys, or Stripe Connect. The wizard collects provider values when the selected mode needs them; no Stripe variables are required in compose.yaml.

Onboarding must finish before the owner can create organizations or projects. After it completes, create the first organization and project directly in the portal and verify that the project appears in the project switcher.

Optional signed lifecycle controller

The direct Compose path above is complete. Operators who also want signature verification, immutable digest resolution, installed release directories, backup/update/rollback commands, and root-owned generated configuration can use the release controller instead. It manages the same Docker topology; it is not required for the copyable Compose path.

Latest stable GitHub Releasesh
curl -fsSLo ffdb-install.sh \
  https://github.com/Forever-Frameworks-LLC/ffdb/releases/latest/download/install.sh
less ffdb-install.sh
sudo sh ffdb-install.sh --profile single-host --start --require-signature
sudo ffdb-host status
# Port 5173 is the packaged nginx gateway, not a Vite server.
curl --fail http://127.0.0.1:5173/readyz
Reproducible exact tagsh
VERSION=0.3.0
RELEASE_BASE="https://github.com/Forever-Frameworks-LLC/ffdb/releases/download/v$VERSION"
curl -fsSLo ffdb-install.sh "$RELEASE_BASE/install.sh"
less ffdb-install.sh
sudo sh ffdb-install.sh --profile single-host --start --require-signature \
  --version "$VERSION" --release-base "$RELEASE_BASE"
sudo ffdb-host status
# Port 5173 is the packaged nginx gateway, not a Vite server.
curl --fail http://127.0.0.1:5173/readyz
Verified mirror or air-gapped bundlesh
sudo env \
  FFDB_VERSION=0.3.0 \
  FFDB_RELEASE_BASE_URL=file:///srv/ffdb/releases/v0.3.0 \
  sh ./install.sh --profile single-host --start --require-signature
sudo ffdb-host status

Understand the single-host boundary

The selected release pins PostgreSQL, MinIO, Mailpit, the FFDB runtime, and the gateway by immutable image digest. The installer generates independent strong PostgreSQL, MinIO, encryption, HMAC, bootstrap, and node credentials, writes them to /etc/ffdb/single-host.env at mode 0600, preserves existing secrets on reinstall or upgrade, and never prints them.

  • The compiled nginx gateway listens at http://127.0.0.1:5173; this is not a Vite development server.
  • The gateway serves the landing site at /, docs at /docs/, and the portal at /app/, then proxies /v1, /healthz, /readyz, and /openapi.json to the private Axum service at api:8080. Raw /metrics returns 404 at the gateway and remains private to Axum.
  • The packaged Docker profiles expose Axum port 8080 only to the Compose network, never directly to the host.
  • MinIO API and console listen at loopback ports 9000 and 9001.
  • Mailpit SMTP and captured-mail UI listen at loopback ports 1025 and 8025.
  • PostgreSQL has no host port; all exposed host ports are loopback-only.
  • The seven named volumes preserve PostgreSQL, object data, captured mail, project SQLite files, encrypted backups, organization usage/billing ledgers, and sync state across normal stop, upgrade, rollback, and uninstall.

Retrieve the generated bootstrap token safely

The bootstrap token remains only in the root-readable single-host environment. Extract it to a root-owned file for secret-manager ingestion without writing the value to the terminal; ffdb-host status and logs do not reveal it.

Terminalsh
sudo sh -c 'umask 077; sed -n "s/^FFDB_BOOTSTRAP_TOKEN=//p" \
  /etc/ffdb/single-host.env > /root/ffdb-bootstrap-token'

Operate an external-provider production release

For internet production, configure the external-provider profile with independently backed-up PostgreSQL, private HTTPS S3-compatible storage, real email delivery, and a TLS gateway. Signed release metadata pins the multi-architecture runtime and gateway images by immutable digest, and ffdb-host invokes the matching Compose model.

Terminalsh
# For a directly downloaded and verified bundle:
sudo ffdb-host install \
  --version 0.3.0 \
  --bundle /srv/ffdb/releases/ffdb-compose-bundle-0.3.0.tar.gz

# The public/local installer already performs the install step:
sudo ffdb-host start
sudo ffdb-host status
sudo ffdb-host logs api

Understand the service boundary

  • volume-init gives fixed UID/GID 10001 ownership of named project-data, backup-data, metrics-data, and sync-data volumes.
  • api supervises bounded ffdb-database-worker children and writes only project, backup, and organization-metrics volumes.
  • sync-worker maintains durable sync checkpoint artifacts and writes only sync-data.
  • gateway is the only FFDB application ingress, bound to 127.0.0.1:${FFDB_GATEWAY_PORT:-5173}; it is compiled nginx serving static production assets and proxying API routes to Axum's internal port 8080, not Vite.
  • All long-running containers are non-root, capability-free, resource-bounded, and use read-only root filesystems.

Operate and troubleshoot

For the direct path, start with docker compose ps and the bounded service logs above. A failed PostgreSQL login usually means POSTGRES_PASSWORD and the password inside FFDB_DATABASE_URL differ. A failed MinIO bootstrap usually means MINIO_ROOT_PASSWORD and FFDB_S3_SECRET_ACCESS_KEY differ. An instance.setup_required response means the owner must finish the portal wizard before creating organizations or projects. For production, also inspect response security headers, directly reload a nested /docs/ route, and verify an authorized browser upload against the configured S3 CORS and gateway CSP.

Direct Compose status, logs, and lifecyclesh
docker compose ps
docker compose logs --tail=200 api
docker compose logs --tail=200 sync-worker
docker compose logs --tail=200 gateway

docker compose stop
docker compose start

# Update only after reading release notes and taking a backup.
docker compose pull
docker compose up --detach --wait
Public TLS checkssh
curl --fail --show-error https://ffdb.example.com/healthz
curl --fail --show-error https://ffdb.example.com/readyz
curl --fail --show-error   https://ffdb.example.com/openapi.json >/dev/null
Service state and logssh
sudo ffdb-host status
sudo ffdb-host logs api
sudo ffdb-host logs sync-worker
sudo ffdb-host logs gateway

Upgrade, roll back, and retain data

ffdb-host update-check compares the active release with the latest stable GitHub Release without changing the host. Read the intervening release notes, complete the topology-appropriate coordinated backup, then run update. No arguments selects latest stable; --version pins an exact target for scheduled production rollouts. The controller installs beside the current release and preserves configuration and named volumes.

Check and updatesh
sudo ffdb-host version
sudo ffdb-host update-check

# Packaged single-host backup. External providers need their coordinated backup.
sudo ffdb-host backup create /secure/ffdb-before-update.tar.gz
sudo FFDB_REQUIRE_SIGNATURE=1 ffdb-host update
sudo ffdb-host status

# Reproducible alternative after reviewing the exact tag:
sudo ffdb-host update-check --version 0.3.1
sudo FFDB_REQUIRE_SIGNATURE=1 ffdb-host update --version 0.3.1

# If acceptance fails:
sudo ffdb-host rollback 0.3.0 --acknowledge-migration-risk

# Normal shutdown preserves durable state:
sudo ffdb-host stop

Install with Docker Compose workflow

  • 1. Save the complete compose.yaml and .env examples in an empty directory.
  • 2. Generate every secret independently, copy matching provider passwords into their FFDB values, and restrict .env to the operator.
  • 3. Run docker compose config, pull, and docker compose up --detach --wait.
  • 4. Verify health, readiness, OpenAPI, and service status through the compiled loopback gateway.
  • 5. Open /app/, create the first owner, choose the instance type, finish onboarding, and create the first organization and project.

Verify install with docker compose

The complete seven-volume stack reports ready on loopback, the first owner can finish instance setup, and no source repository or developer toolchain is involved.

Troubleshoot install with docker compose

  • Compose rejects a missing value — replace the named placeholder rather than removing the check.
  • A container is unhealthy — inspect docker compose ps and bounded logs, then correct the specific dependency or matching credential.
  • Organization creation returns instance.setup_required — finish the owner setup wizard.
  • The single-host stack is being exposed to the internet — stop and move to the external-provider production profile.

Continue from Install with Docker Compose

  • Complete the Quickstart application query.
  • Move production dependencies off host and schedule coordinated encrypted backups and alerts.