Self-Hosting

The Underlay protocol is an open specification. This repository is the reference implementation, but anyone can build an Underlay-compatible server tailored to their infrastructure, language, or use case, as long as it implements the protocol (content-addressed records, hash negotiation, immutable versioning). The protocol is the contract; the implementation is yours.

What follows is how to self-host this implementation. It ships with a self-contained Docker Compose setup that bundles everything you need: the app, auth server, PostgreSQL, S3-compatible storage, and a reverse proxy. One command, no external dependencies.

What gets deployed

  • Underlay app: the main application (API + web UI)
  • KF Auth: authentication server (OAuth2/OIDC) + account management UI
  • PostgreSQL 16: two databases, one for auth and one for the app
  • MinIO: S3-compatible object storage for file uploads (replaceable with external S3)
  • Caddy: reverse proxy with automatic TLS

On first boot, an init container auto-generates all secrets (session keys, OAuth client credentials, S3 credentials). No manual secret management required.

Requirements

  • Docker and Docker Compose (v2)
  • A server with at least 2 GB RAM and 10 GB disk
  • A domain name pointed at your server (for TLS), or localhost for local testing

Quick start

Clone the repo and run:

DOMAIN=https://your-domain.com docker compose -f docker-compose.withauth.yml up -d

That's it. Caddy handles TLS automatically via Let's Encrypt. Visit your domain to create your first account.

For local testing without a domain, omit DOMAIN. It defaults to http://localhost:

docker compose -f docker-compose.withauth.yml up -d

Configuration

Set environment variables in your shell or create a .env file next to the compose file. Only DOMAIN is required; everything else has sensible defaults or is auto-generated.

# Required
DOMAIN=https://your-domain.com

# Optional: email delivery (for password resets, invitations)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
[email protected]
SMTP_USER=apikey
SMTP_PASS=your-smtp-password

# Optional: social login providers
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
ORCID_CLIENT_ID=...
ORCID_CLIENT_SECRET=...

Social login

Without social login configured, users sign up and log in with email/password. To enable GitHub, Google, or ORCID login, set the corresponding client ID and secret. You'll need to register an OAuth app with each provider, using https://your-domain.com/auth/callback/<provider> as the callback URL.

Using external S3

The bundled MinIO service works out of the box, but you can replace it with any S3-compatible storage (AWS S3, Cloudflare R2, DigitalOcean Spaces, etc.):

# In docker-compose.withauth.yml, remove the minio and minio-init services,
# then add these to the app service's environment block:
S3_BUCKET: your-bucket-name
S3_REGION: us-east-1
S3_ENDPOINT: https://your-account-id.r2.cloudflarestorage.com  # omit for AWS S3
S3_ACCESS_KEY: your-access-key
S3_SECRET_KEY: your-secret-key

Also remove the minio-init service dependency from the app service. The S3_ENDPOINT variable is only needed for non-AWS providers; omit it for standard AWS S3.

Data and persistence

All state is in Docker volumes:

  • pgdata: PostgreSQL databases (auth + app)
  • minio-data: uploaded files (if using bundled MinIO)
  • withauth-config: auto-generated secrets and config (created once on first boot)
  • caddy-data: TLS certificates

To completely reset and start fresh, remove all volumes and re-run. The init container will regenerate secrets:

docker compose -f docker-compose.withauth.yml down -v
docker compose -f docker-compose.withauth.yml up -d

Updating

Pull new images and restart. The app runs database migrations automatically on startup. No manual migration step needed.

docker compose -f docker-compose.withauth.yml pull
docker compose -f docker-compose.withauth.yml up -d

Architecture

Caddy listens on ports 80 and 443 and routes requests by path:

  • /auth/* → KF Auth server (authentication, OAuth2)
  • /account/* → KF Auth account UI (profile, password, sessions)
  • Everything else → Underlay app (API at /api/*, web UI for all other paths)

The app server handles both the JSON API and server-side rendered React UI on a single port. All services communicate internally over a Docker network; only Caddy is exposed to the internet.

Source code

The self-hosting setup lives in the main repo:

  • docker-compose.withauth.yml: the compose file
  • selfhost/Caddyfile: Caddy reverse proxy config
  • selfhost/init-db.sh: Postgres init script (creates the app database)

Report issues at github.com/knowledgefutures/underlay. Built by Knowledge Futures, a 501(c)(3) public charity.