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
localhostfor local testing
Quick start
Clone the repo and run:
DOMAIN=https://your-domain.com docker compose -f docker-compose.withauth.yml up -dThat'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 -dConfiguration
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-keyAlso 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 -dUpdating
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 -dArchitecture
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 fileselfhost/Caddyfile: Caddy reverse proxy configselfhost/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.