Skip to content

On-Premises Deployment Guide

This document covers how to package and deliver TheAccessible.Org as a self-contained on-premises product for university and government customers. It builds on the existing Docker Compose local development environment (local-dev.md) and extends it into three deployment tiers:

  1. Docker Compose + Management CLI β€” the primary delivery model
  2. VM Appliance (OVA) β€” for customers who want zero-Docker-knowledge turnkey deployment
  3. Helm Charts β€” for customers already running Kubernetes (future)

The management CLI (accessible-server) is the core of the on-prem experience. It wraps Docker Compose with lifecycle management: setup, upgrades, rollbacks, backups, health monitoring, and secrets management.

Table of Contents


Architecture Overview

The on-prem stack replaces all cloud-managed services with local equivalents running in Docker. The customer’s server runs the entire application β€” API, workers, database, storage, queueing, and monitoring β€” behind a reverse proxy with TLS termination.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Customer Server (Ubuntu 22.04+) β”‚
β”‚ β”‚
β”‚ accessible-server CLI β”‚
β”‚ β”œβ”€β”€ manages Docker Compose lifecycle β”‚
β”‚ β”œβ”€β”€ handles config, secrets, backups β”‚
β”‚ └── exposes health dashboard β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Docker Compose Stack β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ Caddy (reverse proxy + auto TLS) β”‚ β”‚
β”‚ β”‚ β”œβ”€β”€ :443 β†’ Traefik :8800 (API) β”‚ β”‚
β”‚ β”‚ └── :443 β†’ Next.js :3000 (Frontend) β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ Traefik (internal LB) β”‚ β”‚
β”‚ β”‚ β”œβ†’ API Node 1 :8790 β”‚ β”‚
β”‚ β”‚ β””β†’ API Node 2 :8790 β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ PostgreSQL 15 (replaces Supabase Postgres) β”‚ β”‚
β”‚ β”‚ GoTrue (replaces Supabase Auth) β”‚ β”‚
β”‚ β”‚ PostgREST (replaces Supabase REST) β”‚ β”‚
β”‚ β”‚ MinIO (replaces Cloudflare R2 / AWS S3) β”‚ β”‚
β”‚ β”‚ Redis (replaces Cloudflare KV + SQS) β”‚ β”‚
β”‚ β”‚ Batch Worker (replaces EC2 ASG / Lambda) β”‚ β”‚
β”‚ β”‚ WeasyPrint (HTML-to-PDF engine) β”‚ β”‚
β”‚ β”‚ Audiveris (music notation OCR) β”‚ β”‚
β”‚ β”‚ Marker (local PDF parsing β€” no Datalab API) β”‚ β”‚
β”‚ β”‚ Loki + Grafana (monitoring) β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Differences from Cloud Architecture

ConcernCloud (Production)On-Prem
Frontend hostingCloudflare PagesCaddy serving static export
API computeCF Workers + LambdaNode.js in Docker (same Hono app)
DatabaseSupabase managed PostgresSelf-hosted PostgreSQL 15
AuthSupabase Auth (GoTrue)Self-hosted GoTrue
Object storageCloudflare R2MinIO (S3-compatible)
KV / sessionsCloudflare KVRedis
QueueSQSRedis (BullMQ) or Redis Streams
Batch workersEC2 ASG spot instancesDocker container(s), scaled by CLI
PDF parsingDatalab Marker APILocal Marker container (GPU optional)
TLSCloudflare edgeCaddy auto-TLS (Let’s Encrypt or customer cert)
DNSCloudflare DNSCustomer’s DNS
MonitoringGrafana Cloud + Uptime KumaBundled Grafana + Loki
BackupsSupabase daily + R2CLI-managed pg_dump + MinIO mirror

Cloud-to-Local Service Mapping

Already Portable (No Code Changes)

These services use the same code paths that exist today in the Node server mode:

  • Object Storage: S3ObjectStorage class already supports configurable S3_ENDPOINT. Point at MinIO.
  • API: Same Hono app running in Node.js via @hono/node-server (identical to current Docker deployment).
  • PDF Processing: WeasyPrint and Audiveris containers are unchanged.
  • Browser Rendering: Native Puppeteer in the API Node container (same as current Node server mode).
  • Monitoring: Loki + Grafana + Promtail Docker Compose stack is already built.

Requires New Abstraction

ServiceCurrent ImplementationOn-Prem ReplacementWork Required
KV StoreCloudflareKvRestStorage (REST API) or InMemoryKvStorage (local)Redis via ioredisNew RedisKvStorage class implementing the existing KvStorage interface
QueueSQS via AWS SDKRedis via BullMQNew RedisQueue class; replace SQS polling in batch worker
AuthSupabase Auth (GoTrue hosted)Self-hosted GoTrue containerGoTrue config + JWT secret alignment; no API code changes
Database@supabase/supabase-js via Supabase KongSame client pointed at self-hosted Kong/PostgRESTConfig-only change (SUPABASE_URL env var)
PDF ParsingDatalab hosted Marker APILocal Marker Docker containerNew provider in the conversion cascade; env var to select local vs hosted

New Components (On-Prem Only)

ComponentPurposeImplementation
CaddyReverse proxy + automatic TLSOfficial Caddy Docker image with Caddyfile generated by CLI
RedisKV store + job queueOfficial Redis 7 Docker image, AOF persistence
Marker (local)PDF-to-markdown parsingDatalab’s open-source Docker image, GPU optional
Management CLILifecycle managementNode.js CLI (accessible-server)

Container Registry Strategy

