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:
- Docker Compose + Management CLI β the primary delivery model
- VM Appliance (OVA) β for customers who want zero-Docker-knowledge turnkey deployment
- 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
- Cloud-to-Local Service Mapping
- Container Registry Strategy
- Management CLI (
accessible-server) - CLI Command Reference
- Configuration System
- Secrets Management
- Upgrade and Rollback Strategy
- Backup and Restore
- Health Monitoring
- VM Appliance (OVA)
- Networking and TLS
- FERPA Compliance Notes
- Sizing Guidelines
- File and Directory Layout
- Implementation Roadmap
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
| Concern | Cloud (Production) | On-Prem |
|---|---|---|
| Frontend hosting | Cloudflare Pages | Caddy serving static export |
| API compute | CF Workers + Lambda | Node.js in Docker (same Hono app) |
| Database | Supabase managed Postgres | Self-hosted PostgreSQL 15 |
| Auth | Supabase Auth (GoTrue) | Self-hosted GoTrue |
| Object storage | Cloudflare R2 | MinIO (S3-compatible) |
| KV / sessions | Cloudflare KV | Redis |
| Queue | SQS | Redis (BullMQ) or Redis Streams |
| Batch workers | EC2 ASG spot instances | Docker container(s), scaled by CLI |
| PDF parsing | Datalab Marker API | Local Marker container (GPU optional) |
| TLS | Cloudflare edge | Caddy auto-TLS (Letβs Encrypt or customer cert) |
| DNS | Cloudflare DNS | Customerβs DNS |
| Monitoring | Grafana Cloud + Uptime Kuma | Bundled Grafana + Loki |
| Backups | Supabase daily + R2 | CLI-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:
S3ObjectStorageclass already supports configurableS3_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
| Service | Current Implementation | On-Prem Replacement | Work Required |
|---|---|---|---|
| KV Store | CloudflareKvRestStorage (REST API) or InMemoryKvStorage (local) | Redis via ioredis | New RedisKvStorage class implementing the existing KvStorage interface |
| Queue | SQS via AWS SDK | Redis via BullMQ | New RedisQueue class; replace SQS polling in batch worker |
| Auth | Supabase Auth (GoTrue hosted) | Self-hosted GoTrue container | GoTrue config + JWT secret alignment; no API code changes |
| Database | @supabase/supabase-js via Supabase Kong | Same client pointed at self-hosted Kong/PostgREST | Config-only change (SUPABASE_URL env var) |
| PDF Parsing | Datalab hosted Marker API | Local Marker Docker container | New provider in the conversion cascade; env var to select local vs hosted |
New Components (On-Prem Only)
| Component | Purpose | Implementation |
|---|---|---|
| Caddy | Reverse proxy + automatic TLS | Official Caddy Docker image with Caddyfile generated by CLI |
| Redis | KV store + job queue | Official Redis 7 Docker image, AOF persistence |
| Marker (local) | PDF-to-markdown parsing | Datalabβs open-source Docker image, GPU optional |
| Management CLI | Lifecycle management | Node.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.0ghcr.io/anglinai/accessible-api:1.5.0-20260402ghcr.io/anglinai/accessible-api:latestghcr.io/anglinai/accessible-worker:1.5.0ghcr.io/anglinai/accessible-frontend:1.5.0ghcr.io/anglinai/accessible-weasyprint:1.5.0ghcr.io/anglinai/accessible-audiveris:1.5.0ghcr.io/anglinai/accessible-marker:1.5.0Versioning
- Semantic versioning for release tags:
MAJOR.MINOR.PATCH - Date-stamped tags for traceability:
1.5.0-20260402 latesttag always points to the most recent stable releaseedgetag for pre-release builds (opt-in via CLI config)
CI/CD Publishing
A GitHub Actions workflow builds and pushes images on every tagged release:
name: Publish On-Prem Imageson: 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, markerCustomer Authentication
During accessible-server init, the CLI prompts for a registry token and writes it to Dockerβs credential store:
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:
npm install -g @anglinai/accessible-serverFor environments without Node.js, a standalone binary is built with pkg or bun build --compile:
# Download standalone binary (Linux x64)curl -fsSL https://releases.theaccessible.org/cli/latest/accessible-server-linux-x64 \ -o /usr/local/bin/accessible-serverchmod +x /usr/local/bin/accessible-serverCLI 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 frameworkinquirerβ interactive prompts (forinit)chalkβ colored outputoraβ spinnersyamlβ Compose file manipulationexecaβ subprocess execution (docker, docker compose)semverβ version comparisoncli-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:
- Validates the license key against the AnglinAI license API (or offline with a signed JWT license file for air-gapped installs)
- Prompts for hostname, TLS preference, credentials, and AI API keys
- Generates a
config.ymlwith all non-secret settings - Encrypts secrets at rest in
.secrets(AES-256, key derived from a machine-specific fingerprint) - Generates a
docker-compose.ymlfrom the template, injecting the customerβs configuration - Generates a
Caddyfilefor TLS termination - Authenticates with the container registry and pulls all images
- Creates the data directory structure for persistent volumes
- 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:
- Reads
config.ymland.secrets - Runs
docker compose up -d - Waits for each service health check in dependency order
- Prints the service status table
- 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:
- Check the registry for newer image tags (compare semver)
- Fetch and display the changelog (from a
CHANGELOG.mdbaked into the image or fetched from a release API) - Create a pre-upgrade backup (database dump + config snapshot)
- Pull all new images (tagged with the target version)
- Run any new database migrations (from
supabase/migrations/directory inside the API image, diffed against theschema_migrationstable) - Rolling restart: update one API node at a time, keeping the other serving traffic via Traefik
- Run post-upgrade health checks
- 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.0Rollback mechanics:
- Stop all services
- Restore the database from the pre-upgrade pg_dump
- Update
docker-compose.ymlimage tags to the previous version - Start all services
- 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 --quietRetention 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" # FilterWraps 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.pemChanges 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 Configurationversion: "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.eduSecrets 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:
/etc/machine-id(unique per Linux install)- Install timestamp (written during
init) - 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
accessible-server secrets rotate POSTGRES_PASSWORDaccessible-server secrets rotate JWT_SECRETaccessible-server secrets rotate MINIO_ROOT_PASSWORDEach rotation command:
- Generates a new cryptographically random value
- Updates the encrypted secrets file
- Prompts for a restart to apply
- For JWT_SECRET, warns that all active sessions will be invalidated
Upgrade and Rollback Strategy
Version Channels
| Channel | Tag Pattern | Use Case |
|---|---|---|
stable | 1.5.0 | Production deployments (default) |
edge | 1.6.0-rc.1 | Pre-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.jsonRollback Flow
The CLI keeps the previous versionβs images cached locally (Docker image layers). Rollback:
- Stops all services
- Restores the pre-upgrade database backup
- Reverts
docker-compose.ymlimage tags to previous version - Starts all services
- 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
| Component | Method | Included in Routine Backup |
|---|---|---|
| PostgreSQL | pg_dump --format=custom | Yes |
| Config | File copy | Yes |
| Secrets | Encrypted copy | Yes |
| MinIO metadata | Object manifest (bucket, key, size, etag) | Yes |
| MinIO objects | Not included (too large) | No β use MinIO replication or volume-level backup |
| Docker images | Not included (re-pullable) | No |
| Grafana dashboards | Pre-provisioned (baked into image) | No |
MinIO Object Backup
For full object backup, customers have three options:
- Volume-level backup: Mount
/opt/accessible/data/minioon a filesystem with snapshots (ZFS, LVM, or a SAN) - MinIO site replication: Mirror to a second MinIO instance on another server
- Rclone: Schedule
rclone syncto 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:
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.
| Service | Health Endpoint | Check Interval | Unhealthy After |
|---|---|---|---|
| PostgreSQL | pg_isready | 10s | 3 failures |
| GoTrue | GET /health | 10s | 3 failures |
| PostgREST | GET / (200 OK) | 10s | 3 failures |
| Redis | redis-cli ping | 10s | 3 failures |
| MinIO | GET /minio/health/live | 10s | 3 failures |
| API Node | GET /health | 5s | 3 failures |
| Batch Worker | GET /health | 10s | 3 failures |
| WeasyPrint | GET /health | 30s | 3 failures |
| Audiveris | GET /health | 30s | 3 failures |
| Marker | GET /health | 30s | 3 failures |
| Caddy | TCP :443 | 10s | 3 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
| Alert | Condition | Action |
|---|---|---|
| Service down | Any container unhealthy for >2 min | Email to alert_email |
| High error rate | >10 API errors/min for 5 min | |
| Disk usage >85% | Checked every 5 min | |
| TLS cert expiring | <14 days remaining | Email (daily until renewed) |
| Backup failed | Cron backup exit code != 0 |
Alerts are sent via the bundled Grafana alerting engine. SMTP is configured during init:
monitoring: smtp_host: smtp.university.edu smtp_port: 587 smtp_from: accessible-alerts@university.edu alert_email: admin@university.eduVM 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-serverCLI 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
| Format | Hypervisor | Notes |
|---|---|---|
| OVA | VMware ESXi, vSphere, Workstation | Primary target for enterprise |
| QCOW2 | Proxmox, KVM, OpenStack | Common in university IT |
| VMDK | VMware, VirtualBox | Compatibility format |
| VHD/VHDX | Hyper-V | Windows Server environments |
Build Process
The VM image is built with Packer (HashiCorp) using an Ubuntu 22.04 base:
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/bashset -euo pipefailapt-get update && apt-get upgrade -yapt-get install -y \ curl wget git jq htop net-tools \ ca-certificates gnupg lsb-release \ ufw fail2ban unattended-upgrades02-docker.sh
#!/bin/bashset -euo pipefail# Docker official installcurl -fsSL https://get.docker.com | shusermod -aG docker accessiblesystemctl enable docker04-cli.sh
#!/bin/bashset -euo pipefailnpm install -g @anglinai/accessible-server05-pull-images.sh
#!/bin/bashset -euo pipefailVERSION="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"done06-first-boot.sh
#!/bin/bashset -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 SetupAfter=network-online.target docker.serviceWants=network-online.target
[Service]Type=simpleExecStart=/usr/local/bin/accessible-server setup-wizard --port 8080ExecStartPost=/bin/systemctl disable accessible-setup.serviceRestart=on-failureUser=accessible
[Install]WantedBy=multi-user.targetEOF
systemctl daemon-reloadsystemctl enable accessible-setup.serviceFirst-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:
- Prompts for license key, hostname, TLS config, credentials, AI API keys
- Writes
config.ymland.secrets - Generates the Docker Compose file and Caddyfile
- Starts the stack
- Redirects the browser to the live application at
https://hostname - Disables the setup wizard service (one-time only)
VM Sizing Recommendations
| Tier | vCPUs | RAM | Disk | Concurrent Users |
|---|---|---|---|---|
| Small | 4 | 8 GB | 100 GB | Up to 25 |
| Medium | 8 | 16 GB | 250 GB | Up to 100 |
| Large | 16 | 32 GB | 500 GB | Up 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:
- Download an update bundle (tar of Docker images) from a customer portal
- Load images:
accessible-server upgrade --from-file update-1.6.0.tar.gz
The update bundle is built by CI:
# Export all images for a releasedocker save ghcr.io/anglinai/accessible-api:1.6.0 \ ghcr.io/anglinai/accessible-worker:1.6.0 \ ... | gzip > accessible-update-1.6.0.tar.gzNetworking and TLS
Default: Caddy with Automatic TLS
Caddy handles TLS termination with automatic Letβs Encrypt certificates. The customer only needs:
- A public DNS record pointing their hostname to the serverβs IP
- Port 443 open inbound
- Port 80 open inbound (for ACME challenge)
Custom Certificates
For customers behind a corporate firewall or using internal CAs:
accessible-server config set tls.mode customaccessible-server config set tls.cert /path/to/cert.pemaccessible-server config set tls.key /path/to/key.pemaccessible-server restartFirewall Rules
The CLI configures UFW during init:
| Port | Protocol | Direction | Purpose |
|---|---|---|---|
| 443 | TCP | Inbound | HTTPS (application + API) |
| 80 | TCP | Inbound | HTTP β HTTPS redirect + ACME |
| 22 | TCP | Inbound | SSH (management) |
| 9001 | TCP | Localhost only | MinIO console |
| 3000 | TCP | Localhost only | Grafana |
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:
- No sub-processor risk for data-at-rest: Documents, database, and storage never leave the server.
- Marker runs locally: No documents sent to Datalabβs hosted API. See
ferpa-subprocessor-compliance.mdfor details. - 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.
- 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.mdSection: Google Gemini API). - 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).
- 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):
- Use the VM appliance (pre-baked images, no registry pull needed)
- Disable all AI API keys (conversion runs without OCR/descriptions)
- Use custom TLS certificates (no Letβs Encrypt)
- Disable Grafana SMTP alerts (or use an internal SMTP relay)
- Updates via offline bundle:
accessible-server upgrade --from-file
Sizing Guidelines
Minimum Requirements
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 4 cores | 8+ cores |
| RAM | 8 GB | 16+ GB |
| Disk | 50 GB (OS + application) | 100+ GB (plus document storage) |
| Docker | 24.0+ | Latest stable |
| OS | Ubuntu 22.04 LTS | Ubuntu 22.04 or 24.04 LTS |
Scaling
| Dimension | How to Scale | CLI Command |
|---|---|---|
| API throughput | Add API node replicas | accessible-server config set services.api_replicas 4 |
| Processing throughput | Add worker replicas | accessible-server config set services.worker_replicas 3 |
| Storage | Expand MinIO data volume or add drives | Mount additional storage at storage.minio_data_dir |
| Database | Tune PostgreSQL or migrate to external RDS | Edit config.yml database section |
| Marker (PDF parsing) | Enable GPU acceleration | accessible-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.logImplementation 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.
| Task | Effort | Status |
|---|---|---|
| Add Redis container to Compose | 1 day | Not started |
Implement RedisKvStorage (same interface as CloudflareKvRestStorage) | 2 days | Not started |
Implement RedisQueue to replace SQS polling in batch worker | 2 days | Not started |
| Add local Marker container to Compose | 1 day | Not started |
| Add Marker local provider to conversion cascade | 1 day | Not started |
| Add Caddy container for TLS termination | 0.5 days | Not started |
| Add Next.js static export container | 1 day | Not started |
ON_PREM=true env flag to select local providers | 1 day | Not started |
| End-to-end test: full conversion pipeline on local-only stack | 2 days | Not 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.
| Task | Effort | Status |
|---|---|---|
| CLI scaffolding (commander, project structure) | 1 day | Not started |
init command (interactive wizard, config generation) | 3 days | Not started |
start / stop / restart commands | 1 day | Not started |
status command (health dashboard) | 1 day | Not started |
upgrade command (registry check, pull, migrate, rolling restart) | 3 days | Not started |
rollback command | 1 day | Not started |
backup / restore commands | 2 days | Not started |
logs command | 0.5 days | Not started |
config / secrets commands | 1 day | Not started |
doctor command (diagnostics) | 1 day | Not started |
| Secrets encryption at rest | 1 day | Not started |
| Test suite for CLI commands | 2 days | Not started |
Phase 3: Container Registry and CI/CD (1 week)
Goal: Automated image publishing on tagged releases.
| Task | Effort | Status |
|---|---|---|
| GitHub Actions workflow for building + pushing images | 1 day | Not started |
| Image versioning (semver tags + date stamps) | 0.5 days | Not started |
| Changelog generation (from commits/PRs) | 0.5 days | Not started |
| Air-gapped update bundle generation | 1 day | Not started |
| Customer registry token provisioning | 0.5 days | Not started |
Phase 4: VM Appliance (1-2 weeks)
Goal: A downloadable OVA/QCOW2 with the full stack pre-baked.
| Task | Effort | Status |
|---|---|---|
| Packer template for Ubuntu 22.04 | 2 days | Not started |
| Provisioning scripts (Docker, Node, CLI, images) | 1 day | Not started |
| First-boot web wizard | 3 days | Not started |
| OVA/VMDK/QCOW2/VHD export pipeline | 1 day | Not started |
| Security hardening (UFW, fail2ban, unattended-upgrades) | 0.5 days | Not started |
| VM build automation in CI | 1 day | Not started |
| Testing on VMware, Proxmox, Hyper-V | 2 days | Not started |
Phase 5: Documentation and Onboarding (1 week)
Goal: Customer-facing documentation and onboarding materials.
| Task | Effort | Status |
|---|---|---|
| Customer deployment guide (Docusaurus) | 2 days | Not started |
| Air-gapped deployment addendum | 1 day | Not started |
| CLI reference documentation | 1 day | Not started |
| Video walkthrough (setup to first conversion) | 1 day | Not started |
Total Estimated Effort
| Phase | Effort |
|---|---|
| Phase 1: Docker Compose stack | 2-3 weeks |
| Phase 2: Management CLI | 2-3 weeks |
| Phase 3: Registry + CI/CD | 1 week |
| Phase 4: VM Appliance | 1-2 weeks |
| Phase 5: Documentation | 1 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.