Self-host xpntl
The whole platform on one machine with Docker Compose: the API (REST, GraphQL, MCP, WebSocket sync and the embedded worker), the web app, Postgres, and an Azure-Blob-compatible attachment store.
Self-hosting is free. The code is source-available under BUSL-1.1, and production use is covered by a free-tier license key at no cost. That is a license term rather than a switch in the software: nothing here checks for a key.
You build the images yourself. We do not publish a container image today, so the first run compiles the API and web images from the source in this repository. That takes a few minutes rather than a few seconds, and it means you are running exactly the code you can read.
Prerequisites
- Docker Engine 24 or newer with the Compose plugin (
docker compose version) - Around 2 GB of free RAM for the stack
Quickstart
git clone https://github.com/xpntl/xpntl.git
cd xpntl/infra/docker-compose/self-host
cp .env.example .env
# Set the five required secrets in .env:
# POSTGRES_PASSWORD, JWT_SECRET, SESSION_SECRET,
# OAUTH_TICKET_SECRET, LOOP_CREDENTIAL_SECRET
# Generate each one with:
openssl rand -hex 32
docker compose up --build -d
The first run builds the images, applies every migration, and seeds the system
workflow states through a one-shot migrate service. When it is up:
- Web app:
http://localhost:8080 - API health:
http://localhost:4000/v1/health
Create the first account through the web app's sign-up flow.
What runs
Six services, not one container:
| Service | Image | Purpose |
|---|---|---|
db | postgres:16-alpine | Application database |
blob | azurite | Attachment storage, Azure-Blob-compatible |
migrate | built from source | One-shot: migrations, then seeds system states and exits |
api | built from source | REST, GraphQL, MCP, WebSocket sync, worker ticks |
web | built from source (nginx) | The single-page app |
mailpit | axllent/mailpit | Optional local SMTP and inbox, profile mail |
Bring your own SMTP for real mail: set SMTP_HOST, SMTP_PORT
and SMTP_FROM in .env. To try email flows locally, start the
bundled Mailpit instead:
docker compose --profile mail up -d
Leave SMTP_HOST=mailpit and SMTP_PORT=1025, then open the
inbox at http://localhost:8025.
Single sign-on
Optional, and free. Sign-in buttons stay hidden until you set a provider's client id
and secret in .env: GOOGLE_*, GITHUB_* or
MICROSOFT_*. There is no plan gate on SSO. SAML is on the roadmap rather
than shipped, and it will be free on every plan when it lands.
Running behind a domain, with TLS
The API and web ports serve plain HTTP. For a real deployment, terminate TLS at a
reverse proxy (Caddy, nginx, Traefik) in front of port 8080 for the web
app and 4000 for the API, then set PUBLIC_WEB_URL and
PUBLIC_API_URL to your public HTTPS addresses.
The web image bakes the API URL in at build time. If you change
PUBLIC_API_URL you have to rebuild it:
docker compose build web && docker compose up -d web
Upgrades
git pull
docker compose up --build -d
The migrate service re-runs on every up and applies new
migrations before the API starts. Migrations are expand, migrate and contract safe,
so the API and the schema can differ by one release without breaking.
Backups
Two named volumes hold all state: xpntl_pgdata for the database and
xpntl_blobdata for attachments. Back both up together, because an
attachment row without its bytes is not a restore.
docker compose exec db pg_dump -U xpntl xpntl > backup-$(date +%F).sql
docker run --rm -v xpntl_blobdata:/data -v "$PWD":/out alpine \
tar czf /out/blob-$(date +%F).tgz -C /data .
Teardown
docker compose down # stop, keep the data
docker compose down -v # stop and DELETE all data
Kubernetes
A Helm chart lives at infra/helm/xpntl. Note that its default image
repositories point at a registry we do not publish to yet, so you will need to build
and push the API and web images to your own registry and set
image.api.repository and image.web.repository accordingly.