Skip to content
Operate

Deployment

Installation, configuration, secrets, TLS, rate limits, workers and webhook destinations.

Full guide in the repository: docs/deployment.md

Install

GitHub Releases has archives for Linux, macOS and Windows on amd64 and arm64. Each contains ledgerforge, ledgerforge-mcp, the licence and the notice file. Checksums cover the archives, the source and container SBOMs and the recorded image digest. Release binaries are built without CGO, so the optional SQLite heartbeat falls back when SQLite is unavailable.

Release archive
$ curl -LO https://github.com/devaccuracy/ledgerforge/releases/download/v1.0.0/ledgerforge_v1.0.0_linux_amd64.tar.gz$ curl -LO https://github.com/devaccuracy/ledgerforge/releases/download/v1.0.0/checksums.txt$ sha256sum -c checksums.txt --ignore-missingledgerforge_v1.0.0_linux_amd64.tar.gz: OK$ tar -xzf ledgerforge_v1.0.0_linux_amd64.tar.gz$ ./ledgerforge_v1.0.0_linux_amd64/ledgerforge versionledgerforge 1.0.0 (commit 64c7aec, built 2026-10-10T05:53:57+00:00)

The container image ghcr.io/devaccuracy/ledgerforge:1.0.0 is published for linux/amd64 and linux/arm64 and contains both binaries. To build from source, use Go 1.26.9 or later:

go install
$ go install github.com/devaccuracy/ledgerforge/cmd/ledgerforge@v1.0.0$ go install github.com/devaccuracy/ledgerforge/cmd/ledgerforge-mcp@v1.0.0

Configuration and secrets

Copy ledgerforge.example.json to ledgerforge.json, replace the secret placeholder with a random value and set your database and Redis addresses. server.secure: trueenables authentication; don't disable it on a reachable deployment.

ledgerforge.example.json
{  "project_name": "LedgerForge",  "data_source": {    "dns": "postgres://postgres:password@postgres:5432/ledgerforge?sslmode=disable"  },  "redis": {    "dns": "redis:6379"  },  "server": {    "port": "5001",    "secure": true,    "secret_key": "REPLACE_WITH_A_RANDOM_SECRET"  }}

Environment variables such as LEDGERFORGE_SERVER_SECRET_KEY, LEDGERFORGE_DATA_SOURCE_DNS and LEDGERFORGE_REDIS_DNS override loaded fields; pass them to both server and workers. Keep the master secret in a secret manager and out of images, repositories, logs and support requests.

With the repository's compose file
$ cp ledgerforge.example.json ledgerforge.json# Replace server.secret_key first, for example with: openssl rand -hex 32$ LEDGERFORGE_IMAGE=ghcr.io/devaccuracy/ledgerforge:1.0.0 docker compose up -d

Processes

Each release
$ ledgerforge migrate up$ ledgerforge start$ ledgerforge workers

Run migrations before serving a new version, and keep API servers and workers on compatible versions. Pin a release tag or image digest in production rather than :main. Back up PostgreSQL with a tested restore procedure, and monitor worker lag, queue saturation, lock contention, rejected transactions and webhook failures.

Network, TLS and limits

  • Don't expose PostgreSQL, Redis, Typesense, tracing UIs or worker monitoring to the internet. Use PostgreSQL TLS and restricted roles, and Redis TLS across trust boundaries.
  • Terminate TLS at a reverse proxy with deadlines and body and connection limits, or use the built-in ACME support (server.ssl), which needs public ports 80 and 443.
  • Set explicit rate limits. The code's fallback is not a useful production limit, and limits are per process, so apply aggregate limits at the proxy too.
  • Request bodies default to 5 MB and uploads to 256 MB (server.max_request_body_size_mb, server.max_upload_size_mb). Set server.metrics_bearer_token before exposing metrics.
ledgerforge.json
{  "rate_limit": {    "requests_per_second": 100,    "burst": 200,    "cleanup_interval_sec": 60  }}

Webhook destinations

ledgerforge.json
{  "notification": {    "webhook": {      "url": "https://receiver.example/events",      "signing_secret": "REPLACE_WITH_A_SEPARATE_RANDOM_SECRET",      "block_private_destinations": true,      "timeout_sec": 30    }  }}

Registration and updates accept only absolute HTTP(S) URLs without credentials or fragments. block_private_destinations, off by default for compatibility with internal receivers, rejects private and special-purpose ranges at registration and again when connecting. Redirects are not followed, timeouts default to 30 seconds (maximum 120) and hook response bodies are limited to 1 MiB. Restrict egress at the network boundary as well. Non-2xx responses are retried.

Before upgrading an existing installation, read the upgrade notes. After any deployment, run both verification checks.