Warning
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.
- Docker and Docker Compose installed
- A domain name pointing to your server (for automatic HTTPS)
- Ports 80 and 443 open in i.e. UFW
-
Clone the repository
git clone https://github.com/haileyok/cocoon.git cd cocoon -
Create your configuration file
cp .env.example .env
-
Edit
.envwith your settingsRequired settings:
COCOON_DID="did:web:your-domain.com" COCOON_HOSTNAME="your-domain.com" COCOON_CONTACT_EMAIL="[email protected]" 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"
-
Start the services
# Pull pre-built image from GitHub Container Registry docker-compose pull docker-compose up -dOr build locally:
docker-compose build docker-compose up -d
For PostgreSQL deployment:
# Add POSTGRES_PASSWORD to your .env file first! docker-compose -f docker-compose.postgres.yaml up -d -
Get your invite code
On first run, an invite code is automatically created. View it with:
docker-compose logs create-invite
Or check the saved file:
cat keys/initial-invite-code.txt
IMPORTANT: Save this invite code! You'll need it to create your first account.
-
Monitor the services
docker-compose logs -f
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
The following directories will be created automatically:
./keys/- Cryptographic keys (generated automatically)rotation.key- PDS rotation keyjwk.key- JWK private keyinitial-invite-code.txt- Your first invite code (first run only)
./data/- SQLite database and blockstore- Docker volumes for Caddy configuration and certificates
By default, Cocoon uses SQLite which requires no additional setup. For production deployments with higher traffic, you can use PostgreSQL:
# Database type: sqlite (default) or postgres
COCOON_DB_TYPE="postgres"
# PostgreSQL connection string (required if db-type is postgres)
# Format: postgres://user:password@host:port/database?sslmode=disable
COCOON_DATABASE_URL="postgres://cocoon:password@localhost:5432/cocoon?sslmode=disable"
# Or use the standard DATABASE_URL environment variable
DATABASE_URL="postgres://cocoon:password@localhost:5432/cocoon?sslmode=disable"For SQLite (default):
COCOON_DB_TYPE="sqlite"
COCOON_DB_NAME="/data/cocoon/cocoon.db"Note: When using PostgreSQL, database backups to S3 are not handled by Cocoon. Use
pg_dumpor your database provider's backup solution instead.
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="[email protected]"
COCOON_SMTP_NAME="Cocoon PDS"Cocoon supports S3-compatible storage for both database backups (SQLite only) and blob storage (images, videos, etc.):
# Enable S3 backups (SQLite databases only - hourly backups)
COCOON_S3_BACKUPS_ENABLED=true
# Enable S3 for blob storage (images, videos, etc.)
# When enabled, blobs are stored in S3 instead of the database
COCOON_S3_BLOBSTORE_ENABLED=true
# S3 configuration (works with AWS S3, MinIO, Cloudflare R2, etc.)
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"
# Optional: CDN/public URL for blob redirects
# When set, com.atproto.sync.getBlob redirects to this URL instead of proxying
COCOON_S3_CDN_URL="https://cdn.example.com"Blob Storage Options:
COCOON_S3_BLOBSTORE_ENABLED=false(default): Blobs stored in the databaseCOCOON_S3_BLOBSTORE_ENABLED=true: Blobs stored in S3 bucket underblobs/{did}/{cid}
Blob Serving Options:
- Without
COCOON_S3_CDN_URL: Blobs are proxied through the PDS server - With
COCOON_S3_CDN_URL:getBlobreturns a 302 redirect to{CDN_URL}/blobs/{did}/{cid}
Tip: For Cloudflare R2, you can use the public bucket URL as the CDN URL. For AWS S3, you can use CloudFront or the S3 bucket URL directly if public access is enabled.
Create an invite code:
docker exec cocoon-pds /cocoon create-invite-code --uses 1Reset a user's password:
docker exec cocoon-pds /cocoon reset-password --did "did:plc:xxx"docker-compose pull
docker-compose up -dNote
Just because something is implemented doesn't mean it is finished. Tons of these are returning bad errors, don't do validation properly, etc. I'll make a "second pass" checklist at some point to do all of that.
-
com.atproto.identity.getRecommendedDidCredentials -
com.atproto.identity.requestPlcOperationSignature -
com.atproto.identity.resolveHandle -
com.atproto.identity.signPlcOperation -
com.atproto.identity.submitPlcOperation -
com.atproto.identity.updateHandle
-
com.atproto.repo.applyWrites -
com.atproto.repo.createRecord -
com.atproto.repo.putRecord -
com.atproto.repo.deleteRecord -
com.atproto.repo.describeRepo -
com.atproto.repo.getRecord -
com.atproto.repo.importRepo(Works "okay". Use with extreme caution.) -
com.atproto.repo.listRecords -
com.atproto.repo.listMissingBlobs
-
com.atproto.server.activateAccount -
com.atproto.server.checkAccountStatus -
com.atproto.server.confirmEmail -
com.atproto.server.createAccount -
com.atproto.server.createInviteCode -
com.atproto.server.createInviteCodes -
com.atproto.server.deactivateAccount -
com.atproto.server.deleteAccount -
com.atproto.server.deleteSession -
com.atproto.server.describeServer -
com.atproto.server.getAccountInviteCodes -
com.atproto.server.getServiceAuth [ ]- not going to add app passwordscom.atproto.server.listAppPasswords-
com.atproto.server.refreshSession -
com.atproto.server.requestAccountDelete -
com.atproto.server.requestEmailConfirmation -
com.atproto.server.requestEmailUpdate -
com.atproto.server.requestPasswordReset -
com.atproto.server.reserveSigningKey -
com.atproto.server.resetPassword []- not going to add app passwordscom.atproto.server.revokeAppPassword-
com.atproto.server.updateEmail
-
com.atproto.sync.getBlob -
com.atproto.sync.getBlocks -
com.atproto.sync.getLatestCommit -
com.atproto.sync.getRecord -
com.atproto.sync.getRepoStatus -
com.atproto.sync.getRepo -
com.atproto.sync.listBlobs -
com.atproto.sync.listRepos [ ]- BGS doesn't even have this implemented lolcom.atproto.sync.notifyOfUpdate-
com.atproto.sync.requestCrawl -
com.atproto.sync.subscribeRepos
-
com.atproto.label.queryLabels -
com.atproto.moderation.createReport(Note: this should be handled by proxying, not actually implemented in the PDS) -
app.bsky.actor.getPreferences -
app.bsky.actor.putPreferences
This project is licensed under MIT license. server/static/pico.css is also licensed under MIT license, available at https://github.com/picocss/pico/.