diff --git a/content/guides/self-hosting.mdx b/content/guides/self-hosting.mdx
index e54f7be..fafe6be 100644
--- a/content/guides/self-hosting.mdx
+++ b/content/guides/self-hosting.mdx
@@ -12,21 +12,71 @@ When you self-host the Happy Server, you get:
- **Team control** - Run one server for your whole team
- **Zero dependencies** - Never worry about a service shutting down
-The entire server is only 1,293 lines of typescript. You can read it yourself to
-verify it just forwards encrypted messages.
+The server is fully open source and part of the [Happy monorepo](https://github.com/slopus/happy).
+You can read the code yourself to verify it just forwards encrypted messages.
-## Quick Start
+## Quick Start (Easiest)
+
+The simplest way to self-host is with the `happy` CLI. No Docker, no database setup needed.
+
+
+### Install Happy
+
+```bash
+npm install -g happy
+```
+
+### Start the Server
+
+```bash
+happy server
+```
+
+On first start, this will:
+- Create a master secret in `~/.happy/server/master-secret`
+- Start an embedded database (PGlite) - no Postgres needed
+- Serve on `http://127.0.0.1:3005`
+- Write the server URL to your local Happy settings
+
+To expose the server on your local network (for mobile access):
+
+```bash
+happy server --host 0.0.0.0 --port 3005
+```
+
+### Configure Your Devices
+
+**On your phone:**
+1. Open Happy app
+2. Go to Settings
+3. Set "Relay Server URL" to `http://your-server:3005`
+4. Save
+
+**On your computer:**
+```bash
+export HAPPY_SERVER_URL="http://your-server:3005"
+```
+
+If you used `happy server`, the CLI automatically picks up the local server URL from your settings.
+
+
+
+That's it! Your Happy setup now runs through your own server.
+
+## Quick Start with Docker
+
+The standalone Docker image runs everything in a single container with no external dependencies (no Postgres, no Redis, no S3).
### Clone and Build
```bash
# Get the code
-git clone https://github.com/slopus/happy-server
-cd happy-server
+git clone https://github.com/slopus/happy
+cd happy
# Build with Docker
-docker build -t happy-server:latest .
+docker build -t happy-server -f Dockerfile.server .
```
### Run the Server
@@ -34,39 +84,61 @@ docker build -t happy-server:latest .
```bash
docker run -d \
--name happy-server \
- -p 3000:3000 \
- -e NODE_ENV=production \
- -e DATABASE_URL="postgresql://postgres:postgres@localhost:5432/happy-server" \
- -e REDIS_URL="redis://localhost:6379" \
- -e SEED="your-seed-for-token-generation" \
- -e PORT=3005 \
+ -p 3005:3005 \
+ -e HANDY_MASTER_SECRET="$(openssl rand -hex 32)" \
+ -v happy-data:/data \
--restart unless-stopped \
- happy-server:latest
+ happy-server
```
-**Important**: Replace the environment variable values with your actual configuration:
-- `DATABASE_URL`: Your PostgreSQL connection string
-- `REDIS_URL`: Your Redis connection string
-- `SEED`: A secure random seed for token generation
-- `PORT`: The port the application listens on (3005)
+This uses:
+- **PGlite** - an embedded PostgreSQL database (data stored in `/data/pglite`)
+- **Local filesystem** - for file uploads (stored in `/data/files`)
+- **In-memory event bus** - no Redis needed
+
+Data persists in the `happy-data` Docker volume across container restarts.
### Configure Your Devices
**On your phone:**
1. Open Happy app
2. Go to Settings
-3. Set "Relay Server URL" to `http://your-server:3000`
+3. Set "Relay Server URL" to `http://your-server:3005`
4. Save
**On your computer:**
```bash
-export HAPPY_SERVER_URL="http://your-server:3000"
-# Configure your CLI to use your server
+export HAPPY_SERVER_URL="http://your-server:3005"
```
-That's it! Your Happy setup now runs through your own server.
+### Environment Variables
+
+| Variable | Required | Default | Description |
+|----------|----------|---------|-------------|
+| `HANDY_MASTER_SECRET` | Yes | - | Master secret for auth and encryption |
+| `PORT` | No | `3005` | Server port |
+| `PUBLIC_URL` | No | `http://localhost:3005` | Public base URL for file URLs sent to clients |
+| `DATA_DIR` | No | `/data` | Base data directory |
+| `PGLITE_DIR` | No | `/data/pglite` | PGlite database directory |
+| `NODE_ENV` | No | `production` | Environment mode |
+
+### Optional: External Services
+
+For larger deployments, you can use external Postgres or Redis instead of the embedded defaults:
+
+| Variable | Description |
+|----------|-------------|
+| `DATABASE_URL` | PostgreSQL connection URL (bypasses PGlite) |
+| `REDIS_URL` | Redis connection URL (enables cross-process pub/sub) |
+| `S3_HOST` | S3/MinIO host (bypasses local file storage) |
+| `S3_PORT` | S3 port |
+| `S3_ACCESS_KEY` | S3 access key |
+| `S3_SECRET_KEY` | S3 secret key |
+| `S3_BUCKET` | S3 bucket name |
+| `S3_PUBLIC_URL` | Public URL for S3 assets |
+| `S3_USE_SSL` | Whether to use SSL for S3 (`true`/`false`, default `true`) |
## Production Setup with HTTPS
@@ -83,7 +155,7 @@ sudo apt install caddy
# Configure reverse proxy
sudo tee /etc/caddy/Caddyfile </attachments/.enc`. Two bucket-level settings should be applied once at deploy time:
+
+**1. Lifecycle rule for attachment TTL.** Add a lifecycle rule so objects age out automatically. Pick a TTL that matches your retention policy (30 days is a reasonable default).
+
+```bash
+# AWS CLI
+aws s3api put-bucket-lifecycle-configuration --bucket happy-blobs \
+ --lifecycle-configuration '{
+ "Rules": [{
+ "ID": "session-attachments-ttl",
+ "Status": "Enabled",
+ "Filter": { "Prefix": "sessions/" },
+ "Expiration": { "Days": 30 }
+ }]
+ }'
+
+# MinIO
+mc ilm rule add myminio/happy-blobs \
+ --expire-days 30 \
+ --prefix "sessions/"
+```
+
+**2. Server-side encryption (defense-in-depth).** Blobs are already end-to-end encrypted by the client, but enabling AES-256 SSE on the bucket adds an extra layer of protection.
+
+```bash
+# AWS CLI
+aws s3api put-bucket-encryption --bucket happy-blobs \
+ --server-side-encryption-configuration '{
+ "Rules": [{
+ "ApplyServerSideEncryptionByDefault": {
+ "SSEAlgorithm": "AES256"
+ }
+ }]
+ }'
+
+# MinIO
+mc encrypt set sse-s3 myminio/happy-blobs
+```
+
+When using local storage mode (no `S3_HOST`), blobs are written under `/files/sessions//attachments/`. Clean up old session directories on a cron if you want a TTL.
## Security Notes
@@ -291,11 +428,6 @@ spec:
type: ClusterIP
```
-**Configuration Notes**:
-- All ports now consistently use 3005 (the application's actual listening port)
-- Uses the `happy-secrets` secret created in the previous section
-- Update the image name to match your actual container registry
-
## Kubernetes Secrets
Create a secret file for your environment variables:
@@ -308,11 +440,12 @@ metadata:
name: happy-secrets
type: Opaque
stringData:
- DATABASE_URL: "postgresql://postgres:postgres@postgres-service:5432/happy-server"
- REDIS_URL: "redis://redis-service:6379"
- SEED: "your-secure-seed-for-token-generation"
+ HANDY_MASTER_SECRET: "replace-with-a-strong-random-secret"
PORT: "3005"
NODE_ENV: "production"
+ # Add these if using external Postgres/Redis:
+ # DATABASE_URL: "postgresql://postgres:postgres@postgres-service:5432/happy"
+ # REDIS_URL: "redis://redis-service:6379"
```
Apply the secret:
@@ -321,7 +454,7 @@ kubectl apply -f happy-secrets.yaml
```
**Security Note**: Replace the values with your actual configuration. For production:
-- Use a strong, random SEED value
+- Use a strong, random `HANDY_MASTER_SECRET` value (e.g., `openssl rand -hex 32`)
- Use proper database credentials
- Consider using external secret management tools like external-secrets-operator with Vault
@@ -333,4 +466,4 @@ After setting up your server:
2. [Set up monitoring](/guides/monitoring)
3. [Enable voice coding](/docs/features/voice-coding-with-claude-code)
-Running your own Happy Server takes 3 minutes and gives you permanent control over your Claude Code mobile access. No subscriptions, no lock-in, no surprises.
\ No newline at end of file
+Running your own Happy Server takes 3 minutes and gives you permanent control over your Claude Code mobile access. No subscriptions, no lock-in, no surprises.