All on-prem Docker images are published to GitHub Container Registry (ghcr.io) under the anglinai org. Customers authenticate with a read-only Personal Access Token (PAT) or a deploy token provided during onboarding.

Image Naming Convention

ghcr.io/anglinai/accessible-api:1.5.0
ghcr.io/anglinai/accessible-api:1.5.0-20260402
ghcr.io/anglinai/accessible-api:latest
ghcr.io/anglinai/accessible-worker:1.5.0
ghcr.io/anglinai/accessible-frontend:1.5.0
ghcr.io/anglinai/accessible-weasyprint:1.5.0
ghcr.io/anglinai/accessible-audiveris:1.5.0
ghcr.io/anglinai/accessible-marker:1.5.0

Versioning

  • Semantic versioning for release tags: MAJOR.MINOR.PATCH
  • Date-stamped tags for traceability: 1.5.0-20260402
  • latest tag always points to the most recent stable release
  • edge tag for pre-release builds (opt-in via CLI config)

CI/CD Publishing

A GitHub Actions workflow builds and pushes images on every tagged release:

.github/workflows/publish-images.yml
name: Publish On-Prem Images
on:
push:
tags: ['v*']
jobs:
publish:
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
file: workers/api/Dockerfile
push: true
tags: |
ghcr.io/anglinai/accessible-api:${{ github.ref_name }}
ghcr.io/anglinai/accessible-api:latest
# Repeat for worker, frontend, weasyprint, audiveris, marker

Customer Authentication

During accessible-server init, the CLI prompts for a registry token and writes it to Docker’s credential store:

Terminal window
docker login ghcr.io -u customer-org -p <token>

The token is scoped to read:packages only.


Management CLI (accessible-server)

The management CLI is the primary interface for on-prem customers. It wraps Docker Compose with opinionated lifecycle management, eliminating the need for customers to understand Docker internals.

Distribution

The CLI is distributed as an npm global package:

Terminal window
npm install -g @anglinai/accessible-server

For environments without Node.js, a standalone binary is built with pkg or bun build --compile:

Terminal window
# Download standalone binary (Linux x64)
curl -fsSL https://releases.theaccessible.org/cli/latest/accessible-server-linux-x64 \
-o /usr/local/bin/accessible-server
chmod +x /usr/local/bin/accessible-server

CLI Package Structure

packages/accessible-server/
β”œβ”€β”€ package.json
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ index.ts # Entry point, command router
β”‚ β”œβ”€β”€ commands/
β”‚ β”‚ β”œβ”€β”€ init.ts # Interactive setup wizard
β”‚ β”‚ β”œβ”€β”€ start.ts # Start all services
β”‚ β”‚ β”œβ”€β”€ stop.ts # Graceful shutdown
β”‚ β”‚ β”œβ”€β”€ restart.ts # Stop + start
β”‚ β”‚ β”œβ”€β”€ status.ts # Health dashboard
β”‚ β”‚ β”œβ”€β”€ upgrade.ts # Pull new images, migrate, restart
β”‚ β”‚ β”œβ”€β”€ rollback.ts # Revert to previous version
β”‚ β”‚ β”œβ”€β”€ backup.ts # Database + storage backup
β”‚ β”‚ β”œβ”€β”€ restore.ts # Restore from backup
β”‚ β”‚ β”œβ”€β”€ logs.ts # Tail service logs
β”‚ β”‚ β”œβ”€β”€ config.ts # View/edit configuration
β”‚ β”‚ β”œβ”€β”€ secrets.ts # Manage API keys and credentials
β”‚ β”‚ β”œβ”€β”€ doctor.ts # Diagnose common issues
β”‚ β”‚ └── uninstall.ts # Clean removal
β”‚ β”œβ”€β”€ lib/
β”‚ β”‚ β”œβ”€β”€ compose.ts # Docker Compose subprocess wrapper
β”‚ β”‚ β”œβ”€β”€ config.ts # Config file read/write
β”‚ β”‚ β”œβ”€β”€ health.ts # Health check logic
β”‚ β”‚ β”œβ”€β”€ migrate.ts # Database migration runner
β”‚ β”‚ β”œβ”€β”€ backup.ts # Backup/restore logic
β”‚ β”‚ β”œβ”€β”€ registry.ts # Image pull and version check
β”‚ β”‚ β”œβ”€β”€ tls.ts # TLS certificate management
β”‚ β”‚ └── ui.ts # Terminal UI helpers (spinners, tables)
β”‚ └── templates/
β”‚ β”œβ”€β”€ docker-compose.yml # Production Compose template
β”‚ β”œβ”€β”€ Caddyfile # Reverse proxy template
β”‚ β”œβ”€β”€ .env.template # Environment variable template
β”‚ └── grafana/ # Pre-configured dashboards
β”œβ”€β”€ bin/
β”‚ └── accessible-server # Shebang entry point
└── __tests__/
β”œβ”€β”€ commands/
└── lib/

Dependencies

  • commander β€” CLI framework
  • inquirer β€” interactive prompts (for init)
  • chalk β€” colored output
  • ora β€” spinners
  • yaml β€” Compose file manipulation
  • execa β€” subprocess execution (docker, docker compose)
  • semver β€” version comparison
  • cli-table3 β€” formatted tables

CLI Command Reference

accessible-server init

Interactive setup wizard. Run once on a fresh server.

