+1
-1
.env.example
+1
-1
.env.example
+2
-2
.github/workflows/docker-image.yml
+2
-2
.github/workflows/docker-image.yml
+10
Caddyfile
+10
Caddyfile
+132
-1
README.md
+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
+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
+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
+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
+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!"