Chapter 05

Deployment

From local file to multi-region topology. What to configure, what to back up, and how to run across datacenters.

Deployment modes

DocumentForge is a library — there's no daemon to install. You deploy it inside your application. There are three common shapes:

ModeWho uses itReplication
EmbeddedOne process, one fileNone needed
API tierMultiple app servers → one DB processOptional followers
Leader + followersHigh availability / read scaleRequired
Multi-regionGlobal deploymentCross-region async

Embedded

The simplest mode. Your app opens the database file directly. Perfect for desktop apps, CLI tools, single-node services, tests, and demos.

using var db = DocumentForgeDb.OpenOrCreate("app.dfdb");

No network, no auth, no config. The file moves with your app. One process holds the write lock; other processes see a FileShare.Read lock and should either open their own file or connect via a network API layer.

API tier

When multiple application servers need to share data, run a dfdb serve process in front of the database. Each node reads a small node.json:

{
  "nodeName": "prod-1",
  "port": 5000,
  "dataDir": "/var/lib/documentforge"
}
dfdb serve --config node.json

# or with CLI flags
dfdb serve --node-name prod-1 --port 5000 --data-dir /var/lib/documentforge

# or with env vars
DFDB_NODE_NAME=prod-1 DFDB_PORT=5000 DFDB_DATA_DIR=/var/lib/documentforge dfdb serve

App servers send queries over HTTP:

POST /query  {"sql": "SELECT * FROM orders WHERE pnr = 'ABC123'"}

A single machine with a decent CPU handles tens of thousands of QPS comfortably.

Local multi-node dev cluster

To experiment with sharding on one box, the repo ships with launch scripts that start three nodes on ports 5001–5003 with separate data folders:

# Windows
.\scripts\start-cluster.ps1

# macOS / Linux
./scripts/start-cluster.sh

# Check health
dfdb health scripts/sample-cluster/cluster.json

Leader + Follower(s)

For read scaling and standby capability, run one leader and N followers. Writes go to the leader; reads can go to any node.

┌───────────────────┐
│  Leader (writes)  │ ──logical replication──┐
│  app.dfdb         │                        │
└───────────────────┘                        ▼
                                   ┌─────────────────┐
                                   │ Follower (read) │
                                   │ replica.dfdb    │
                                   └─────────────────┘

Setup (each process):

// On the leader host
using var db = DocumentForgeDb.OpenOrCreate("app.dfdb");
db.StartLogicalReplicationServer(5500);

// On each follower host
using var db = DocumentForgeDb.OpenOrCreate("replica.dfdb");
db.StartLogicalReplicationFollower("leader-host.internal", 5500);

Point your read-heavy queries at any follower. Writes always go to the leader.

Network: replication uses a raw TCP connection. Open firewall rules for the replication port between your nodes. There's no auth or encryption at the protocol level yet — run it on a trusted internal network or tunnel over VPN / WireGuard / service mesh mTLS.

Multi-region

For a global deployment — e.g. writes in Dubai, reads in Singapore and London — run followers in each region. Logical replication is asynchronous and handles cross-continental latency gracefully.

                          ┌────────────┐
                          │  Dubai     │
                          │  LEADER    │
                          └─────┬──────┘
                 ┌──────────────┼──────────────┐
                 │              │              │
        ┌────────▼──────┐  ┌────▼────────┐  ┌──▼─────────┐
        │  Singapore    │  │  Frankfurt  │  │  New York  │
        │  Follower     │  │  Follower   │  │  Follower  │
        └───────────────┘  └─────────────┘  └────────────┘

Read requests are routed to the nearest follower by your load balancer or client logic. Writes hit the Dubai leader; followers catch up in seconds.

To migrate the leader role between regions (e.g. rotating for planned maintenance), see the datacenter move runbook.

Files on disk

A DocumentForge database is up to four files. Keep them together.

