From 21fb119be8d5dcc5bab8da7ea6b0bc27e479a3c9 Mon Sep 17 00:00:00 2001 From: hailey Date: Mon, 27 Oct 2025 15:54:49 +0000 Subject: [PATCH] add docker-compose, caddy (#35) * add docker compose, caddy * tweaks * more tweaks * tweak * fix gh action --- .env.example | 2 +- .github/workflows/docker-image.yml | 4 ++-- .gitignore | 2 ++ Caddyfile | 10 ++++++++++ README.md | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- cmd/cocoon/main.go | 65 ++++++++++++++++++++++++++--------------------------------------- create-initial-invite.sh | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yaml | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ init-keys.sh | 34 ++++++++++++++++++++++++++++++++++ 9 file(s) changed, 388 insertion(s)(+), 43 deletion(s)(-) diff --git a/.env.example b/.env.example --- a/.env.example +++ b/.env.example @@ -6,5 +6,5 @@ COCOON_CONTACT_EMAIL="me@example.com" COCOON_RELAYS=https://bsky.network # Generate with `openssl rand -hex 16` COCOON_ADMIN_PASSWORD= -# openssl rand -hex 32 +# Generate with `openssl rand -hex 32` COCOON_SESSION_SECRET= diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -2,7 +2,7 @@ name: Docker image on: workflow_dispatch: - push: + push: main env: REGISTRY: ghcr.io @@ -55,4 +55,4 @@ uses: actions/attest-build-provenance@v1 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} subject-digest: ${{ steps.push.outputs.digest }} - push-to-registry: true \ No newline at end of file + push-to-registry: true diff --git a/.gitignore b/.gitignore --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ /cocoon *.key *.secret .DS_Store +data/ +keys/ diff --git a/Caddyfile b/Caddyfile new file mode 100644 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,10 @@ +{$COCOON_HOSTNAME} { + reverse_proxy localhost:8080 + + encode gzip + + log { + output file /data/access.log + format json + } +} diff --git a/README.md b/README.md --- a/README.md +++ b/README.md @@ -1,9 +1,140 @@ # Cocoon > [!WARNING] -You should not use this PDS. You should not rely on this code as a reference for a PDS implementation. You should not trust this code. Using this PDS implementation may result in data loss, corruption, etc. +I migrated and have been running my main account on this PDS for months now without issue, however, I am still not responsible if things go awry, particularly during account migration. Please use caution. Cocoon is a PDS implementation in Go. It is highly experimental, and is not ready for any production use. + +## Quick Start with Docker Compose + +### Prerequisites + +- Docker and Docker Compose installed +- A domain name pointing to your server (for automatic HTTPS) +- Ports 80 and 443 open in i.e. UFW + +### Installation + +1. **Clone the repository** + ```bash + git clone https://github.com/haileyok/cocoon.git + cd cocoon + ``` + +2. **Create your configuration file** + ```bash + cp .env.example .env + ``` + +3. **Edit `.env` with your settings** + + Required settings: + ```bash + COCOON_DID="did:web:your-domain.com" + COCOON_HOSTNAME="your-domain.com" + COCOON_CONTACT_EMAIL="you@example.com" + COCOON_RELAYS="https://bsky.network" + + # Generate with: openssl rand -hex 16 + COCOON_ADMIN_PASSWORD="your-secure-password" + + # Generate with: openssl rand -hex 32 + COCOON_SESSION_SECRET="your-session-secret" + ``` + +4. **Start the services** + ```bash + # Pull pre-built image from GitHub Container Registry + docker-compose pull + docker-compose up -d + ``` + + Or build locally: + ```bash + docker-compose build + docker-compose up -d + ``` + +5. **Get your invite code** + + On first run, an invite code is automatically created. View it with: + ```bash + docker-compose logs create-invite + ``` + + Or check the saved file: + ```bash + cat keys/initial-invite-code.txt + ``` + + **IMPORTANT**: Save this invite code! You'll need it to create your first account. + +6. **Monitor the services** + ```bash + docker-compose logs -f + ``` + +### What Gets Set Up + +The Docker Compose setup includes: + +- **init-keys**: Automatically generates cryptographic keys (rotation key and JWK) on first run +- **cocoon**: The main PDS service running on port 8080 +- **create-invite**: Automatically creates an initial invite code after Cocoon starts (first run only) +- **caddy**: Reverse proxy with automatic HTTPS via Let's Encrypt + +### Data Persistence + +The following directories will be created automatically: + +- `./keys/` - Cryptographic keys (generated automatically) + - `rotation.key` - PDS rotation key + - `jwk.key` - JWK private key + - `initial-invite-code.txt` - Your first invite code (first run only) +- `./data/` - SQLite database and blockstore +- Docker volumes for Caddy configuration and certificates + +### Optional Configuration + +#### SMTP Email Settings +```bash +COCOON_SMTP_USER="your-smtp-username" +COCOON_SMTP_PASS="your-smtp-password" +COCOON_SMTP_HOST="smtp.example.com" +COCOON_SMTP_PORT="587" +COCOON_SMTP_EMAIL="noreply@example.com" +COCOON_SMTP_NAME="Cocoon PDS" +``` + +#### S3 Storage +```bash +COCOON_S3_BACKUPS_ENABLED=true +COCOON_S3_BLOBSTORE_ENABLED=true +COCOON_S3_REGION="us-east-1" +COCOON_S3_BUCKET="your-bucket" +COCOON_S3_ENDPOINT="https://s3.amazonaws.com" +COCOON_S3_ACCESS_KEY="your-access-key" +COCOON_S3_SECRET_KEY="your-secret-key" +``` + +### Management Commands + +Create an invite code: +```bash +docker exec cocoon-pds /cocoon create-invite-code --uses 1 +``` + +Reset a user's password: +```bash +docker exec cocoon-pds /cocoon reset-password --did "did:plc:xxx" +``` + +### Updating + +```bash +docker-compose pull +docker-compose up -d +``` ## Implemented Endpoints diff --git a/cmd/cocoon/main.go b/cmd/cocoon/main.go --- a/cmd/cocoon/main.go +++ b/cmd/cocoon/main.go @@ -39,69 +39,56 @@ Value: "cocoon.db", EnvVars: []string{"COCOON_DB_NAME"}, }, &cli.StringFlag{ - Name: "did", - Required: true, - EnvVars: []string{"COCOON_DID"}, + Name: "did", + EnvVars: []string{"COCOON_DID"}, }, &cli.StringFlag{ - Name: "hostname", - Required: true, - EnvVars: []string{"COCOON_HOSTNAME"}, + Name: "hostname", + EnvVars: []string{"COCOON_HOSTNAME"}, }, &cli.StringFlag{ - Name: "rotation-key-path", - Required: true, - EnvVars: []string{"COCOON_ROTATION_KEY_PATH"}, + Name: "rotation-key-path", + EnvVars: []string{"COCOON_ROTATION_KEY_PATH"}, }, &cli.StringFlag{ - Name: "jwk-path", - Required: true, - EnvVars: []string{"COCOON_JWK_PATH"}, + Name: "jwk-path", + EnvVars: []string{"COCOON_JWK_PATH"}, }, &cli.StringFlag{ - Name: "contact-email", - Required: true, - EnvVars: []string{"COCOON_CONTACT_EMAIL"}, + Name: "contact-email", + EnvVars: []string{"COCOON_CONTACT_EMAIL"}, }, &cli.StringSliceFlag{ - Name: "relays", - Required: true, - EnvVars: []string{"COCOON_RELAYS"}, + Name: "relays", + EnvVars: []string{"COCOON_RELAYS"}, }, &cli.StringFlag{ - Name: "admin-password", - Required: true, - EnvVars: []string{"COCOON_ADMIN_PASSWORD"}, + Name: "admin-password", + EnvVars: []string{"COCOON_ADMIN_PASSWORD"}, }, &cli.StringFlag{ - Name: "smtp-user", - Required: false, - EnvVars: []string{"COCOON_SMTP_USER"}, + Name: "smtp-user", + EnvVars: []string{"COCOON_SMTP_USER"}, }, &cli.StringFlag{ - Name: "smtp-pass", - Required: false, - EnvVars: []string{"COCOON_SMTP_PASS"}, + Name: "smtp-pass", + EnvVars: []string{"COCOON_SMTP_PASS"}, }, &cli.StringFlag{ - Name: "smtp-host", - Required: false, - EnvVars: []string{"COCOON_SMTP_HOST"}, + Name: "smtp-host", + EnvVars: []string{"COCOON_SMTP_HOST"}, }, &cli.StringFlag{ - Name: "smtp-port", - Required: false, - EnvVars: []string{"COCOON_SMTP_PORT"}, + Name: "smtp-port", + EnvVars: []string{"COCOON_SMTP_PORT"}, }, &cli.StringFlag{ - Name: "smtp-email", - Required: false, - EnvVars: []string{"COCOON_SMTP_EMAIL"}, + Name: "smtp-email", + EnvVars: []string{"COCOON_SMTP_EMAIL"}, }, &cli.StringFlag{ - Name: "smtp-name", - Required: false, - EnvVars: []string{"COCOON_SMTP_NAME"}, + Name: "smtp-name", + EnvVars: []string{"COCOON_SMTP_NAME"}, }, &cli.BoolFlag{ Name: "s3-backups-enabled", diff --git a/create-initial-invite.sh b/create-initial-invite.sh new file mode 100644 --- /dev/null +++ b/create-initial-invite.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +INVITE_FILE="/keys/initial-invite-code.txt" +MARKER="/keys/.invite_created" + +# Check if invite code was already created +if [ -f "$MARKER" ]; then + echo "✓ Initial invite code already created" + exit 0 +fi + +echo "Waiting for database to be ready..." +sleep 10 + +# Try to create invite code - retry until database is ready +MAX_ATTEMPTS=30 +ATTEMPT=0 +INVITE_CODE="" + +while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do + ATTEMPT=$((ATTEMPT + 1)) + OUTPUT=$(/cocoon create-invite-code --uses 1 2>&1) + INVITE_CODE=$(echo "$OUTPUT" | grep -oE '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{8}' || echo "") + + if [ -n "$INVITE_CODE" ]; then + break + fi + + if [ $((ATTEMPT % 5)) -eq 0 ]; then + echo " Waiting for database... ($ATTEMPT/$MAX_ATTEMPTS)" + fi + sleep 2 +done + +if [ -n "$INVITE_CODE" ]; then + echo "" + echo "╔════════════════════════════════════════╗" + echo "║ SAVE THIS INVITE CODE! ║" + echo "║ ║" + echo "║ $INVITE_CODE ║" + echo "║ ║" + echo "║ Use this to create your first ║" + echo "║ account on your PDS. ║" + echo "╚════════════════════════════════════════╝" + echo "" + + echo "$INVITE_CODE" > "$INVITE_FILE" + echo "✓ Invite code saved to: $INVITE_FILE" + + touch "$MARKER" + echo "✓ Initial setup complete!" +else + echo "✗ Failed to create invite code" + echo "Output: $OUTPUT" + exit 1 +fi diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,125 @@ +version: '3.8' + +services: + init-keys: + build: + context: . + dockerfile: Dockerfile + image: ghcr.io/haileyok/cocoon:latest + container_name: cocoon-init-keys + volumes: + - ./keys:/keys + - ./data:/data/cocoon + - ./init-keys.sh:/init-keys.sh:ro + environment: + COCOON_DID: ${COCOON_DID} + COCOON_HOSTNAME: ${COCOON_HOSTNAME} + COCOON_ROTATION_KEY_PATH: /keys/rotation.key + COCOON_JWK_PATH: /keys/jwk.key + COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL} + COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network} + COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD} + entrypoint: ["/bin/sh", "/init-keys.sh"] + restart: "no" + + cocoon: + build: + context: . + dockerfile: Dockerfile + image: ghcr.io/haileyok/cocoon:latest + container_name: cocoon-pds + network_mode: host + depends_on: + init-keys: + condition: service_completed_successfully + volumes: + - ./data:/data/cocoon + - ./keys/rotation.key:/keys/rotation.key:ro + - ./keys/jwk.key:/keys/jwk.key:ro + environment: + # Required settings + COCOON_DID: ${COCOON_DID} + COCOON_HOSTNAME: ${COCOON_HOSTNAME} + COCOON_ROTATION_KEY_PATH: /keys/rotation.key + COCOON_JWK_PATH: /keys/jwk.key + COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL} + COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network} + COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD} + COCOON_SESSION_SECRET: ${COCOON_SESSION_SECRET} + + # Server configuration + COCOON_ADDR: ":8080" + COCOON_DB_NAME: /data/cocoon/cocoon.db + COCOON_BLOCKSTORE_VARIANT: ${COCOON_BLOCKSTORE_VARIANT:-sqlite} + + # Optional: SMTP settings for email + COCOON_SMTP_USER: ${COCOON_SMTP_USER:-} + COCOON_SMTP_PASS: ${COCOON_SMTP_PASS:-} + COCOON_SMTP_HOST: ${COCOON_SMTP_HOST:-} + COCOON_SMTP_PORT: ${COCOON_SMTP_PORT:-} + COCOON_SMTP_EMAIL: ${COCOON_SMTP_EMAIL:-} + COCOON_SMTP_NAME: ${COCOON_SMTP_NAME:-} + + # Optional: S3 configuration + COCOON_S3_BACKUPS_ENABLED: ${COCOON_S3_BACKUPS_ENABLED:-false} + COCOON_S3_BLOBSTORE_ENABLED: ${COCOON_S3_BLOBSTORE_ENABLED:-false} + COCOON_S3_REGION: ${COCOON_S3_REGION:-} + COCOON_S3_BUCKET: ${COCOON_S3_BUCKET:-} + COCOON_S3_ENDPOINT: ${COCOON_S3_ENDPOINT:-} + COCOON_S3_ACCESS_KEY: ${COCOON_S3_ACCESS_KEY:-} + COCOON_S3_SECRET_KEY: ${COCOON_S3_SECRET_KEY:-} + + # Optional: Fallback proxy + COCOON_FALLBACK_PROXY: ${COCOON_FALLBACK_PROXY:-} + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/xrpc/_health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + create-invite: + build: + context: . + dockerfile: Dockerfile + image: ghcr.io/haileyok/cocoon:latest + container_name: cocoon-create-invite + network_mode: host + volumes: + - ./keys:/keys + - ./create-initial-invite.sh:/create-initial-invite.sh:ro + environment: + COCOON_DID: ${COCOON_DID} + COCOON_HOSTNAME: ${COCOON_HOSTNAME} + COCOON_ROTATION_KEY_PATH: /keys/rotation.key + COCOON_JWK_PATH: /keys/jwk.key + COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL} + COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network} + COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD} + COCOON_DB_NAME: /data/cocoon/cocoon.db + depends_on: + - init-keys + entrypoint: ["/bin/sh", "/create-initial-invite.sh"] + restart: "no" + + caddy: + image: caddy:2-alpine + container_name: cocoon-caddy + network_mode: host + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + restart: unless-stopped + environment: + COCOON_HOSTNAME: ${COCOON_HOSTNAME} + CADDY_ACME_EMAIL: ${COCOON_CONTACT_EMAIL:-} + +volumes: + data: + driver: local + caddy_data: + driver: local + caddy_config: + driver: local diff --git a/init-keys.sh b/init-keys.sh new file mode 100644 --- /dev/null +++ b/init-keys.sh @@ -0,0 +1,34 @@ +#!/bin/sh +set -e + +mkdir -p /keys +mkdir -p /data/cocoon + +if [ ! -f /keys/rotation.key ]; then + echo "Generating rotation key..." + /cocoon create-rotation-key --out /keys/rotation.key 2>/dev/null || true + if [ -f /keys/rotation.key ]; then + echo "✓ Rotation key generated at /keys/rotation.key" + else + echo "✗ Failed to generate rotation key" + exit 1 + fi +else + echo "✓ Rotation key already exists" +fi + +if [ ! -f /keys/jwk.key ]; then + echo "Generating JWK..." + /cocoon create-private-jwk --out /keys/jwk.key 2>/dev/null || true + if [ -f /keys/jwk.key ]; then + echo "✓ JWK generated at /keys/jwk.key" + else + echo "✗ Failed to generate JWK" + exit 1 + fi +else + echo "✓ JWK already exists" +fi + +echo "" +echo "✓ Key initialization complete!" -- tangled.sh