Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
80e5ad9
docs: design stateless production deployments
beubax Jun 10, 2026
f4a6cf5
feat: add production backend config
beubax Jun 10, 2026
a87328f
fix: lazy load postgres driver
beubax Jun 10, 2026
7a39de6
fix: restore sqlite store branch
beubax Jun 10, 2026
8a817d9
feat: add store migrations and postgres pooling
beubax Jun 10, 2026
9c141f3
fix: bind store transactions to pooled connection
beubax Jun 10, 2026
fc3189b
refactor: move pop replay cache to server
beubax Jun 10, 2026
c68de27
fix: preserve pop replay auth errors
beubax Jun 10, 2026
604d18e
feat: add auth session store contract
beubax Jun 10, 2026
60e21e1
fix: clean redis auth session indexes
beubax Jun 10, 2026
0ea30dc
refactor: split pending claim storage
beubax Jun 10, 2026
b754b80
fix: atomically consume redis pending claims
beubax Jun 10, 2026
d828d2b
feat: select redis runtime state
beubax Jun 10, 2026
448c8b2
fix: clean up failed runtime startup
beubax Jun 10, 2026
ea0fd94
fix: close redis client on ping failure
beubax Jun 10, 2026
3b8cfd0
feat: add root health check
beubax Jun 10, 2026
5457d10
fix: remove duplicate health route
beubax Jun 10, 2026
caac894
test: add gated redis runtime tests
beubax Jun 10, 2026
ea4eaa7
docs: add production self-hosting path
beubax Jun 10, 2026
5a15e41
fix: avoid hardcoded compose secrets
beubax Jun 10, 2026
ee25d5c
fix: clarify compose master key secret
beubax Jun 10, 2026
69b263b
fix: exclude local build artifacts from package
beubax Jun 10, 2026
b374fb4
fix: address production readiness review
beubax Jun 10, 2026
ff3d542
fix: harden container startup path
beubax Jun 10, 2026
d459556
Merge branch 'main' into feature/stateless-production-deployments
beubax Jun 12, 2026
be5c646
fix: require production backend URLs
beubax Jun 12, 2026
eb37f84
fix: hide optional asyncpg import from ty
beubax Jun 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ __pycache__
*.egg-info
dist/
.venv/
.venv*/
.uv/

