A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
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