Deployment#

Prerequisites#

  • A PostgreSQL, MySQL/MariaDB or MongoDB server that the API host can reach (and nobody else).
  • Two database accounts as described in SECURITY.md; scripts in docker/grants.
  • A TLS certificate, or a reverse proxy that provides one. Plain HTTP is only for loopback and local testing.
  • A random secret per caller: openssl rand -base64 48.

Configuration reference#

Settings come from appsettings.json, appsettings.<Environment>.json and environment variables named LDB_Ldb__<Section>__<Name> (for lists: …__Keys__0__KeyId). The environment wins.

Setting Default Meaning
Database:Provider postgres postgres, mysql (also MariaDB) or mongodb
Database:DmlConnectionString – Connection string / URL for data access (required)
Database:DdlConnectionString data connection Used only by schema sync
Database:MongoDatabase ldb MongoDB database name
Database:CommandTimeoutSeconds 15 Per statement; operations are cancelled after four times this
Security:AllowInsecureHttp false Accept plain HTTP from non-loopback addresses (local testing only)
Security:TimestampToleranceSeconds 30 Allowed clock difference
Security:MaxBodyBytes 1048576 Request size limit
Security:FailedAuthPerMinute 10 Failed attempts per address and minute before lock-out
Security:TrustedProxies [] Proxy addresses whose X-Forwarded-* headers are trusted
Security:Keys[n]:KeyId – Letters, digits, -, _, . (max 64)
Security:Keys[n]:Current / Next – Secret references env:NAME or file:/path (≥ 32 bytes)
Security:Keys[n]:AllowedNamespaces – Required. ["*"] allows all
Security:Keys[n]:AllowedOperations all e.g. ["find","count","insert"]
Security:Keys[n]:AllowedCidrs any e.g. ["203.0.113.10/32","10.0.0.0/8"]
Security:Keys[n]:RequestsPerMinute 600 Per key
Security:Keys[n]:AllowUnfilteredWrites false Permit all: true on update and delete
Schema:DestructiveMode Apply Apply, Quarantine, Deny
Schema:MaxTables 64 Tables per namespace
Limits:MaxRows 1000 Rows per find
Limits:MaxInsertRows 1000 Rows per insert
WriteBehind:* off Optional write buffering with scheduled flushes. All settings are listed in WRITE-BEHIND.md

Connection string examples:

postgres: Host=db.internal;Port=5432;Database=ldb;Username=ldb_data;Password=…;SSL Mode=Require
mysql:    Server=db.internal;Port=3306;Database=ldb;User ID=ldb_data;Password=…;SslMode=Required
mongodb:  mongodb://ldb_data:…@db.internal:27017/ldb?authSource=ldb&tls=true

Because connection strings contain passwords, prefer supplying them through the environment or an appsettings.Production.json with restricted permissions rather than committing them anywhere.

Docker#

The image (docker/Dockerfile) is multi-stage, runs as a non-root user on a chiseled base image and has no shell.

cp .env.example .env && $EDITOR .env
docker compose --profile postgres up -d --build      # or mysql / mongodb

The compose file starts the chosen database next to the API for local testing and allows plain HTTP. For production run the image against your own database and put TLS in front:

docker run -d --name ldb --restart unless-stopped --read-only --cap-drop ALL \
  -p 127.0.0.1:8080:8080 \
  -e LDB_Ldb__Database__Provider=postgres \
  -e LDB_Ldb__Database__DmlConnectionString='Host=…' \
  -e LDB_Ldb__Security__Keys__0__KeyId=main \
  -e LDB_Ldb__Security__Keys__0__Current=file:/run/secrets/ldb_main \
  -e LDB_Ldb__Security__Keys__0__AllowedNamespaces__0=mygame \
  -e LDB_Ldb__Security__TrustedProxies__0=172.17.0.1 \
  -v /etc/ldb/main.secret:/run/secrets/ldb_main:ro \
  ghcr.io/landminentester/lt-database-api:latest

With write-behind and a journal, give the container a writable volume (the image has an empty /data owned by the app user):

docker run … -v ldb-data:/data   -e LDB_Ldb__WriteBehind__Enabled=true   -e LDB_Ldb__WriteBehind__JournalPath=/data/write-behind.journal …

Reverse proxy with TLS#

Terminate TLS in a proxy, forward to 127.0.0.1:8080, and list the proxy under Security:TrustedProxies so the API sees the caller's real address and X-Forwarded-Proto: https.

Caddy (automatic certificates):

db.example.com {
    request_body { max_size 2MB }
    reverse_proxy 127.0.0.1:8080
}

nginx:

server {
    listen 443 ssl http2;
    server_name db.example.com;
    ssl_certificate     /etc/ssl/db.example.com.pem;
    ssl_certificate_key /etc/ssl/db.example.com.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    client_max_body_size 2m;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Without a proxy, give Kestrel a certificate:

{ "Kestrel": { "Endpoints": { "Https": { "Url": "https://*:8443", "Certificate": { "Path": "/etc/ldb/cert.pfx", "Password": "…" } } } } }

Linux (systemd)#

sudo useradd --system --home /opt/ldb --shell /usr/sbin/nologin ldb
sudo install -d -o root -g ldb -m 750 /opt/ldb /etc/ldb
sudo tar -xzf lt-database-api-linux-x64.tar.gz -C /opt/ldb
sudo install -m 640 -o root -g ldb ldb.env /etc/ldb/ldb.env      # LDB_… variables, one per line

/etc/systemd/system/ldb.service:

[Unit]
Description=lt-database-api
After=network-online.target

[Service]
Type=notify
User=ldb
WorkingDirectory=/opt/ldb
ExecStart=/opt/ldb/Ldb.Api
EnvironmentFile=/etc/ldb/ldb.env
Environment=ASPNETCORE_URLS=http://127.0.0.1:8080
Restart=on-failure
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
CapabilityBoundingSet=
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX

[Install]
WantedBy=multi-user.target

sudo systemctl enable --now ldb

Windows (service)#

Expand-Archive lt-database-api-win-x64.zip -DestinationPath C:\ldb
# Settings: machine-wide environment variables, or C:\ldb\appsettings.Production.json readable only by the service account
New-Service -Name ldb -BinaryPathName 'C:\ldb\Ldb.Api.exe' -DisplayName 'lt-database-api' -StartupType Automatic
Start-Service ldb

Put the TLS certificate in Kestrel's configuration (above) or run a proxy such as IIS ARR, Caddy or nginx in front. Restrict the file ACLs of the folder and secret files to the service account and administrators.

Operations#

  • Probes: GET /healthz (process is up), GET /readyz (database reachable, returns 503 otherwise).
  • Logs: JSON lines on stdout. Look for audit lines per request.
  • Backups: back up the database. Without write-behind the API holds no local state except caches. When buffering is enabled, also protect and back up pending journals and dead letters. Structure is recoverable from the caller's declaration, but data is not.
  • Upgrades: replace the binary or image and restart. Schema bookkeeping tables are forward compatible.
  • Rotating a secret: see SECURITY.md.
  • Several instances: possible, but nonce and idempotency caches are per instance; use sticky routing per key.

Type to search the documentation.