$ accessible-server init
TheAccessible.Org β€” On-Premises Setup
? Enter your license key: XXXX-XXXX-XXXX-XXXX
βœ” License validated (Organization: State University, Tier: Enterprise)
? Server hostname (FQDN): accessible.university.edu
? TLS certificate source:
❯ Automatic (Let's Encrypt)
Custom certificate (provide cert + key paths)
Self-signed (development only)
? Container registry token: ghcr_xxxxxxxxxxxxx
βœ” Authenticated with ghcr.io/anglinai
? PostgreSQL password: β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’
? MinIO root password: β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’
? JWT secret (auto-generate recommended): [auto]
βœ” Generated 256-bit JWT secret
AI Service Configuration (optional β€” skip for air-gapped deployments):
? Anthropic API key (Claude): sk-ant-β€’β€’β€’β€’β€’β€’β€’β€’
? Google Cloud credentials file: /path/to/vertex-ai.json
? Gemini API key: β€’β€’β€’β€’β€’β€’β€’β€’
? Mathpix App ID: (skipped)
? Mathpix App Key: (skipped)
? Data directory (for volumes): /opt/accessible/data
? Backup directory: /opt/accessible/backups
βœ” Configuration written to /opt/accessible/config.yml
βœ” Secrets encrypted and stored in /opt/accessible/.secrets
βœ” Docker Compose file generated
βœ” Caddyfile generated for accessible.university.edu
βœ” Pulling images from ghcr.io/anglinai... (this may take a few minutes)
βœ” All 12 images pulled (v1.5.0)
Run 'accessible-server start' to launch the stack.

What init does:

  1. Validates the license key against the AnglinAI license API (or offline with a signed JWT license file for air-gapped installs)
  2. Prompts for hostname, TLS preference, credentials, and AI API keys
  3. Generates a config.yml with all non-secret settings
  4. Encrypts secrets at rest in .secrets (AES-256, key derived from a machine-specific fingerprint)
  5. Generates a docker-compose.yml from the template, injecting the customer’s configuration
  6. Generates a Caddyfile for TLS termination
  7. Authenticates with the container registry and pulls all images
  8. Creates the data directory structure for persistent volumes
  9. Runs a preflight check: Docker version, available disk, available RAM, port conflicts

accessible-server start

Starts the entire stack in the correct dependency order.

$ accessible-server start
Starting TheAccessible.Org v1.5.0...
βœ” PostgreSQL healthy (0.8s)
βœ” GoTrue (Auth) healthy (1.2s)
βœ” PostgREST healthy (0.5s)
βœ” Redis healthy (0.3s)
βœ” MinIO healthy (0.6s)
βœ” WeasyPrint healthy (0.4s)
βœ” Audiveris healthy (0.7s)
βœ” Marker healthy (1.5s)
βœ” API Node 1 healthy (2.1s)
βœ” API Node 2 healthy (2.3s)
βœ” Batch Worker healthy (1.8s)
βœ” Caddy (TLS) healthy (0.9s)
βœ” Grafana healthy (1.1s)
All 13 services running.
Application: https://accessible.university.edu
Grafana: https://accessible.university.edu/grafana
MinIO Console: http://localhost:9001 (internal only)

What start does:

  1. Reads config.yml and .secrets
  2. Runs docker compose up -d
  3. Waits for each service health check in dependency order
  4. Prints the service status table
  5. If any service fails health check after 60s, prints diagnostic info and suggests accessible-server doctor

accessible-server stop

Graceful shutdown with configurable drain period.

$ accessible-server stop
Draining active requests (30s grace period)...
βœ” All services stopped. Data volumes preserved.

accessible-server status

Dashboard view of all services, resource usage, and version info.

$ accessible-server status
TheAccessible.Org v1.5.0 (build 247, 2026-04-01)
Hostname: accessible.university.edu
Uptime: 14d 6h 32m
Service Status CPU RAM Restarts
─────────────────────────────────────────────────
PostgreSQL healthy 2.1% 512MB 0
GoTrue healthy 0.3% 64MB 0
PostgREST healthy 0.2% 48MB 0
Redis healthy 0.5% 128MB 0
MinIO healthy 1.0% 256MB 0
API Node 1 healthy 8.2% 1.1GB 0
API Node 2 healthy 7.8% 1.0GB 0
Batch Worker healthy 3.4% 780MB 0
WeasyPrint healthy 0.1% 92MB 0
Audiveris healthy 0.1% 180MB 0
Marker healthy 0.5% 640MB 0
Caddy healthy 0.1% 32MB 0
Grafana healthy 0.8% 128MB 0
Storage: 42.1 GB / 500 GB (8.4%)
Database: 2.3 GB (1,247 documents processed)
Last backup: 2026-04-02 02:00 UTC (successful)
TLS certificate: valid until 2026-06-30 (89 days)

accessible-server upgrade

Pulls new images, runs migrations, and restarts services with zero-downtime rolling update.

$ accessible-server upgrade
Current version: 1.5.0
Checking for updates...
βœ” New version available: 1.6.0
Changelog:
- Added batch remediation for Word documents
- Improved math OCR accuracy for scanned PDFs
- Fixed: empty alt text on decorative images
- Security: updated Puppeteer to 23.x
? Proceed with upgrade to 1.6.0? (Y/n) Y
βœ” Pre-upgrade backup completed β†’ /opt/accessible/backups/pre-upgrade-1.5.0-20260402.tar.gz
βœ” Pulling new images... (1.6.0)
βœ” Running database migrations (3 new)...
- 20260315_058_add_batch_word_support.sql βœ”
- 20260322_059_alt_text_decorative_flag.sql βœ”
- 20260401_060_usage_metrics_index.sql βœ”
βœ” Rolling restart (API Node 1 β†’ API Node 2 β†’ Worker)...
βœ” Health checks passing.
Upgrade complete: 1.5.0 β†’ 1.6.0
Rollback available: 'accessible-server rollback'

Upgrade flow:

  1. Check the registry for newer image tags (compare semver)
  2. Fetch and display the changelog (from a CHANGELOG.md baked into the image or fetched from a release API)
  3. Create a pre-upgrade backup (database dump + config snapshot)
  4. Pull all new images (tagged with the target version)
  5. Run any new database migrations (from supabase/migrations/ directory inside the API image, diffed against the schema_migrations table)
  6. Rolling restart: update one API node at a time, keeping the other serving traffic via Traefik
  7. Run post-upgrade health checks
  8. If any health check fails, automatically trigger rollback

accessible-server rollback

Reverts to the previous version. Uses the pre-upgrade backup and previously-pulled images.

$ accessible-server rollback
Current version: 1.6.0
Rollback target: 1.5.0 (backup from 2026-04-02 14:30 UTC)
? Confirm rollback to 1.5.0? This will restore the database to pre-upgrade state. (Y/n) Y
βœ” Stopping services...
βœ” Restoring database from backup...
βœ” Switching images to 1.5.0 tags...
βœ” Starting services...
βœ” Health checks passing.
Rollback complete: 1.6.0 β†’ 1.5.0

Rollback mechanics:

  1. Stop all services
  2. Restore the database from the pre-upgrade pg_dump
  3. Update docker-compose.yml image tags to the previous version
  4. Start all services
  5. Verify health

accessible-server backup

Creates a point-in-time backup of database, object storage metadata, and configuration.

$ accessible-server backup
Creating backup...
βœ” PostgreSQL dump (2.3 GB compressed)
βœ” Configuration snapshot
βœ” MinIO metadata export (object list, not file contents)
βœ” Backup saved: /opt/accessible/backups/backup-20260402-143000.tar.gz (2.4 GB)
Retention: 14 backups kept (oldest: 2026-03-19)

What’s backed up:

  • PostgreSQL: full pg_dump (custom format, compressed)
  • Config: config.yml, secrets (encrypted), Caddyfile, docker-compose.yml
  • MinIO metadata: bucket listing + object manifest (not the actual files β€” those are too large for routine backup; customers use MinIO’s built-in replication or mount the data volume on backed-up storage)
  • Version manifest: current image tags, migration state

Automated backups:

accessible-server init installs a cron job:

0 2 * * * /usr/local/bin/accessible-server backup --quiet

Retention is configurable (default: 14 backups).

accessible-server restore <backup-file>

Restores from a backup archive.

$ accessible-server restore /opt/accessible/backups/backup-20260402-143000.tar.gz
⚠ This will replace the current database and configuration.
? Confirm restore? (Y/n) Y
βœ” Stopping services...
βœ” Restoring PostgreSQL...
βœ” Restoring configuration...
βœ” Starting services...
βœ” Health checks passing.
Restore complete from backup-20260402-143000.

accessible-server logs [service]

Tail logs from one or all services.

$ accessible-server logs # All services
$ accessible-server logs api # API nodes only
$ accessible-server logs worker # Batch worker
$ accessible-server logs --since 1h # Last hour
$ accessible-server logs --grep "error" # Filter

Wraps docker compose logs with friendlier service aliases.

accessible-server config

View or update configuration.

$ accessible-server config show # Print current config (secrets redacted)
$ accessible-server config set hostname accessible2.university.edu
$ accessible-server config set workers 4 # Scale batch workers
$ accessible-server config set tls.cert /path/to/cert.pem
$ accessible-server config set tls.key /path/to/key.pem

Changes that require a restart prompt the user:

βœ” Configuration updated.
⚠ This change requires a restart. Run 'accessible-server restart' to apply.

accessible-server secrets

Manage API keys and credentials without editing files directly.

$ accessible-server secrets set ANTHROPIC_API_KEY
? Enter value: sk-ant-β€’β€’β€’β€’β€’β€’β€’β€’
βœ” Secret stored (encrypted at rest).
⚠ Restart required to apply.
$ accessible-server secrets list
ANTHROPIC_API_KEY β€’β€’β€’β€’β€’β€’nt-a3Xk set 2026-03-15
GEMINI_API_KEY β€’β€’β€’β€’β€’β€’Bx9m set 2026-03-15
MATHPIX_APP_KEY (not set)
POSTGRES_PASSWORD β€’β€’β€’β€’β€’β€’β€’β€’ set 2026-03-01
MINIO_ROOT_PASSWORD β€’β€’β€’β€’β€’β€’β€’β€’ set 2026-03-01
JWT_SECRET β€’β€’β€’β€’β€’β€’β€’β€’ set 2026-03-01
REGISTRY_TOKEN β€’β€’β€’β€’β€’β€’gh_x set 2026-03-01
$ accessible-server secrets rotate JWT_SECRET
⚠ Rotating the JWT secret will invalidate all active sessions.
? Confirm? (Y/n) Y
βœ” New JWT secret generated and stored. Restart required.

accessible-server doctor

Diagnoses common issues and suggests fixes.

$ accessible-server doctor
Running diagnostics...
βœ” Docker version 27.1.0 (minimum: 24.0)
βœ” Docker Compose v2.29.1
βœ” Available disk: 423 GB (minimum: 50 GB)
βœ” Available RAM: 14.2 GB (minimum: 8 GB)
βœ” All ports available (443, 8800, 5432, 9000, 6379)
βœ” Container registry reachable (ghcr.io)
βœ” DNS resolves accessible.university.edu β†’ 10.0.1.50
βœ” TLS certificate valid (89 days remaining)
⚠ Anthropic API key: connection test failed (401 Unauthorized)
β†’ Run 'accessible-server secrets set ANTHROPIC_API_KEY' to update
βœ” Gemini API key: connection test passed
βœ” PostgreSQL accepting connections
βœ” MinIO accepting connections
βœ” Redis accepting connections
βœ” All API health checks passing
12/13 checks passed. 1 warning.

Configuration System

Config File: /opt/accessible/config.yml

# TheAccessible.Org On-Premises Configuration
version: "1"
license_key: "XXXX-XXXX-XXXX-XXXX"
server:
hostname: accessible.university.edu
data_dir: /opt/accessible/data
backup_dir: /opt/accessible/backups
backup_retention: 14 # number of backups to keep
log_level: info # debug, info, warn, error
tls:
mode: letsencrypt # letsencrypt, custom, selfsigned
email: admin@university.edu # for Let's Encrypt notifications
# cert: /path/to/cert.pem # for custom mode
# key: /path/to/key.pem # for custom mode
services:
api_replicas: 2 # number of API node containers
worker_replicas: 1 # number of batch worker containers
marker_enabled: true # local Marker for PDF parsing
marker_gpu: false # enable GPU acceleration for Marker
ai:
# Which AI providers are enabled (at least one vision LLM required)
anthropic_enabled: true
gemini_enabled: true
openai_enabled: false
mathpix_enabled: false # disabled by default for FERPA
registry:
url: ghcr.io/anglinai
channel: stable # stable, edge
database:
max_connections: 100
shared_buffers: 256MB # auto-tuned by CLI based on available RAM
storage:
minio_data_dir: /opt/accessible/data/minio
monitoring:
grafana_enabled: true
grafana_anonymous: false # require login to view dashboards
alert_email: admin@university.edu

Secrets File: /opt/accessible/.secrets

Encrypted at rest using AES-256-GCM. The encryption key is derived from a machine-specific fingerprint (machine-id + install timestamp) using PBKDF2.

The CLI decrypts secrets in memory when generating the Docker Compose environment, then passes them via Docker secrets or env_file (never written to disk in plaintext except in the ephemeral .env consumed by Compose).


Secrets Management

Encryption at Rest

All secrets (API keys, database passwords, JWT secret) are stored in an encrypted file. The decryption key is derived from:

  1. /etc/machine-id (unique per Linux install)
  2. Install timestamp (written during init)
  3. PBKDF2 with 100,000 iterations

This means the secrets file is useless if copied to another machine. For backup/restore across machines, the CLI includes accessible-server secrets export (password-protected archive) and accessible-server secrets import.

Secret Rotation

Terminal window
accessible-server secrets rotate POSTGRES_PASSWORD
accessible-server secrets rotate JWT_SECRET
accessible-server secrets rotate MINIO_ROOT_PASSWORD

Each rotation command:

  1. Generates a new cryptographically random value
  2. Updates the encrypted secrets file
  3. Prompts for a restart to apply
  4. For JWT_SECRET, warns that all active sessions will be invalidated

Upgrade and Rollback Strategy

Version Channels

ChannelTag PatternUse Case
stable1.5.0Production deployments (default)
edge1.6.0-rc.1Pre-release testing

Upgrade Flow (Detail)

accessible-server upgrade
β”‚
β”œβ”€ 1. Query registry for latest stable tag
β”œβ”€ 2. Compare with current version (semver)
β”œβ”€ 3. If newer: fetch changelog, display to user
β”œβ”€ 4. User confirms
β”œβ”€ 5. Create pre-upgrade backup
β”‚ β”œβ”€ pg_dump β†’ backup archive
β”‚ β”œβ”€ config.yml + .secrets snapshot
β”‚ └─ Record current image tags in manifest
β”œβ”€ 6. docker compose pull (new images)
β”œβ”€ 7. Extract migrations from new API image
β”‚ └─ docker run --rm api cat /app/supabase/migrations/ > /tmp/migrations/
β”œβ”€ 8. Diff against schema_migrations table
β”‚ └─ Run only new migrations in order
β”œβ”€ 9. Rolling restart
β”‚ β”œβ”€ Update api-node-1 β†’ wait for healthy
β”‚ β”œβ”€ Update api-node-2 β†’ wait for healthy
β”‚ └─ Update worker, marker, etc.
β”œβ”€ 10. Post-upgrade health check (all services)
β”‚ β”œβ”€ If healthy: βœ” upgrade complete
β”‚ └─ If unhealthy: automatic rollback
└─ 11. Record upgrade in /opt/accessible/data/upgrade-history.json

Rollback Flow

The CLI keeps the previous version’s images cached locally (Docker image layers). Rollback:

  1. Stops all services
  2. Restores the pre-upgrade database backup
  3. Reverts docker-compose.yml image tags to previous version
  4. Starts all services
  5. Verifies health

Limitation: Rollback restores the database to pre-upgrade state. Any data written between upgrade and rollback is lost. The CLI warns about this explicitly.


Backup and Restore

What’s Backed Up

ComponentMethodIncluded in Routine Backup
PostgreSQLpg_dump --format=customYes
ConfigFile copyYes
SecretsEncrypted copyYes
MinIO metadataObject manifest (bucket, key, size, etag)Yes
MinIO objectsNot included (too large)No β€” use MinIO replication or volume-level backup
Docker imagesNot included (re-pullable)No
Grafana dashboardsPre-provisioned (baked into image)No

MinIO Object Backup

For full object backup, customers have three options:

  1. Volume-level backup: Mount /opt/accessible/data/minio on a filesystem with snapshots (ZFS, LVM, or a SAN)
  2. MinIO site replication: Mirror to a second MinIO instance on another server
  3. Rclone: Schedule rclone sync to an external S3 bucket, NAS, or cloud storage

The CLI does not manage MinIO object backup directly because document volumes vary enormously (100 MB to 10+ TB). The admin guide includes setup instructions for all three options.

Backup Storage

Default: local filesystem at /opt/accessible/backups/. Customers can configure an external target:

config.yml
backup:
target: s3 # local, s3, nfs
s3_endpoint: https://s3.university.edu
s3_bucket: accessible-backups
s3_access_key: (via secrets)
s3_secret_key: (via secrets)

Health Monitoring

Built-in Health Checks

Every container has a Docker HEALTHCHECK. The CLI polls these and exposes a unified status via accessible-server status.

ServiceHealth EndpointCheck IntervalUnhealthy After
PostgreSQLpg_isready10s3 failures
GoTrueGET /health10s3 failures
PostgRESTGET / (200 OK)10s3 failures
Redisredis-cli ping10s3 failures
MinIOGET /minio/health/live10s3 failures
API NodeGET /health5s3 failures
Batch WorkerGET /health10s3 failures
WeasyPrintGET /health30s3 failures
AudiverisGET /health30s3 failures
MarkerGET /health30s3 failures
CaddyTCP :44310s3 failures

Grafana Dashboards (Pre-configured)

The on-prem Grafana instance ships with pre-provisioned dashboards:

  • System Overview: service health, CPU, RAM, disk usage
  • API Performance: request rate, latency p50/p95/p99, error rate
  • Conversion Pipeline: queue depth, processing time, success/failure rate
  • Storage: MinIO usage, PostgreSQL size, Redis memory

Alert Rules

AlertConditionAction
Service downAny container unhealthy for >2 minEmail to alert_email
High error rate>10 API errors/min for 5 minEmail
Disk usage >85%Checked every 5 minEmail
TLS cert expiring<14 days remainingEmail (daily until renewed)
Backup failedCron backup exit code != 0Email

Alerts are sent via the bundled Grafana alerting engine. SMTP is configured during init:

config.yml
monitoring:
smtp_host: smtp.university.edu
smtp_port: 587
smtp_from: accessible-alerts@university.edu
alert_email: admin@university.edu

VM Appliance (OVA)

For customers who want a true zero-touch appliance β€” no Docker knowledge, no terminal comfort required.

What It Is

A pre-baked Ubuntu 22.04 LTS virtual machine image containing:

  • Docker Engine + Docker Compose pre-installed
  • accessible-server CLI pre-installed
  • All container images pre-pulled (for the current release)
  • A first-boot setup wizard (web-based)
  • Unattended security updates enabled (Ubuntu unattended-upgrades)

The customer downloads the OVA (or QCOW2/VMDK), imports it into their hypervisor, boots it, and completes setup through a browser-based wizard.

Supported Hypervisors

FormatHypervisorNotes
OVAVMware ESXi, vSphere, WorkstationPrimary target for enterprise
QCOW2Proxmox, KVM, OpenStackCommon in university IT
VMDKVMware, VirtualBoxCompatibility format
VHD/VHDXHyper-VWindows Server environments

Build Process

The VM image is built with Packer (HashiCorp) using an Ubuntu 22.04 base:

packer/accessible-server.pkr.hcl
source "qemu" "accessible" {
iso_url = "https://releases.ubuntu.com/22.04/ubuntu-22.04.5-live-server-amd64.iso"
iso_checksum = "sha256:..."
vm_name = "accessible-server-1.5.0"
disk_size = "100G"
memory = 8192
cpus = 4
ssh_username = "accessible"
ssh_password = "changeme" # forced reset on first boot
shutdown_command = "sudo shutdown -P now"
}
build {
sources = ["source.qemu.accessible"]
# Base system
provisioner "shell" {
scripts = [
"packer/scripts/01-base-packages.sh", # apt update, essential tools
"packer/scripts/02-docker.sh", # Docker Engine + Compose
"packer/scripts/03-node.sh", # Node.js 22 LTS
"packer/scripts/04-cli.sh", # npm install -g @anglinai/accessible-server
"packer/scripts/05-pull-images.sh", # docker pull all images for current version
"packer/scripts/06-first-boot.sh", # systemd service for setup wizard
"packer/scripts/07-security.sh", # UFW, fail2ban, unattended-upgrades
"packer/scripts/08-cleanup.sh", # apt clean, zero free space for compression
]
}
# Export formats
post-processor "vagrant" {} # optional
post-processor "shell-local" {
inline = [
# Convert QCOW2 to OVA, VMDK, VHD
"qemu-img convert -O vmdk output/accessible-server-1.5.0 accessible-server-1.5.0.vmdk",
"# OVA packaging with ovftool or manual tar",
]
}
}

Packer Provisioning Scripts

01-base-packages.sh

#!/bin/bash
set -euo pipefail
apt-get update && apt-get upgrade -y
apt-get install -y \
curl wget git jq htop net-tools \
ca-certificates gnupg lsb-release \
ufw fail2ban unattended-upgrades

02-docker.sh

#!/bin/bash
set -euo pipefail
# Docker official install
curl -fsSL https://get.docker.com | sh
usermod -aG docker accessible
systemctl enable docker

04-cli.sh

#!/bin/bash
set -euo pipefail
npm install -g @anglinai/accessible-server

05-pull-images.sh

#!/bin/bash
set -euo pipefail
VERSION="1.5.0"
IMAGES=(
"ghcr.io/anglinai/accessible-api:${VERSION}"
"ghcr.io/anglinai/accessible-worker:${VERSION}"
"ghcr.io/anglinai/accessible-frontend:${VERSION}"
"ghcr.io/anglinai/accessible-weasyprint:${VERSION}"
"ghcr.io/anglinai/accessible-audiveris:${VERSION}"
"ghcr.io/anglinai/accessible-marker:${VERSION}"
"traefik:v3"
"caddy:2"
"postgres:15"
"supabase/gotrue:v2.158.1"
"postgrest/postgrest:v12.2.3"
"minio/minio:latest"
"redis:7-alpine"
"grafana/grafana:11.5.2"
"grafana/loki:3.4.2"
"grafana/promtail:3.4.2"
)
for img in "${IMAGES[@]}"; do
docker pull "$img"
done

06-first-boot.sh

#!/bin/bash
set -euo pipefail
# Create a systemd service that runs a web-based setup wizard on port 8080
# on the first boot. After setup completes, the service disables itself.
cat > /etc/systemd/system/accessible-setup.service <<EOF
[Unit]
Description=TheAccessible.Org First-Boot Setup
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/accessible-server setup-wizard --port 8080
ExecStartPost=/bin/systemctl disable accessible-setup.service
Restart=on-failure
User=accessible
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable accessible-setup.service

First-Boot Web Wizard

When the VM boots for the first time, a lightweight web server on port 8080 serves a setup wizard. The customer navigates to http://<vm-ip>:8080 and completes the same configuration as accessible-server init, but through a browser UI.

The wizard:

  1. Prompts for license key, hostname, TLS config, credentials, AI API keys
  2. Writes config.yml and .secrets
  3. Generates the Docker Compose file and Caddyfile
  4. Starts the stack
  5. Redirects the browser to the live application at https://hostname
  6. Disables the setup wizard service (one-time only)

VM Sizing Recommendations

TiervCPUsRAMDiskConcurrent Users
Small48 GB100 GBUp to 25
Medium816 GB250 GBUp to 100
Large1632 GB500 GBUp to 500

The CLI auto-tunes PostgreSQL (shared_buffers, work_mem, max_connections) and API node count based on detected RAM.

VM Update Path

Updates on the VM use the same accessible-server upgrade flow. The VM connects to the container registry (if internet-connected) and pulls new images. For air-gapped environments, the customer can:

  1. Download an update bundle (tar of Docker images) from a customer portal
  2. Load images: accessible-server upgrade --from-file update-1.6.0.tar.gz

The update bundle is built by CI:

Terminal window
# Export all images for a release
docker save ghcr.io/anglinai/accessible-api:1.6.0 \
ghcr.io/anglinai/accessible-worker:1.6.0 \
... | gzip > accessible-update-1.6.0.tar.gz

Networking and TLS

Default: Caddy with Automatic TLS

Caddy handles TLS termination with automatic Let’s Encrypt certificates. The customer only needs:

  1. A public DNS record pointing their hostname to the server’s IP
  2. Port 443 open inbound
  3. Port 80 open inbound (for ACME challenge)

Custom Certificates

For customers behind a corporate firewall or using internal CAs:

Terminal window
accessible-server config set tls.mode custom
accessible-server config set tls.cert /path/to/cert.pem
accessible-server config set tls.key /path/to/key.pem
accessible-server restart

Firewall Rules

The CLI configures UFW during init:

PortProtocolDirectionPurpose
443TCPInboundHTTPS (application + API)
80TCPInboundHTTP β†’ HTTPS redirect + ACME
22TCPInboundSSH (management)
9001TCPLocalhost onlyMinIO console
3000TCPLocalhost onlyGrafana

All other ports are internal to the Docker network and not exposed to the host.


FERPA Compliance Notes

The on-prem deployment is the strongest FERPA compliance posture because all data stays within the customer’s infrastructure. Key points:

  1. No sub-processor risk for data-at-rest: Documents, database, and storage never leave the server.
  2. Marker runs locally: No documents sent to Datalab’s hosted API. See ferpa-subprocessor-compliance.md for details.
  3. AI API keys are optional: For fully air-gapped deployments, all AI providers can be disabled. The conversion pipeline degrades gracefully β€” OCR and image descriptions are skipped, but structural HTML conversion still works.
  4. AI providers in the on-prem data path: If AI API keys are configured, document content is sent to those providers for processing. The customer controls which providers are enabled. Recommended for FERPA: Gemini via Vertex AI in the customer’s own GCP project (see ferpa-subprocessor-compliance.md Section: Google Gemini API).
  5. No telemetry, no phone-home: The on-prem stack does not send usage data, analytics, or crash reports to AnglinAI. The only outbound connections are: container registry (for updates), AI API endpoints (if configured), SMTP (if configured), and Let’s Encrypt (if auto-TLS is used).
  6. Data residency: All data stays on the customer’s server. No CDN, no edge caching, no Cloudflare proxy.

Air-Gapped Deployment

For maximum isolation (FISMA, classified environments):

  1. Use the VM appliance (pre-baked images, no registry pull needed)
  2. Disable all AI API keys (conversion runs without OCR/descriptions)
  3. Use custom TLS certificates (no Let’s Encrypt)
  4. Disable Grafana SMTP alerts (or use an internal SMTP relay)
  5. Updates via offline bundle: accessible-server upgrade --from-file

Sizing Guidelines

Minimum Requirements

ResourceMinimumRecommended
CPU4 cores8+ cores
RAM8 GB16+ GB
Disk50 GB (OS + application)100+ GB (plus document storage)
Docker24.0+Latest stable
OSUbuntu 22.04 LTSUbuntu 22.04 or 24.04 LTS

Scaling

DimensionHow to ScaleCLI Command
API throughputAdd API node replicasaccessible-server config set services.api_replicas 4
Processing throughputAdd worker replicasaccessible-server config set services.worker_replicas 3
StorageExpand MinIO data volume or add drivesMount additional storage at storage.minio_data_dir
DatabaseTune PostgreSQL or migrate to external RDSEdit config.yml database section
Marker (PDF parsing)Enable GPU accelerationaccessible-server config set services.marker_gpu true

File and Directory Layout

Installation Directory: /opt/accessible/

/opt/accessible/
β”œβ”€β”€ config.yml # Main configuration
β”œβ”€β”€ .secrets # Encrypted secrets
β”œβ”€β”€ docker-compose.yml # Generated Compose file (do not edit directly)
β”œβ”€β”€ Caddyfile # Generated reverse proxy config
β”œβ”€β”€ upgrade-history.json # Record of all upgrades/rollbacks
β”œβ”€β”€ data/ # Persistent data volumes
β”‚ β”œβ”€β”€ postgres/ # PostgreSQL data directory
β”‚ β”œβ”€β”€ minio/ # MinIO object storage
β”‚ β”œβ”€β”€ redis/ # Redis AOF persistence
β”‚ β”œβ”€β”€ grafana/ # Grafana database
β”‚ └── loki/ # Log storage
β”œβ”€β”€ backups/ # Backup archives
β”‚ β”œβ”€β”€ backup-20260402-020000.tar.gz
β”‚ β”œβ”€β”€ backup-20260401-020000.tar.gz
β”‚ └── pre-upgrade-1.5.0-20260402.tar.gz
└── logs/ # CLI operation logs
└── accessible-server.log

Implementation Roadmap

Phase 1: Docker Compose On-Prem Stack (2-3 weeks)

Goal: A working docker-compose.onprem.yml that replaces all cloud dependencies with local services.

TaskEffortStatus
Add Redis container to Compose1 dayNot started
Implement RedisKvStorage (same interface as CloudflareKvRestStorage)2 daysNot started
Implement RedisQueue to replace SQS polling in batch worker2 daysNot started
Add local Marker container to Compose1 dayNot started
Add Marker local provider to conversion cascade1 dayNot started
Add Caddy container for TLS termination0.5 daysNot started
Add Next.js static export container1 dayNot started
ON_PREM=true env flag to select local providers1 dayNot started
End-to-end test: full conversion pipeline on local-only stack2 daysNot started

Prerequisite: The existing local Docker Compose profile already runs Supabase, MinIO, LocalStack, and monitoring. Phase 1 replaces LocalStack (AWS emulation) with native Redis, adds Marker, and adds Caddy.

Phase 2: Management CLI (2-3 weeks)

Goal: Ship @anglinai/accessible-server as an npm package with core commands.

TaskEffortStatus
CLI scaffolding (commander, project structure)1 dayNot started
init command (interactive wizard, config generation)3 daysNot started
start / stop / restart commands1 dayNot started
status command (health dashboard)1 dayNot started
upgrade command (registry check, pull, migrate, rolling restart)3 daysNot started
rollback command1 dayNot started
backup / restore commands2 daysNot started
logs command0.5 daysNot started
config / secrets commands1 dayNot started
doctor command (diagnostics)1 dayNot started
Secrets encryption at rest1 dayNot started
Test suite for CLI commands2 daysNot started

Phase 3: Container Registry and CI/CD (1 week)

Goal: Automated image publishing on tagged releases.

TaskEffortStatus
GitHub Actions workflow for building + pushing images1 dayNot started
Image versioning (semver tags + date stamps)0.5 daysNot started
Changelog generation (from commits/PRs)0.5 daysNot started
Air-gapped update bundle generation1 dayNot started
Customer registry token provisioning0.5 daysNot started

Phase 4: VM Appliance (1-2 weeks)

Goal: A downloadable OVA/QCOW2 with the full stack pre-baked.

TaskEffortStatus
Packer template for Ubuntu 22.042 daysNot started
Provisioning scripts (Docker, Node, CLI, images)1 dayNot started
First-boot web wizard3 daysNot started
OVA/VMDK/QCOW2/VHD export pipeline1 dayNot started
Security hardening (UFW, fail2ban, unattended-upgrades)0.5 daysNot started
VM build automation in CI1 dayNot started
Testing on VMware, Proxmox, Hyper-V2 daysNot started

Phase 5: Documentation and Onboarding (1 week)

Goal: Customer-facing documentation and onboarding materials.

TaskEffortStatus
Customer deployment guide (Docusaurus)2 daysNot started
Air-gapped deployment addendum1 dayNot started
CLI reference documentation1 dayNot started
Video walkthrough (setup to first conversion)1 dayNot started

Total Estimated Effort

PhaseEffort
Phase 1: Docker Compose stack2-3 weeks
Phase 2: Management CLI2-3 weeks
Phase 3: Registry + CI/CD1 week
Phase 4: VM Appliance1-2 weeks
Phase 5: Documentation1 week
Total~7-10 weeks (one developer)

Phases 1 and 2 can partially overlap β€” the CLI can be developed against the existing local Compose profile while the on-prem-specific Compose changes are in progress.