Self-Hosting
Deploy Keyway on your own infrastructure with Docker Compose. Your encryption keys, your servers, your rules.
Prerequisites
- Docker and Docker Compose v2+
- A domain name with DNS access (e.g.,
example.com) - A GitHub App (created below)
- Ports 80 and 443 open for HTTPS
Quick Start
# 1. Clone the repo
git clone https://github.com/keywaysh/keyway.git
cd keyway
# 2. Configure
cp .env.example .env
# Edit .env — fill in DOMAIN, ENCRYPTION_KEY, JWT_SECRET, and GitHub App values
# 3. Deploy
docker compose up --build -d
Your instance will be available at:
- Dashboard:
https://app.your-domain.com - API:
https://api.your-domain.com
Step 1: DNS Setup
Create DNS A records pointing to your server:
| Record | Type | Value |
|---|---|---|
example.com | A | <your-server-ip> |
app.example.com | A | <your-server-ip> |
api.example.com | A | <your-server-ip> |
Caddy automatically provisions Let's Encrypt TLS certificates for these domains.
Step 2: Create a GitHub App
Keyway requires a GitHub App for authentication and repository access verification.
-
Go to https://github.com/settings/apps/new (or your GitHub Enterprise instance)
-
Fill in the form:
Field Value App name keyway(or any unique name)Homepage URL https://example.comCallback URL https://api.example.com/v1/auth/callbackRequest user authorization during installation Checked Enable Device Flow Checked Webhook URL https://api.example.com/v1/github/webhook(optional)Webhook secret Generate a random string (optional) -
Set permissions:
Scope Permission Repository: Metadata Read-only Repository: Administration Read-only Account: Email addresses Read-only -
Subscribe to events (optional, for real-time sync):
- Installation
- Installation repositories
-
After creating, note down:
- App ID (visible on the app settings page)
- Client ID
- Client Secret (generate one)
- Private Key (generate and download the
.pemfile)
-
Base64-encode the private key for the
.envfile:cat your-app.private-key.pem | base64 | tr -d '\n'
Step 3: Configure Environment
Copy .env.example to .env and fill in the required values:
cp .env.example .env
Required Variables
# Your domain
DOMAIN=example.com
# Encryption key (CANNOT be changed after secrets are stored)
ENCRYPTION_KEY= # openssl rand -hex 32
# JWT secret
JWT_SECRET= # openssl rand -base64 32
# GitHub App credentials (from Step 2)
GITHUB_APP_ID=123456
GITHUB_APP_CLIENT_ID=Iv1.abc123
GITHUB_APP_CLIENT_SECRET=abc123...
GITHUB_APP_PRIVATE_KEY=base64encodedkey...
GITHUB_APP_NAME=keyway
make setup can auto-generate ENCRYPTION_KEY and JWT_SECRET for you.
URL Defaults
All URLs are derived from DOMAIN automatically:
| Service | URL |
|---|---|
| Dashboard | https://app.<DOMAIN> |
| API | https://api.<DOMAIN> |
Override individual URLs if needed:
DASHBOARD_URL=https://custom-dashboard.example.com
ALLOWED_ORIGINS=https://custom-dashboard.example.com
Step 4: Deploy
docker compose up --build -d
This starts all services:
| Service | Description | Port |
|---|---|---|
| db | PostgreSQL 16 | 5432 |
| crypto | AES-256-GCM encryption gRPC service | 50051 |
| backend | Fastify API | 8080 |
| dashboard | Next.js dashboard | 3000 |
| caddy | Reverse proxy with auto-HTTPS | 80, 443 |
Database migrations run automatically on backend startup.
Verify the deployment
# Check all services are healthy
docker compose ps
# Check backend logs
docker compose logs backend
# Test the API
curl https://api.example.com/health
Step 5: Connect the CLI
Install the Keyway CLI and point it at your instance:
# Install
brew install keywaysh/tap/keyway
# Set your API URL
export KEYWAY_API_URL=https://api.example.com
# Login
keyway login
# Use it
keyway init
keyway push
keyway pull
Add KEYWAY_API_URL to your shell profile (~/.bashrc, ~/.zshrc) to persist it.
Architecture
┌──────────┐
│ Caddy │ :80, :443
│(reverse │ Auto HTTPS via Let's Encrypt
│ proxy) │
└────┬─────┘
│
┌─────────┴─────────┐
│ │
┌─────▼──────┐ ┌─────▼──────┐
│ Dashboard │ │ Backend │
│ (Next.js) │ │ (Fastify) │
│ :3000 │ │ :8080 │
└────────────┘ └──┬───┬─────┘
│ │
┌────▼┐ ┌▼────────┐
│ DB │ │ Crypto │
│(PG) │ │ (gRPC) │
│:5432│ │ :50051 │
└─────┘ └──────────┘
- Caddy handles TLS termination and reverse proxying
- Backend runs database migrations on startup, handles all API logic
- Crypto is an isolated gRPC service that holds the encryption key
- PostgreSQL stores users, vaults, encrypted secrets, and audit logs
Optional Services
All third-party services are optional. Without them, Keyway works with full core functionality. Leave their env vars empty to disable them.
Billing (Stripe)
Controls plan limits (free vs pro vs team). Disabled by default — all users get unlimited access.
BILLING_ENABLED=true
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
Email (Resend)
Sends welcome emails and security alerts (new device login, impossible travel).
RESEND_API_KEY=re_...
EMAIL_FROM_ADDRESS=hello@mail.example.com
EMAIL_FROM_NAME=Keyway
Without this, no emails are sent. Users can still authenticate and use all features.
Analytics (PostHog)
Usage metrics. Never tracks secret values.
# Server-side (backend)
POSTHOG_API_KEY=phc_...
POSTHOG_HOST=https://app.posthog.com
# Client-side (dashboard build args)
NEXT_PUBLIC_POSTHOG_KEY=phc_...
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
Error Tracking (Sentry)
SENTRY_DSN=https://...@sentry.io/...
NEXT_PUBLIC_SENTRY_DSN=https://...@sentry.io/...
IP Geolocation (ipinfo.io)
Enables security features like impossible travel detection and new location alerts.
IPINFO_TOKEN=abc123
Provider Integrations (Vercel/Netlify)
Enables syncing secrets to deployment platforms.
VERCEL_CLIENT_ID=...
VERCEL_CLIENT_SECRET=...
NETLIFY_CLIENT_ID=...
NETLIFY_CLIENT_SECRET=...
GitHub Enterprise
To use GitHub Enterprise Server instead of GitHub.com:
GITHUB_URL=https://github.example.com
GITHUB_BASE_URL=https://github.example.com/api/v3
The CLI also supports this:
export KEYWAY_GITHUB_URL=https://github.example.com
export KEYWAY_GITHUB_API_URL=https://github.example.com/api/v3
GitHub Action
Use the Keyway GitHub Action in CI workflows with your self-hosted instance:
- uses: keywaysh/keyway-action@v1
with:
api-url: https://api.example.com
token: ${{ secrets.KEYWAY_TOKEN }}
MCP Server
The MCP server for AI assistants works with self-hosted instances:
KEYWAY_API_URL=https://api.example.com npx @keywaysh/mcp
Local Development
For local development with keyway.local domains and mkcert certificates:
# Run setup (configures /etc/hosts and generates certs)
make setup
# Use the local Caddyfile
CADDYFILE=./Caddyfile docker compose up --build
This gives you:
https://app.keyway.local(dashboard)https://api.keyway.local(API)
Alternatively, run services directly without Docker:
make dev # Starts crypto, backend, and dashboard
make dev-backend # Backend only
make dev-dashboard # Dashboard only
Upgrading
git pull
docker compose up --build -d
Database migrations run automatically on backend startup.
Troubleshooting
Backend won't start
Check that the database is healthy and the encryption key is set:
docker compose logs db
docker compose logs crypto
docker compose logs backend
TLS certificate issues
Caddy needs ports 80 and 443 open for the ACME challenge:
docker compose logs caddy
For local development, use CADDYFILE=./Caddyfile with mkcert certificates instead.
Cookie/logout issues on custom domain
The dashboard derives the cookie domain from the browser hostname. Verify that the API and dashboard share a common parent domain.
CLI can't connect
Ensure KEYWAY_API_URL is set and the API is reachable:
export KEYWAY_API_URL=https://api.example.com
curl $KEYWAY_API_URL/health
keyway login
GitHub App callback fails
Verify the callback URL in your GitHub App settings matches exactly:
https://api.example.com/v1/auth/callback