FilePurposeSafe to delete?
app.dfdbThe database itselfNever (it's your data)
app.dfdb.walWrite-ahead logOnly when DB is cleanly closed
app.dfdb.recoveryCrash recovery logOnly when DB is cleanly closed
app.dfdb.followerseqFollower's last-applied seqYes, but triggers full catchup

Configuration

Most tuning is via DatabaseOptions:

var options = new DatabaseOptions
{
    CacheSizeInPages = 10_000,   // 10K pages × 8KB = 80MB cache
    EnableWal        = true      // WAL + recovery + replication
};

using var db = DocumentForgeDb.OpenOrCreate("app.dfdb", options);

Cache sizing rule of thumb

Docker

A production-ready Dockerfile ships in the repo root. It's a multi-stage build that publishes a self-contained single-file binary on top of runtime-deps:9.0-bookworm-slim — no .NET runtime needed in the final image. Final image size lands around 90–120 MB depending on compression.

Build and run locally

# Build the image
docker build -t dfdb .

# Run with a named volume so data survives restarts
docker run --rm -p 5000:5000 -v dfdb-data:/data dfdb

# In another terminal
curl http://localhost:5000/health

Production run (API key + replication secret)

docker run -d --name dfdb \
    -p 5000:5000 \
    -v dfdb-data:/data \
    -e DFDB_NODE_NAME=prod-1 \
    -e DFDB_API_KEY=sk_prod_abc123 \
    -e DFDB_REPLICATION_SECRET=repl_shared_xyz \
    dfdb

As a replication leader

docker run -d --name dfdb-leader \
    -p 5000:5000 -p 5500:5500 \
    -v dfdb-leader-data:/data \
    -e DFDB_REPLICATION_SECRET=repl_shared_xyz \
    dfdb serve --bind-all \
      --replication-role leader --replication-port 5500

As a follower with auto-failover

docker run -d --name dfdb-follower \
    -p 5010:5000 \
    -v dfdb-follower-data:/data \
    -e DFDB_REPLICATION_SECRET=repl_shared_xyz \
    dfdb serve --bind-all \
      --replication-role follower \
      --leader-host dfdb-leader --leader-port 5500 \
      --auto-failover-seconds 10
Image layout: runs as the non-root dfdb user, writes only to /data, exposes ports 5000 (HTTP) and 5500 (replication). Health check hits /health every 30 s. Signals are handled via tini so docker stop flushes cleanly.

Docker Compose (leader + follower, one file)

services:
  leader:
    image: dfdb
    build: .
    ports: ["5000:5000", "5500:5500"]
    volumes: ["leader-data:/data"]
    environment:
      DFDB_NODE_NAME: leader
      DFDB_REPLICATION_SECRET: repl_shared_xyz
    command: >
      serve --bind-all
      --replication-role leader --replication-port 5500

  follower:
    image: dfdb
    build: .
    ports: ["5010:5000"]
    volumes: ["follower-data:/data"]
    environment:
      DFDB_NODE_NAME: follower
      DFDB_REPLICATION_SECRET: repl_shared_xyz
    depends_on: [leader]
    command: >
      serve --bind-all
      --replication-role follower
      --leader-host leader --leader-port 5500
      --auto-failover-seconds 10

volumes:
  leader-data:
  follower-data:

docker compose up → you have a leader, a follower replicating from it, and auto-failover wired in one command.

Deploying to Render.com

A ready-to-use render.yaml blueprint ships in the repo root. Render reads it, provisions a web service + a persistent disk, and wires up a health check automatically.

One-click deploy

  1. Push this repo to GitHub or GitLab.
  2. In the Render dashboard: New +Blueprint → point it at the repo.
  3. Render reads render.yaml and creates:
    • A web service named documentforge built from the Dockerfile at the repo root.
    • A 10 GB persistent disk mounted at /data.
    • A health check on /health.
  4. Fill in the two "set on dashboard" env vars: DFDB_API_KEY and DFDB_REPLICATION_SECRET.
  5. Deploy. First build takes ~3–4 minutes; subsequent deploys reuse the Docker layer cache.

What the blueprint looks like

services:
  - type: web
    name: documentforge
    runtime: docker
    plan: starter
    region: oregon
    dockerfilePath: ./Dockerfile
    healthCheckPath: /health
    disk:
      name: dfdb-data
      mountPath: /data
      sizeGB: 10
    envVars:
      - key: DFDB_NODE_NAME
        value: render-1
      - key: DFDB_API_KEY
        sync: false             # set in dashboard, not in git
      - key: DFDB_REPLICATION_SECRET
        sync: false

How Render's $PORT is handled

Render injects a PORT env var at start time. The container's entrypoint (docker/entrypoint.sh) maps PORTDFDB_PORT before starting the binary, so the service listens on whichever port Render assigned. You don't need to hard-code anything.

TLS, custom domain, auth

Scaling on Render

Other platforms: the same image runs unchanged on Fly.io, Railway, Koyeb, Azure Container Apps, Google Cloud Run (with a persistent volume), AWS App Runner, and any Kubernetes cluster. The only platform-specific bit is the $PORT env var translation in docker/entrypoint.sh, which is a no-op when $PORT isn't set.

Backups

Because the database is a file, backup is file copy. But don't just cp a live database — you might get a torn page mid-write.

Cold backup (simplest, downtime required)

dotnet my-app.dll --shutdown       # stop the process
cp app.dfdb /backup/app.$(date).dfdb
cp app.dfdb.wal /backup/app.$(date).dfdb.wal
dotnet my-app.dll                  # start again

Warm backup (no downtime, uses a follower)

Much better approach for production: spin up a follower for the purpose of backup. Let it catch up. Pause replication. Copy the follower's file. Resume.

Streaming backup (future)

A future version will include an online backup command that walks the data file page-by-page with a stable snapshot, without blocking writes. Until then, the follower-as-backup pattern is the right approach.

Monitoring

DocumentForge exposes its state through the DatabaseStatistics object. Log these at regular intervals.

var s = db.GetStatistics();

Console.WriteLine($"File:     {s.FileSize / 1024.0 / 1024.0:F2} MB");
Console.WriteLine($"Pages:    {s.PageCount:N0}");
Console.WriteLine($"Cache:    {s.CachedPages:N0} pages ({s.DirtyPages} dirty)");
foreach (var coll in s.Collections)
    Console.WriteLine($"{coll.Name}: {coll.DocumentCount} docs, {coll.IndexCount} indexes");

For replication:

// Leader metrics
leader.LeaderCurrentSeq            // how many ops have been issued
leader.GetLogicalFollowerCount()   // connected followers

// Follower metrics
follower.FollowerLastSeq           // how far caught up
follower.LogicallyReplicatedOps    // running total
follower.GapsDetected              // > 0 means lost ops — alert!

Replication lag is leader.LeaderCurrentSeq - follower.FollowerLastSeq. Healthy systems keep this near zero during normal traffic.

Production checklist