A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
at refactor 403 lines 16 kB view raw view rendered
1# ATCR Hold Service 2 3> The storage backend component of ATCR (ATProto Container Registry) 4 5## Overview 6 7**Hold Service** is the storage backend component of ATCR. It enables BYOS (Bring Your Own Storage) - users can store their own container image layers in their own S3, Storj, Minio, or filesystem storage. Each hold runs as a full ATProto user with an embedded PDS, exposing both standard ATProto sync endpoints and custom XRPC endpoints for OCI multipart blob uploads. 8 9### What Hold Service Does 10 11Hold Service is the storage layer that: 12 13- **Bring Your Own Storage (BYOS)** - Store your own container image layers in your own S3, Storj, Minio, or filesystem 14- **Embedded ATProto PDS** - Each hold is a full ATProto user with its own DID, repository, and identity 15- **Custom XRPC Endpoints** - OCI-compatible multipart upload endpoints (`io.atcr.hold.*`) for blob operations 16- **Presigned URL Generation** - Creates time-limited S3 URLs for direct client-to-storage transfers (~99% bandwidth reduction) 17- **Crew Management** - Controls access via captain and crew records stored in the hold's embedded PDS 18- **Standard ATProto Sync** - Exposes com.atproto.sync.* endpoints for repository synchronization and firehose 19- **Multi-Backend Support** - Works with S3, Storj, Minio, filesystem, Azure, GCS via distribution's driver system 20- **Bluesky Integration** - Optional: Posts container image push notifications from the hold's identity to Bluesky 21 22### The ATCR Ecosystem 23 24Hold Service is the **storage backend** of a multi-component architecture: 25 261. **[AppView](https://atcr.io/r/evan.jarrett.net/atcr-appview)** - Registry API + web interface 272. **Hold Service** (this component) - Storage backend with embedded PDS 283. **Credential Helper** - Client-side tool for ATProto OAuth authentication 29 30**Data flow:** 31``` 32Docker Client → AppView (resolves identity) → User's PDS (stores manifest) 33 34 Hold Service (generates presigned URL) 35 36 S3/Storj/etc. (client uploads/downloads blobs directly) 37``` 38 39Manifests (small JSON metadata) live in users' ATProto PDS, while blobs (large binary layers) live in hold services. AppView orchestrates the routing, and hold services provide presigned URLs to eliminate bandwidth bottlenecks. 40 41## When to Run Your Own Hold 42 43Most users can push to the default hold at **https://hold01.atcr.io** - you don't need to run your own hold. 44 45**Run your own hold if you want to:** 46- Control where your container layer data is stored (own S3 bucket, Storj, etc.) 47- Manage access for a team or organization via crew membership 48- Reduce bandwidth costs by using presigned URLs for direct S3 transfers 49- Run a shared hold for a community or project 50- Maintain data sovereignty (keep blobs in specific geographic regions) 51 52**Prerequisites:** 53- S3-compatible storage (AWS S3, Storj, Minio, UpCloud, etc.) OR filesystem storage 54- (Optional) Domain name with SSL/TLS certificates for production 55- ATProto DID for hold owner (get from: `https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=yourhandle.bsky.social`) 56 57## Quick Start 58 59### Using Docker Compose 60 61The fastest way to run Hold service with S3 storage: 62 63```bash 64# Clone repository 65git clone https://tangled.org/@evan.jarrett.net/at-container-registry 66cd atcr 67 68# Copy and configure environment 69cp .env.hold.example .env.hold 70# Edit .env.hold - set HOLD_PUBLIC_URL, HOLD_OWNER, S3 credentials (see Configuration below) 71 72# Start hold service 73docker-compose -f docker-compose.hold.yml up -d 74 75# Verify 76curl http://localhost:8080/.well-known/did.json 77``` 78 79### Minimal Configuration 80 81At minimum, you must set: 82 83```bash 84# Required: Public URL (generates did:web identity) 85HOLD_PUBLIC_URL=https://hold.example.com 86 87# Required: Your ATProto DID (for captain record) 88HOLD_OWNER=did:plc:your-did-here 89 90# Required: Storage driver type 91STORAGE_DRIVER=s3 92 93# Required for S3: Credentials and bucket 94AWS_ACCESS_KEY_ID=your-access-key 95AWS_SECRET_ACCESS_KEY=your-secret-key 96S3_BUCKET=your-bucket-name 97 98# Recommended: Database directory for embedded PDS 99HOLD_DATABASE_DIR=/var/lib/atcr-hold 100``` 101 102See **Configuration Reference** below for all options. 103 104## Configuration Reference 105 106Hold Service is configured entirely via environment variables. Load them with: 107```bash 108source .env.hold 109./bin/atcr-hold 110``` 111 112Or via Docker Compose (recommended). 113 114### Server Configuration 115 116#### `HOLD_PUBLIC_URL` ⚠️ REQUIRED 117- **Default:** None (required) 118- **Description:** Public URL of this hold service. Used to generate the hold's did:web identity. The hostname becomes the hold's DID. 119- **Format:** `https://hold.example.com` or `http://127.0.0.1:8080` (development) 120- **Example:** `https://hold01.atcr.io` → DID is `did:web:hold01.atcr.io` 121- **Note:** This URL must be reachable by AppView and Docker clients 122 123#### `HOLD_SERVER_ADDR` 124- **Default:** `:8080` 125- **Description:** HTTP listen address for XRPC endpoints 126- **Example:** `:8080`, `:9000`, `0.0.0.0:8080` 127 128#### `HOLD_PUBLIC` 129- **Default:** `false` 130- **Description:** Allow public blob reads (pulls) without authentication. Writes always require crew membership. 131- **Use cases:** 132 - `true`: Public registry (anyone can pull, authenticated users can push if crew) 133 - `false`: Private registry (authentication required for both push and pull) 134 135### Storage Configuration 136 137#### `STORAGE_DRIVER` 138- **Default:** `s3` 139- **Options:** `s3`, `filesystem` 140- **Description:** Storage backend type. S3 enables presigned URLs for direct client-to-storage transfers (~99% bandwidth reduction). Filesystem stores blobs locally (development/testing). 141 142#### S3 Storage (when `STORAGE_DRIVER=s3`) 143 144##### `AWS_ACCESS_KEY_ID` ⚠️ REQUIRED for S3 145- **Description:** S3 access key ID for authentication 146- **Example:** `AKIAIOSFODNN7EXAMPLE` 147 148##### `AWS_SECRET_ACCESS_KEY` ⚠️ REQUIRED for S3 149- **Description:** S3 secret access key for authentication 150- **Example:** `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` 151 152##### `AWS_REGION` 153- **Default:** `us-east-1` 154- **Description:** S3 region 155- **AWS regions:** `us-east-1`, `us-west-2`, `eu-west-1`, etc. 156- **UpCloud regions:** `us-chi1`, `us-nyc1`, `de-fra1`, `uk-lon1`, `sg-sin1` 157 158##### `S3_BUCKET` ⚠️ REQUIRED for S3 159- **Description:** S3 bucket name where blobs will be stored 160- **Example:** `atcr-blobs`, `my-company-registry-blobs` 161- **Note:** Bucket must already exist 162 163##### `S3_ENDPOINT` 164- **Default:** None (uses AWS S3) 165- **Description:** S3-compatible endpoint URL for non-AWS providers 166- **Storj:** `https://gateway.storjshare.io` 167- **UpCloud:** `https://[bucket-id].upcloudobjects.com` 168- **Minio:** `http://minio:9000` 169- **Note:** Leave empty for AWS S3 170 171#### Filesystem Storage (when `STORAGE_DRIVER=filesystem`) 172 173##### `STORAGE_ROOT_DIR` 174- **Default:** `/var/lib/atcr/hold` 175- **Description:** Directory path where blobs will be stored on local filesystem 176- **Use case:** Development, testing, or single-server deployments 177- **Note:** Presigned URLs are not available with filesystem driver (hold proxies all blob transfers) 178 179### Embedded PDS Configuration 180 181#### `HOLD_DATABASE_DIR` 182- **Default:** `/var/lib/atcr-hold` 183- **Description:** Directory path for embedded PDS carstore (SQLite database). Carstore creates `db.sqlite3` inside this directory. 184- **Note:** This must be a directory path, NOT a file path. If empty, embedded PDS is disabled (not recommended - hold authorization requires PDS). 185 186#### `HOLD_KEY_PATH` 187- **Default:** `{HOLD_DATABASE_DIR}/signing.key` 188- **Description:** Path to hold's signing key (secp256k1). Auto-generated on first run if missing. 189- **Note:** Keep this secure - it's used to sign ATProto commits in the hold's repository 190 191### Access Control 192 193#### `HOLD_OWNER` 194- **Default:** None 195- **Description:** Your ATProto DID. Used to create the captain record and add you as the first crew member with admin role. 196- **Get your DID:** `https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=yourhandle.bsky.social` 197- **Example:** `did:plc:abc123xyz789` 198- **Note:** If set, the hold will initialize with your DID as owner on first run 199 200#### `HOLD_ALLOW_ALL_CREW` 201- **Default:** `false` 202- **Description:** Allow any authenticated ATCR user to write to this hold (treat all as crew) 203- **Security model:** 204 - `true`: Any authenticated user can push images (useful for shared/community holds) 205 - `false`: Only hold owner and explicit crew members can push (verified via crew records in hold's PDS) 206- **Use cases:** 207 - Public registry: `HOLD_PUBLIC=true, HOLD_ALLOW_ALL_CREW=true` 208 - ATProto users only: `HOLD_PUBLIC=false, HOLD_ALLOW_ALL_CREW=true` 209 - Private hold: `HOLD_PUBLIC=false, HOLD_ALLOW_ALL_CREW=false` (default) 210 211### Bluesky Integration 212 213#### `HOLD_BLUESKY_POSTS_ENABLED` 214- **Default:** `false` 215- **Description:** Create Bluesky posts when users push container images. Posts include image name, tag, size, and layer count. 216- **Note:** Posts are created from the hold's embedded PDS identity (did:web). Requires hold to be crawled by Bluesky relay. 217- **Enable relay crawl:** `./deploy/request-crawl.sh hold.example.com` 218 219#### `HOLD_PROFILE_AVATAR` 220- **Default:** `https://imgs.blue/evan.jarrett.net/1TpTOdtS60GdJWBYEqtK22y688jajbQ9a5kbYRFtwuqrkBAE` 221- **Description:** URL to download avatar image for hold's Bluesky profile. Downloaded and uploaded as blob during bootstrap. 222- **Note:** Avatar is stored in hold's PDS and displayed on Bluesky profile 223 224### Advanced Configuration 225 226#### `TEST_MODE` 227- **Default:** `false` 228- **Description:** Enable test mode (skips some validations). Do not use in production. 229 230#### `DISABLE_PRESIGNED_URLS` 231- **Default:** `false` 232- **Description:** Force proxy mode even with S3 configured (for testing). Disables presigned URL generation and routes all blob transfers through the hold service. 233- **Use case:** Testing, debugging, or environments where presigned URLs don't work 234 235## XRPC Endpoints 236 237Hold Service exposes two types of XRPC endpoints: 238 239### ATProto Sync Endpoints (Standard) 240- `GET /.well-known/did.json` - DID document (did:web resolution) 241- `GET /xrpc/com.atproto.sync.getRepo` - Download full repository as CAR file 242- `GET /xrpc/com.atproto.sync.getBlob` - Get blob or presigned download URL 243- `GET /xrpc/com.atproto.sync.subscribeRepos` - WebSocket firehose for real-time events 244- `GET /xrpc/com.atproto.sync.listRepos` - List all repositories (single-user PDS) 245- `GET /xrpc/com.atproto.repo.describeRepo` - Repository metadata 246- `GET /xrpc/com.atproto.repo.getRecord` - Get record by collection and rkey 247- `GET /xrpc/com.atproto.repo.listRecords` - List records in collection 248- `POST /xrpc/com.atproto.repo.deleteRecord` - Delete record (owner/crew admin only) 249 250### OCI Multipart Upload Endpoints (Custom) 251- `POST /xrpc/io.atcr.hold.initiateUpload` - Start multipart upload session 252- `POST /xrpc/io.atcr.hold.getPartUploadUrl` - Get presigned URL for uploading a part 253- `PUT /xrpc/io.atcr.hold.uploadPart` - Direct buffered part upload (alternative to presigned URLs) 254- `POST /xrpc/io.atcr.hold.completeUpload` - Finalize multipart upload 255- `POST /xrpc/io.atcr.hold.abortUpload` - Cancel multipart upload 256- `POST /xrpc/io.atcr.hold.notifyManifest` - Notify hold of manifest upload (creates layer records, Bluesky posts) 257 258## Authorization Model 259 260Hold Service uses crew membership records in its embedded PDS for access control: 261 262### Read Access (Blob Downloads) 263 264**Public Hold** (`HOLD_PUBLIC=true`): 265- Anonymous users: ✅ Allowed 266- Authenticated users: ✅ Allowed 267 268**Private Hold** (`HOLD_PUBLIC=false`): 269- Anonymous users: ❌ Forbidden 270- Authenticated users with crew membership: ✅ Allowed 271- Crew must have `blob:read` permission 272 273### Write Access (Blob Uploads) 274 275Regardless of `HOLD_PUBLIC` setting: 276- Hold owner (from captain record): ✅ Allowed 277- Crew members with `blob:write` permission: ✅ Allowed 278- Non-crew authenticated users: Depends on `HOLD_ALLOW_ALL_CREW` 279 - `HOLD_ALLOW_ALL_CREW=true`: ✅ Allowed 280 - `HOLD_ALLOW_ALL_CREW=false`: ❌ Forbidden 281 282### Authentication Method 283 284AppView uses **service tokens** from user's PDS to authenticate with hold service: 2851. AppView calls user's PDS: `com.atproto.server.getServiceAuth` with hold DID 2862. User's PDS returns a service token scoped to the hold DID 2873. AppView includes service token in XRPC requests to hold 2884. Hold validates token and checks crew membership in its embedded PDS 289 290## Deployment Scenarios 291 292### Personal Hold (Single User) 293 294Your own storage for your images: 295 296```bash 297# Hold config 298HOLD_PUBLIC_URL=https://hold.alice.com 299HOLD_OWNER=did:plc:alice-did 300HOLD_PUBLIC=false # Private (only you can pull) 301HOLD_ALLOW_ALL_CREW=false # Only you can push 302HOLD_DATABASE_DIR=/var/lib/atcr-hold 303 304# S3 storage 305STORAGE_DRIVER=s3 306AWS_ACCESS_KEY_ID=your-key 307AWS_SECRET_ACCESS_KEY=your-secret 308S3_BUCKET=alice-container-registry 309S3_ENDPOINT=https://gateway.storjshare.io # Using Storj 310``` 311 312### Shared Hold (Team/Organization) 313 314Shared storage for a team with crew members: 315 316```bash 317# Hold config 318HOLD_PUBLIC_URL=https://hold.acme.corp 319HOLD_OWNER=did:plc:acme-org-did 320HOLD_PUBLIC=false # Private reads (crew only) 321HOLD_ALLOW_ALL_CREW=false # Explicit crew membership required 322HOLD_DATABASE_DIR=/var/lib/atcr-hold 323 324# S3 storage 325STORAGE_DRIVER=s3 326AWS_ACCESS_KEY_ID=your-key 327AWS_SECRET_ACCESS_KEY=your-secret 328S3_BUCKET=acme-registry-blobs 329``` 330 331Then add crew members via XRPC or hold PDS records. 332 333### Public Hold (Community Registry) 334 335Open storage allowing anyone to push and pull: 336 337```bash 338# Hold config 339HOLD_PUBLIC_URL=https://hold.community.io 340HOLD_OWNER=did:plc:community-did 341HOLD_PUBLIC=true # Public reads (anyone can pull) 342HOLD_ALLOW_ALL_CREW=true # Any authenticated user can push 343HOLD_DATABASE_DIR=/var/lib/atcr-hold 344 345# S3 storage 346STORAGE_DRIVER=s3 347AWS_ACCESS_KEY_ID=your-key 348AWS_SECRET_ACCESS_KEY=your-secret 349S3_BUCKET=community-registry-blobs 350``` 351 352### Development/Testing 353 354Local filesystem storage for testing: 355 356```bash 357# Hold config 358HOLD_PUBLIC_URL=http://127.0.0.1:8080 359HOLD_OWNER=did:plc:your-test-did 360HOLD_PUBLIC=true 361HOLD_ALLOW_ALL_CREW=true 362HOLD_DATABASE_DIR=/tmp/atcr-hold 363 364# Filesystem storage 365STORAGE_DRIVER=filesystem 366STORAGE_ROOT_DIR=/tmp/atcr-hold-blobs 367``` 368 369## Production Deployment 370 371For production deployments with: 372- SSL/TLS certificates 373- S3 storage with presigned URLs 374- Proper access control 375- Systemd service files 376- Monitoring 377 378See **[deploy/README.md](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/deploy/README.md)** for comprehensive production deployment guide. 379 380### Quick Production Checklist 381 382Before going to production: 383 384- [ ] Set `HOLD_PUBLIC_URL` to your public HTTPS URL 385- [ ] Set `HOLD_OWNER` to your ATProto DID 386- [ ] Configure S3 storage (`STORAGE_DRIVER=s3`) 387- [ ] Set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `S3_BUCKET`, `S3_ENDPOINT` 388- [ ] Set `HOLD_DATABASE_DIR` to persistent directory 389- [ ] Configure `HOLD_PUBLIC` and `HOLD_ALLOW_ALL_CREW` for desired access model 390- [ ] Configure SSL/TLS termination (Caddy/nginx/Cloudflare) 391- [ ] Verify DID document: `curl https://hold.example.com/.well-known/did.json` 392- [ ] Test presigned URLs: Check logs for "presigned URL" messages during push 393- [ ] Monitor crew membership: `curl https://hold.example.com/xrpc/com.atproto.repo.listRecords?repo={holdDID}&collection=io.atcr.hold.crew` 394- [ ] (Optional) Enable Bluesky posts: `HOLD_BLUESKY_POSTS_ENABLED=true` 395- [ ] (Optional) Request relay crawl: `./deploy/request-crawl.sh hold.example.com` 396 397## Configuration Files Reference 398 399- **[.env.hold.example](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/.env.hold.example)** - All available environment variables with documentation 400- **[deploy/.env.prod.template](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/deploy/.env.prod.template)** - Production configuration template (includes both AppView and Hold) 401- **[deploy/README.md](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/deploy/README.md)** - Production deployment guide 402- **[AppView Documentation](https://atcr.io/r/evan.jarrett.net/atcr-appview)** - Registry API server setup 403- **[BYOS Architecture](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/docs/BYOS.md)** - Bring Your Own Storage technical design