# Node build artefacts
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ activemq-data/
.env
.envrc
.venv
.venv*/
env/
venv/
ENV/
Expand Down Expand Up @@ -209,6 +210,11 @@ cython_debug/
# Generated static dashboard bundle — run scripts/build-ui.sh before uv build
src/authsome/ui/web/*

# Local UI dependency installs and builds
ui/node_modules/
ui/.next/
ui/out/

# PyPI configuration file
.pypirc

Expand Down
15 changes: 11 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,25 @@ RUN mkdir -p src/authsome/ui/web && \
FROM python:3.13-slim AS runtime

RUN groupadd -r authsome && \
useradd -r -g authsome -d /home/authsome -m -s /sbin/nologin authsome
useradd -r -g authsome -d /home/authsome -m -s /sbin/nologin authsome && \
mkdir -p /data/authsome && \
chown -R authsome:authsome /data/authsome

COPY --from=py-builder /dist /dist
RUN pip install --no-cache-dir /dist/*.whl && rm -rf /dist
COPY --from=ghcr.io/astral-sh/uv:python3.13-bookworm-slim /usr/local/bin/uv /usr/local/bin/uv
RUN wheel="$(find /dist -maxdepth 1 -name '*.whl' -print -quit)" && \
test -n "$wheel" && \
uv pip install --system --no-cache "${wheel}[postgres,redis]" && \
rm -rf /dist

ENV AUTHSOME_HOME=/data/authsome

EXPOSE 7998

VOLUME ["/data/authsome"]
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:7998/health', timeout=3).read()"]

USER authsome

ENTRYPOINT ["authsome", "daemon", "serve"]
ENTRYPOINT ["authsome", "--log-file", "", "daemon", "serve"]
CMD ["--host", "0.0.0.0", "--port", "7998"]
65 changes: 54 additions & 11 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,71 @@
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${AUTHSOME_POSTGRES_DB:-authsome}
POSTGRES_USER: ${AUTHSOME_POSTGRES_USER:-authsome}
POSTGRES_PASSWORD: ${AUTHSOME_POSTGRES_PASSWORD:?set AUTHSOME_POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${AUTHSOME_POSTGRES_USER:-authsome} -d ${AUTHSOME_POSTGRES_DB:-authsome}",
]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s

redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s

authsome:
build: .
image: authsome:latest
restart: unless-stopped
ports:
- "7998:7998"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- authsome-data:/data/authsome
environment:
AUTHSOME_HOST: 0.0.0.0
AUTHSOME_PORT: "7998"
AUTHSOME_HOME: /data/authsome
# Set this to the public URL of this server so OAuth callbacks resolve correctly.
# Example: AUTHSOME_SERVER_BASE_URL: https://auth.example.com
AUTHSOME_SERVER_BASE_URL: ""
# Encryption mode: "local_key" (default) or "keyring"
AUTHSOME_ENCRYPTION_MODE: local_key
AUTHSOME_LOG_LEVEL: info
AUTHSOME_ENV: prod
AUTHSOME_BASE_URL: ${AUTHSOME_BASE_URL:-http://localhost:7998}
AUTHSOME_DATABASE_URL: postgresql://${AUTHSOME_POSTGRES_USER:-authsome}:${AUTHSOME_POSTGRES_PASSWORD:?set AUTHSOME_POSTGRES_PASSWORD}@postgres:5432/${AUTHSOME_POSTGRES_DB:-authsome}
AUTHSOME_REDIS_URL: redis://redis:6379/0
AUTHSOME_DO_NOT_TRACK: "1"
# Set AUTHSOME_MASTER_KEY from your platform secret store before production use.
# AUTHSOME_MASTER_KEY_FILE remains available for platforms that mount a secret file into the container.
AUTHSOME_MASTER_KEY: ${AUTHSOME_MASTER_KEY:?set AUTHSOME_MASTER_KEY}
# Must be identical on every replica because browser sessions are stateless signed JWTs.
AUTHSOME_UI_SESSION_KEY: ${AUTHSOME_UI_SESSION_KEY:?set AUTHSOME_UI_SESSION_KEY}
# Uncomment to use a pre-built image from a registry instead of building locally:
# image: ghcr.io/agentrhq/authsome:latest

# ── Optional: Caddy reverse proxy for TLS ─────────────────────────
# Uncomment the block below and add a caddy service to terminate TLS.
# labels:
# caddy: auth.example.com
# caddy.reverse_proxy: "{{upstreams 7998}}"
# The authsome home volume is retained for logs and fallback key material.
# Postgres and Redis carry the primary production state.

volumes:
authsome-data:
postgres-data:
redis-data:
174 changes: 84 additions & 90 deletions docs/guides/self-hosting.md
Original file line number Diff line number Diff line change
@@ -1,125 +1,119 @@
# Self-hosting Authsome

Run a persistent Authsome daemon in a container — useful for CI runners, shared agent hosts, or any environment where installing Python tooling is inconvenient.
Run Authsome as a production service with Postgres for the server registries and Redis for shared runtime state plus the encrypted vault raw KV layer.

## Quick start

```bash
# Clone the repo (or copy docker-compose.yml)
git clone https://github.com/agentrhq/authsome.git
cd authsome
The repository ships a compose file that wires the daemon to Postgres and Redis. Set a stable master key source first, then bring the stack up and verify the root health check.

# Start the daemon
```bash
export AUTHSOME_POSTGRES_PASSWORD='change-me-to-a-long-random-password'
export AUTHSOME_MASTER_KEY='base64-encoded-32-byte-key'
export AUTHSOME_UI_SESSION_KEY='base64-encoded-32-byte-key'
docker compose up -d

# Verify it's running
curl http://localhost:7998/health
```

The daemon is now available at `http://localhost:7998`.
The daemon should answer on `http://localhost:7998`. The root `/health` endpoint is the container health target used by the image and by `docker compose`.
The included compose file reads `AUTHSOME_MASTER_KEY` and `AUTHSOME_UI_SESSION_KEY` from the host environment. The `_FILE` variants are supported by Authsome itself, but if you want to use file-mounted secrets you must add those mounts and pass the file paths yourself in a custom compose file.

Point agents at it by setting:
## What this deployment does

```bash
export AUTHSOME_BASE_URL=http://localhost:7998
```
- Postgres stores the relational server registries: identities, principals, vaults, claims, and bindings.
- Redis stores shared runtime state and, when configured, backs the raw KV layer that holds encrypted vault blobs.
- The Authsome container keeps only a small home directory for logs and optional fallback key material. Primary production state lives in Postgres and Redis.
- Browser sessions remain stateless signed cookies for now, so every replica must use the same `AUTHSOME_UI_SESSION_KEY`. Any future stateful browser session store is tracked separately.

## Prerequisites

- Docker and Docker Compose v2.
- Postgres 16.
- Redis 7.
- Stable `AUTHSOME_MASTER_KEY` and `AUTHSOME_UI_SESSION_KEY` values for the included compose file.

## Environment variables
Do not commit production secrets. Use your platform secret store or Docker secrets for the included compose file. If you prefer `_FILE` variables, wire up your own secret mounts and file paths in a custom compose file.

## Required environment variables

| Variable | Default | Description |
|---|---|---|
| `AUTHSOME_HOME` | `/data/authsome` | Root directory for credentials, keys, and the database |
| `AUTHSOME_HOST` | `0.0.0.0` | Interface the daemon binds to inside the container |
| `AUTHSOME_PORT` | `7998` | TCP port |
| `AUTHSOME_BASE_URL` | _(derived from host:port)_ | Public URL used to build OAuth callback URLs. **Must be set when behind a reverse proxy.** On client machines, set this to point the CLI at a remote daemon. |
| `AUTHSOME_ENCRYPTION_MODE` | `local_key` | `local_key` stores the master key on disk; `keyring` uses the OS keyring (not available in containers) |
| `AUTHSOME_LOG_LEVEL` | `info` | Uvicorn log level (`debug`, `info`, `warning`, `error`) |
| `AUTHSOME_ANALYTICS` | `1` | Set to `0` to disable telemetry |
| `AUTHSOME_ENV` | `dev` | Runtime mode. Set to `prod` for production deployments; in `prod`, `AUTHSOME_DATABASE_URL` and `AUTHSOME_REDIS_URL` are required. |
| `AUTHSOME_DATABASE_URL` | none | Postgres DSN for the daemon-owned registries. The compose file points this at the bundled Postgres service. |
| `AUTHSOME_REDIS_URL` | none | Redis URL for shared runtime state and the encrypted vault raw KV backend. |
| `AUTHSOME_POSTGRES_PASSWORD` | none | Required password used by the bundled Postgres service and the daemon's database URL. |
| `AUTHSOME_POSTGRES_USER` | `authsome` | Postgres role name used by the bundled compose file. |
| `AUTHSOME_POSTGRES_DB` | `authsome` | Postgres database name used by the bundled compose file. |
| `AUTHSOME_MASTER_KEY` | none | Base64-encoded 32-byte master key. Highest priority when set. |
| `AUTHSOME_MASTER_KEY_FILE` | none | Advanced alternative for custom compose or platform-secret setups where you mount a file into the container and point Authsome at that path yourself. |
| `AUTHSOME_UI_SESSION_KEY` | none | Shared signing secret for stateless browser session JWTs. Must be identical on every replica. |
| `AUTHSOME_UI_SESSION_KEY_FILE` | none | Advanced alternative for custom compose or platform-secret setups where you mount the UI session key into the container. |
| `AUTHSOME_BASE_URL` | `http://localhost:7998` | Public daemon URL used to build OAuth callback URLs. Set this to the reverse-proxy URL in production. |
| `AUTHSOME_HOME` | `/data/authsome` | Home directory for logs, generated fallback secrets, and other daemon-local files. |
| `AUTHSOME_HOST` | `0.0.0.0` | Host interface the daemon binds to inside the container. |
| `AUTHSOME_PORT` | `7998` | TCP port the daemon listens on. |
| `AUTHSOME_DO_NOT_TRACK` | `1` | Set to `0` only if you intentionally want telemetry enabled. |
| `AUTHSOME_POSTHOG_API_KEY` | none | Enables PostHog analytics when present and telemetry is not opted out. |
| `AUTHSOME_POSTHOG_HOST` | `https://us.i.posthog.com` | Override the PostHog ingestion host if needed. |

## Volume
The daemon still accepts the legacy `DATABASE_URL` alias, but production deployments should set `AUTHSOME_DATABASE_URL`. The included compose file sets `AUTHSOME_ENV=prod`, which makes the Postgres and Redis URLs mandatory at startup.
The included compose file hard-requires `AUTHSOME_MASTER_KEY` and `AUTHSOME_UI_SESSION_KEY` from the host environment; it does not mount secret files or pass `_FILE` paths for you.

All credentials and keys live at `AUTHSOME_HOME` (`/data/authsome` by default), which is declared as a Docker named volume.
## Secret resolution

```
/data/authsome/
server/
authsome.db # SQLite database (identities, principals, vaults)
master.key # Vault encryption key — back this up
kv_store/ # Encrypted credential blobs
client/
logs/
```
On startup, Authsome resolves the master key and UI session signing key in this order:

> **Keep `master.key` safe.** Without it, stored credentials cannot be decrypted.
1. `AUTHSOME_MASTER_KEY`
2. `AUTHSOME_MASTER_KEY_FILE`, or the default server key file at `AUTHSOME_HOME/server/master.key`
3. The OS keyring entry
4. A generated fallback, stored in the keyring when possible, otherwise written to the default server key file

## Upgrading
`AUTHSOME_MASTER_KEY` and `AUTHSOME_UI_SESSION_KEY` are the strongest and cleanest production options for the included compose file because they avoid writing secret material to disk. If you use `_FILE` variables, mount them read-only, point Authsome at the mounted paths, and treat that as a custom compose setup rather than the out-of-the-box quick start.

```bash
docker compose pull # fetch the latest image
docker compose up -d # restart with zero downtime (data volume is preserved)
```
## Compose layout

`docker-compose.yml` runs three services:

- `authsome` exposes port `7998`, mounts `authsome-data` for logs and fallback secret material, and points the daemon at the internal Postgres and Redis services.
- `postgres` uses `postgres:16-alpine` with a health check and the `postgres-data` volume.
- `redis` uses `redis:7-alpine`, enables append-only persistence, and stores data in the `redis-data` volume.

The `authsome` service depends on healthy Postgres and Redis before startup. If either backend is missing, unreachable, or the optional Python driver is not installed, the daemon fails fast during boot.

## Startup and migrations

Authsome applies relational schema migrations at startup before it serves traffic. That means the daemon must be able to reach Postgres and Redis on boot. In production, treat a failing container start as a dependency or secret problem, not a warning to ignore.

When Postgres or Redis is configured but unreachable, startup aborts. If the image was built without the matching optional extras, startup also aborts because the required drivers are missing.

## Backup and restore

```bash
# Backup
docker run --rm -v authsome-data:/data/authsome -v $(pwd):/backup \
busybox tar czf /backup/authsome-backup.tar.gz -C /data/authsome .
Back up these pieces together:

# Restore
docker run --rm -v authsome-data:/data/authsome -v $(pwd):/backup \
busybox tar xzf /backup/authsome-backup.tar.gz -C /data/authsome
```
- Postgres data, because it stores the server registries.
- Redis persistence, if you enable or rely on it for encrypted vault blobs or shared runtime state.
- The master key or key file, because encrypted vault data cannot be decrypted without it.
- The UI session key or key file, because stateless browser session JWTs cannot be verified consistently across replicas without it.
- The `authsome-data` volume only if you want daemon logs or a fallback key file to survive container replacement.

## TLS with Caddy

Add a Caddy sidecar to the compose file for automatic HTTPS:

```yaml
services:
authsome:
image: authsome:latest
restart: unless-stopped
expose:
- "7998"
environment:
AUTHSOME_BASE_URL: https://auth.example.com
volumes:
- authsome-data:/data/authsome

caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
depends_on:
- authsome

volumes:
authsome-data:
caddy-data:
```
Browser sessions remain stateless signed cookies for now, so there is no separate session database to back up yet.

`Caddyfile`:
For restores, bring back the master key first, then restore Postgres and Redis, then start the daemon.

```
auth.example.com {
reverse_proxy authsome:7998
}
```
## Upgrades

## Building the image locally
Pull the new image, restart the stack, and watch the health endpoint until it responds:

```bash
docker build -t authsome:local .
docker compose pull
docker compose up -d
curl http://localhost:7998/health
```

The build is multi-stage:
Because schema migrations run at startup, keep the Postgres and Redis services healthy during the upgrade. If the daemon restarts, re-check `/health` after the migration pass completes.

## Example production notes

1. **`ui-builder`** — Node 24 + pnpm compiles the Next.js dashboard to static HTML.
2. **`py-builder`** — uv bundles the Python package (including the built UI) into a wheel.
3. **`runtime`** — Slim Python 3.13 image; installs the wheel, runs as a non-root `authsome` user.
- Use your platform secret store for `AUTHSOME_MASTER_KEY` and `AUTHSOME_UI_SESSION_KEY`. Only switch to `_FILE` variables if you have added real secret mounts and file paths to your own compose file.
- Set `AUTHSOME_BASE_URL` to the public URL behind your reverse proxy.
- Keep `AUTHSOME_HOME` mounted only if you want local logs or fallback key material to persist.
- Consider pointing `AUTHSOME_POSTHOG_API_KEY` at a real analytics key only if you have opted in to telemetry.
Loading
Loading