An atproto PDS written in Go

add docker-compose, caddy (#35)

* add docker compose, caddy

* tweaks

* more tweaks

* tweak

* fix gh action

authored by hailey.at and committed by GitHub 21fb119b 89013a78

+1 -1
.env.example
··· 6 6 COCOON_RELAYS=https://bsky.network 7 7 # Generate with `openssl rand -hex 16` 8 8 COCOON_ADMIN_PASSWORD= 9 - # openssl rand -hex 32 9 + # Generate with `openssl rand -hex 32` 10 10 COCOON_SESSION_SECRET=
+2 -2
.github/workflows/docker-image.yml
··· 2 2 3 3 on: 4 4 workflow_dispatch: 5 - push: 5 + push: main 6 6 7 7 env: 8 8 REGISTRY: ghcr.io ··· 55 55 with: 56 56 subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} 57 57 subject-digest: ${{ steps.push.outputs.digest }} 58 - push-to-registry: true 58 + push-to-registry: true
+2
.gitignore
··· 4 4 *.key 5 5 *.secret 6 6 .DS_Store 7 + data/ 8 + keys/
+10
Caddyfile
··· 1 + {$COCOON_HOSTNAME} { 2 + reverse_proxy localhost:8080 3 + 4 + encode gzip 5 + 6 + log { 7 + output file /data/access.log 8 + format json 9 + } 10 + }
+132 -1
README.md
··· 1 1 # Cocoon 2 2 3 3 > [!WARNING] 4 - 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. 4 + 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. 5 5 6 6 Cocoon is a PDS implementation in Go. It is highly experimental, and is not ready for any production use. 7 + 8 + ## Quick Start with Docker Compose 9 + 10 + ### Prerequisites 11 + 12 + - Docker and Docker Compose installed 13 + - A domain name pointing to your server (for automatic HTTPS) 14 + - Ports 80 and 443 open in i.e. UFW 15 + 16 + ### Installation 17 + 18 + 1. **Clone the repository** 19 + ```bash 20 + git clone https://github.com/haileyok/cocoon.git 21 + cd cocoon 22 + ``` 23 + 24 + 2. **Create your configuration file** 25 + ```bash 26 + cp .env.example .env 27 + ``` 28 + 29 + 3. **Edit `.env` with your settings** 30 + 31 + Required settings: 32 + ```bash 33 + COCOON_DID="did:web:your-domain.com" 34 + COCOON_HOSTNAME="your-domain.com" 35 + COCOON_CONTACT_EMAIL="you@example.com" 36 + COCOON_RELAYS="https://bsky.network" 37 + 38 + # Generate with: openssl rand -hex 16 39 + COCOON_ADMIN_PASSWORD="your-secure-password" 40 + 41 + # Generate with: openssl rand -hex 32 42 + COCOON_SESSION_SECRET="your-session-secret" 43 + ``` 44 + 45 + 4. **Start the services** 46 + ```bash 47 + # Pull pre-built image from GitHub Container Registry 48 + docker-compose pull 49 + docker-compose up -d 50 + ``` 51 + 52 + Or build locally: 53 + ```bash 54 + docker-compose build 55 + docker-compose up -d 56 + ``` 57 + 58 + 5. **Get your invite code** 59 + 60 + On first run, an invite code is automatically created. View it with: 61 + ```bash 62 + docker-compose logs create-invite 63 + ``` 64 + 65 + Or check the saved file: 66 + ```bash 67 + cat keys/initial-invite-code.txt 68 + ``` 69 + 70 + **IMPORTANT**: Save this invite code! You'll need it to create your first account. 71 + 72 + 6. **Monitor the services** 73 + ```bash 74 + docker-compose logs -f 75 + ``` 76 + 77 + ### What Gets Set Up 78 + 79 + The Docker Compose setup includes: 80 + 81 + - **init-keys**: Automatically generates cryptographic keys (rotation key and JWK) on first run 82 + - **cocoon**: The main PDS service running on port 8080 83 + - **create-invite**: Automatically creates an initial invite code after Cocoon starts (first run only) 84 + - **caddy**: Reverse proxy with automatic HTTPS via Let's Encrypt 85 + 86 + ### Data Persistence 87 + 88 + The following directories will be created automatically: 89 + 90 + - `./keys/` - Cryptographic keys (generated automatically) 91 + - `rotation.key` - PDS rotation key 92 + - `jwk.key` - JWK private key 93 + - `initial-invite-code.txt` - Your first invite code (first run only) 94 + - `./data/` - SQLite database and blockstore 95 + - Docker volumes for Caddy configuration and certificates 96 + 97 + ### Optional Configuration 98 + 99 + #### SMTP Email Settings 100 + ```bash 101 + COCOON_SMTP_USER="your-smtp-username" 102 + COCOON_SMTP_PASS="your-smtp-password" 103 + COCOON_SMTP_HOST="smtp.example.com" 104 + COCOON_SMTP_PORT="587" 105 + COCOON_SMTP_EMAIL="noreply@example.com" 106 + COCOON_SMTP_NAME="Cocoon PDS" 107 + ``` 108 + 109 + #### S3 Storage 110 + ```bash 111 + COCOON_S3_BACKUPS_ENABLED=true 112 + COCOON_S3_BLOBSTORE_ENABLED=true 113 + COCOON_S3_REGION="us-east-1" 114 + COCOON_S3_BUCKET="your-bucket" 115 + COCOON_S3_ENDPOINT="https://s3.amazonaws.com" 116 + COCOON_S3_ACCESS_KEY="your-access-key" 117 + COCOON_S3_SECRET_KEY="your-secret-key" 118 + ``` 119 + 120 + ### Management Commands 121 + 122 + Create an invite code: 123 + ```bash 124 + docker exec cocoon-pds /cocoon create-invite-code --uses 1 125 + ``` 126 + 127 + Reset a user's password: 128 + ```bash 129 + docker exec cocoon-pds /cocoon reset-password --did "did:plc:xxx" 130 + ``` 131 + 132 + ### Updating 133 + 134 + ```bash 135 + docker-compose pull 136 + docker-compose up -d 137 + ``` 7 138 8 139 ## Implemented Endpoints 9 140
+26 -39
cmd/cocoon/main.go
··· 39 39 EnvVars: []string{"COCOON_DB_NAME"}, 40 40 }, 41 41 &cli.StringFlag{ 42 - Name: "did", 43 - Required: true, 44 - EnvVars: []string{"COCOON_DID"}, 42 + Name: "did", 43 + EnvVars: []string{"COCOON_DID"}, 45 44 }, 46 45 &cli.StringFlag{ 47 - Name: "hostname", 48 - Required: true, 49 - EnvVars: []string{"COCOON_HOSTNAME"}, 46 + Name: "hostname", 47 + EnvVars: []string{"COCOON_HOSTNAME"}, 50 48 }, 51 49 &cli.StringFlag{ 52 - Name: "rotation-key-path", 53 - Required: true, 54 - EnvVars: []string{"COCOON_ROTATION_KEY_PATH"}, 50 + Name: "rotation-key-path", 51 + EnvVars: []string{"COCOON_ROTATION_KEY_PATH"}, 55 52 }, 56 53 &cli.StringFlag{ 57 - Name: "jwk-path", 58 - Required: true, 59 - EnvVars: []string{"COCOON_JWK_PATH"}, 54 + Name: "jwk-path", 55 + EnvVars: []string{"COCOON_JWK_PATH"}, 60 56 }, 61 57 &cli.StringFlag{ 62 - Name: "contact-email", 63 - Required: true, 64 - EnvVars: []string{"COCOON_CONTACT_EMAIL"}, 58 + Name: "contact-email", 59 + EnvVars: []string{"COCOON_CONTACT_EMAIL"}, 65 60 }, 66 61 &cli.StringSliceFlag{ 67 - Name: "relays", 68 - Required: true, 69 - EnvVars: []string{"COCOON_RELAYS"}, 62 + Name: "relays", 63 + EnvVars: []string{"COCOON_RELAYS"}, 70 64 }, 71 65 &cli.StringFlag{ 72 - Name: "admin-password", 73 - Required: true, 74 - EnvVars: []string{"COCOON_ADMIN_PASSWORD"}, 66 + Name: "admin-password", 67 + EnvVars: []string{"COCOON_ADMIN_PASSWORD"}, 75 68 }, 76 69 &cli.StringFlag{ 77 - Name: "smtp-user", 78 - Required: false, 79 - EnvVars: []string{"COCOON_SMTP_USER"}, 70 + Name: "smtp-user", 71 + EnvVars: []string{"COCOON_SMTP_USER"}, 80 72 }, 81 73 &cli.StringFlag{ 82 - Name: "smtp-pass", 83 - Required: false, 84 - EnvVars: []string{"COCOON_SMTP_PASS"}, 74 + Name: "smtp-pass", 75 + EnvVars: []string{"COCOON_SMTP_PASS"}, 85 76 }, 86 77 &cli.StringFlag{ 87 - Name: "smtp-host", 88 - Required: false, 89 - EnvVars: []string{"COCOON_SMTP_HOST"}, 78 + Name: "smtp-host", 79 + EnvVars: []string{"COCOON_SMTP_HOST"}, 90 80 }, 91 81 &cli.StringFlag{ 92 - Name: "smtp-port", 93 - Required: false, 94 - EnvVars: []string{"COCOON_SMTP_PORT"}, 82 + Name: "smtp-port", 83 + EnvVars: []string{"COCOON_SMTP_PORT"}, 95 84 }, 96 85 &cli.StringFlag{ 97 - Name: "smtp-email", 98 - Required: false, 99 - EnvVars: []string{"COCOON_SMTP_EMAIL"}, 86 + Name: "smtp-email", 87 + EnvVars: []string{"COCOON_SMTP_EMAIL"}, 100 88 }, 101 89 &cli.StringFlag{ 102 - Name: "smtp-name", 103 - Required: false, 104 - EnvVars: []string{"COCOON_SMTP_NAME"}, 90 + Name: "smtp-name", 91 + EnvVars: []string{"COCOON_SMTP_NAME"}, 105 92 }, 106 93 &cli.BoolFlag{ 107 94 Name: "s3-backups-enabled",
+56
create-initial-invite.sh
··· 1 + #!/bin/sh 2 + 3 + INVITE_FILE="/keys/initial-invite-code.txt" 4 + MARKER="/keys/.invite_created" 5 + 6 + # Check if invite code was already created 7 + if [ -f "$MARKER" ]; then 8 + echo "✓ Initial invite code already created" 9 + exit 0 10 + fi 11 + 12 + echo "Waiting for database to be ready..." 13 + sleep 10 14 + 15 + # Try to create invite code - retry until database is ready 16 + MAX_ATTEMPTS=30 17 + ATTEMPT=0 18 + INVITE_CODE="" 19 + 20 + while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do 21 + ATTEMPT=$((ATTEMPT + 1)) 22 + OUTPUT=$(/cocoon create-invite-code --uses 1 2>&1) 23 + INVITE_CODE=$(echo "$OUTPUT" | grep -oE '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{8}' || echo "") 24 + 25 + if [ -n "$INVITE_CODE" ]; then 26 + break 27 + fi 28 + 29 + if [ $((ATTEMPT % 5)) -eq 0 ]; then 30 + echo " Waiting for database... ($ATTEMPT/$MAX_ATTEMPTS)" 31 + fi 32 + sleep 2 33 + done 34 + 35 + if [ -n "$INVITE_CODE" ]; then 36 + echo "" 37 + echo "╔════════════════════════════════════════╗" 38 + echo "║ SAVE THIS INVITE CODE! ║" 39 + echo "║ ║" 40 + echo "║ $INVITE_CODE ║" 41 + echo "║ ║" 42 + echo "║ Use this to create your first ║" 43 + echo "║ account on your PDS. ║" 44 + echo "╚════════════════════════════════════════╝" 45 + echo "" 46 + 47 + echo "$INVITE_CODE" > "$INVITE_FILE" 48 + echo "✓ Invite code saved to: $INVITE_FILE" 49 + 50 + touch "$MARKER" 51 + echo "✓ Initial setup complete!" 52 + else 53 + echo "✗ Failed to create invite code" 54 + echo "Output: $OUTPUT" 55 + exit 1 56 + fi
+125
docker-compose.yaml
··· 1 + version: '3.8' 2 + 3 + services: 4 + init-keys: 5 + build: 6 + context: . 7 + dockerfile: Dockerfile 8 + image: ghcr.io/haileyok/cocoon:latest 9 + container_name: cocoon-init-keys 10 + volumes: 11 + - ./keys:/keys 12 + - ./data:/data/cocoon 13 + - ./init-keys.sh:/init-keys.sh:ro 14 + environment: 15 + COCOON_DID: ${COCOON_DID} 16 + COCOON_HOSTNAME: ${COCOON_HOSTNAME} 17 + COCOON_ROTATION_KEY_PATH: /keys/rotation.key 18 + COCOON_JWK_PATH: /keys/jwk.key 19 + COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL} 20 + COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network} 21 + COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD} 22 + entrypoint: ["/bin/sh", "/init-keys.sh"] 23 + restart: "no" 24 + 25 + cocoon: 26 + build: 27 + context: . 28 + dockerfile: Dockerfile 29 + image: ghcr.io/haileyok/cocoon:latest 30 + container_name: cocoon-pds 31 + network_mode: host 32 + depends_on: 33 + init-keys: 34 + condition: service_completed_successfully 35 + volumes: 36 + - ./data:/data/cocoon 37 + - ./keys/rotation.key:/keys/rotation.key:ro 38 + - ./keys/jwk.key:/keys/jwk.key:ro 39 + environment: 40 + # Required settings 41 + COCOON_DID: ${COCOON_DID} 42 + COCOON_HOSTNAME: ${COCOON_HOSTNAME} 43 + COCOON_ROTATION_KEY_PATH: /keys/rotation.key 44 + COCOON_JWK_PATH: /keys/jwk.key 45 + COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL} 46 + COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network} 47 + COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD} 48 + COCOON_SESSION_SECRET: ${COCOON_SESSION_SECRET} 49 + 50 + # Server configuration 51 + COCOON_ADDR: ":8080" 52 + COCOON_DB_NAME: /data/cocoon/cocoon.db 53 + COCOON_BLOCKSTORE_VARIANT: ${COCOON_BLOCKSTORE_VARIANT:-sqlite} 54 + 55 + # Optional: SMTP settings for email 56 + COCOON_SMTP_USER: ${COCOON_SMTP_USER:-} 57 + COCOON_SMTP_PASS: ${COCOON_SMTP_PASS:-} 58 + COCOON_SMTP_HOST: ${COCOON_SMTP_HOST:-} 59 + COCOON_SMTP_PORT: ${COCOON_SMTP_PORT:-} 60 + COCOON_SMTP_EMAIL: ${COCOON_SMTP_EMAIL:-} 61 + COCOON_SMTP_NAME: ${COCOON_SMTP_NAME:-} 62 + 63 + # Optional: S3 configuration 64 + COCOON_S3_BACKUPS_ENABLED: ${COCOON_S3_BACKUPS_ENABLED:-false} 65 + COCOON_S3_BLOBSTORE_ENABLED: ${COCOON_S3_BLOBSTORE_ENABLED:-false} 66 + COCOON_S3_REGION: ${COCOON_S3_REGION:-} 67 + COCOON_S3_BUCKET: ${COCOON_S3_BUCKET:-} 68 + COCOON_S3_ENDPOINT: ${COCOON_S3_ENDPOINT:-} 69 + COCOON_S3_ACCESS_KEY: ${COCOON_S3_ACCESS_KEY:-} 70 + COCOON_S3_SECRET_KEY: ${COCOON_S3_SECRET_KEY:-} 71 + 72 + # Optional: Fallback proxy 73 + COCOON_FALLBACK_PROXY: ${COCOON_FALLBACK_PROXY:-} 74 + restart: unless-stopped 75 + healthcheck: 76 + test: ["CMD", "curl", "-f", "http://localhost:8080/xrpc/_health"] 77 + interval: 30s 78 + timeout: 10s 79 + retries: 3 80 + start_period: 40s 81 + 82 + create-invite: 83 + build: 84 + context: . 85 + dockerfile: Dockerfile 86 + image: ghcr.io/haileyok/cocoon:latest 87 + container_name: cocoon-create-invite 88 + network_mode: host 89 + volumes: 90 + - ./keys:/keys 91 + - ./create-initial-invite.sh:/create-initial-invite.sh:ro 92 + environment: 93 + COCOON_DID: ${COCOON_DID} 94 + COCOON_HOSTNAME: ${COCOON_HOSTNAME} 95 + COCOON_ROTATION_KEY_PATH: /keys/rotation.key 96 + COCOON_JWK_PATH: /keys/jwk.key 97 + COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL} 98 + COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network} 99 + COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD} 100 + COCOON_DB_NAME: /data/cocoon/cocoon.db 101 + depends_on: 102 + - init-keys 103 + entrypoint: ["/bin/sh", "/create-initial-invite.sh"] 104 + restart: "no" 105 + 106 + caddy: 107 + image: caddy:2-alpine 108 + container_name: cocoon-caddy 109 + network_mode: host 110 + volumes: 111 + - ./Caddyfile:/etc/caddy/Caddyfile:ro 112 + - caddy_data:/data 113 + - caddy_config:/config 114 + restart: unless-stopped 115 + environment: 116 + COCOON_HOSTNAME: ${COCOON_HOSTNAME} 117 + CADDY_ACME_EMAIL: ${COCOON_CONTACT_EMAIL:-} 118 + 119 + volumes: 120 + data: 121 + driver: local 122 + caddy_data: 123 + driver: local 124 + caddy_config: 125 + driver: local
+34
init-keys.sh
··· 1 + #!/bin/sh 2 + set -e 3 + 4 + mkdir -p /keys 5 + mkdir -p /data/cocoon 6 + 7 + if [ ! -f /keys/rotation.key ]; then 8 + echo "Generating rotation key..." 9 + /cocoon create-rotation-key --out /keys/rotation.key 2>/dev/null || true 10 + if [ -f /keys/rotation.key ]; then 11 + echo "✓ Rotation key generated at /keys/rotation.key" 12 + else 13 + echo "✗ Failed to generate rotation key" 14 + exit 1 15 + fi 16 + else 17 + echo "✓ Rotation key already exists" 18 + fi 19 + 20 + if [ ! -f /keys/jwk.key ]; then 21 + echo "Generating JWK..." 22 + /cocoon create-private-jwk --out /keys/jwk.key 2>/dev/null || true 23 + if [ -f /keys/jwk.key ]; then 24 + echo "✓ JWK generated at /keys/jwk.key" 25 + else 26 + echo "✗ Failed to generate JWK" 27 + exit 1 28 + fi 29 + else 30 + echo "✓ JWK already exists" 31 + fi 32 + 33 + echo "" 34 + echo "✓ Key initialization complete!"