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
3Hold Service is the BYOS (Bring Your Own Storage) blob storage backend for ATCR. It stores container image layers in your own S3-compatible storage (AWS S3, Storj, Minio, UpCloud, etc.) and generates presigned URLs so clients transfer data directly to/from S3. Each hold runs an embedded ATProto PDS with its own DID, repository, and crew-based access control.
4
5Hold Service is one component of the ATCR ecosystem:
6
71. **[AppView](https://atcr.io/r/evan.jarrett.net/atcr-appview)** — Registry API + web interface
82. **Hold Service** (this component) — Storage backend with embedded PDS
93. **Credential Helper** — Client-side tool for ATProto OAuth authentication
10
11```
12Docker Client --> AppView (resolves identity) --> User's PDS (stores manifest)
13 |
14 Hold Service (generates presigned URL)
15 |
16 S3/Storj/etc. (client uploads/downloads directly)
17```
18
19Manifests (small JSON metadata) live in users' ATProto PDS. Blobs (large binary layers) live in hold services. AppView orchestrates the routing.
20
21## When to Run Your Own Hold
22
23Most users can push to the default hold at **https://hold01.atcr.io** — you don't need to run your own.
24
25Run your own hold if you want to:
26- Control where your container layer data is stored (own S3 bucket, geographic region)
27- Manage access for a team or organization via crew membership
28- Run a shared hold for a community or project
29- Use a CDN pull zone for faster downloads
30
31**Prerequisites:** S3-compatible storage with a bucket already created, and a domain with TLS for production.
32
33## Quick Start
34
35### 1. Generate Configuration
36
37```bash
38# Build the hold binary
39go build -o bin/atcr-hold ./cmd/hold
40
41# Generate a fully-commented config file with all defaults
42./bin/atcr-hold config init config-hold.yaml
43```
44
45Or generate config from Docker without building locally:
46
47```bash
48docker run --rm -i $(docker build -q -f Dockerfile.hold .) config init > config-hold.yaml
49```
50
51The generated file documents every option with inline comments. Edit only what you need.
52
53### 2. Minimal Configuration
54
55Only three things need to be set — everything else has sensible defaults:
56
57```yaml
58storage:
59 access_key: "YOUR_S3_ACCESS_KEY"
60 secret_key: "YOUR_S3_SECRET_KEY"
61 bucket: "your-bucket-name"
62 endpoint: "https://gateway.storjshare.io" # omit for AWS S3
63
64server:
65 public_url: "https://hold.example.com"
66
67registration:
68 owner_did: "did:plc:your-did-here"
69```
70
71- **`server.public_url`** — Your hold's public HTTPS URL. This becomes the hold's `did:web` identity.
72- **`storage.bucket`** — S3 bucket name (must already exist).
73- **`registration.owner_did`** — Your ATProto DID. Creates you as captain (admin) on first boot. Get yours from: `https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=yourhandle.bsky.social`
74
75### 3. Build and Run with Docker
76
77```bash
78# Build the image
79docker build -f Dockerfile.hold -t atcr-hold:latest .
80
81# Run it
82docker run -d \
83 --name atcr-hold \
84 -p 8080:8080 \
85 -v $(pwd)/config-hold.yaml:/config.yaml:ro \
86 -v atcr-hold-data:/var/lib/atcr-hold \
87 atcr-hold:latest serve --config /config.yaml
88```
89
90- **`/var/lib/atcr-hold`** — Persistent volume for the embedded PDS (carstore database + signing keys). Back this up.
91- **Port 8080** — Default listen address. Put a reverse proxy (Caddy, nginx) in front for TLS.
92- The image is built `FROM scratch` — the binary includes SQLite statically linked.
93- Optional: `docker build --build-arg BILLING_ENABLED=true` to include Stripe billing support.
94
95## Configuration
96
97Config loads in layers: **defaults → YAML file → environment variables**. Later layers override earlier ones.
98
99All YAML fields can be overridden with environment variables using the `HOLD_` prefix and `_` path separators. For example, `server.public_url` becomes `HOLD_SERVER_PUBLIC_URL`.
100
101S3 credentials also accept standard AWS environment variable names: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `S3_BUCKET`, `S3_ENDPOINT`.
102
103For the complete configuration reference with all options and defaults, see [`config-hold.example.yaml`](../config-hold.example.yaml) or run `atcr-hold config init`.
104
105## Access Control
106
107| Setting | Who can pull | Who can push |
108|---|---|---|
109| `server.public: true` | Anyone | Captain + crew with `blob:write` |
110| `server.public: false` (default) | Crew with `blob:read` | Captain + crew with `blob:write` |
111| + `registration.allow_all_crew: true` | (per above) | Any authenticated user |
112
113The captain (set via `registration.owner_did`) has all permissions implicitly. `blob:write` implies `blob:read`.
114
115Authentication uses ATProto service tokens: AppView requests a token from the user's PDS scoped to the hold's DID, then includes it in XRPC requests. The hold validates the token and checks crew membership.
116
117See [BYOS.md](BYOS.md) for the full authorization model.
118
119## Optional Subsystems
120
121| Subsystem | Default | Config key | Notes |
122|---|---|---|---|
123| Admin panel | Enabled | `admin.enabled` | Web UI for crew, settings, and storage management |
124| Quotas | Disabled | `quota.tiers` | Tier-based storage limits (e.g., deckhand=5GB, bosun=50GB) |
125| Garbage collection | Disabled | `gc.enabled` | Nightly cleanup of orphaned blobs and records |
126| Vulnerability scanner | Disabled | `scanner.secret` | Requires separate scanner service; see [SBOM_SCANNING.md](SBOM_SCANNING.md) |
127| Billing (Stripe) | Disabled | Build flag + env | Build with `--build-arg BILLING_ENABLED=true`; see [BILLING.md](BILLING.md) |
128| Bluesky posts | Disabled | `registration.enable_bluesky_posts` | Posts push notifications from hold's identity |
129
130## Hold Identity
131
132**did:web (default)** — Derived from `server.public_url` with zero setup. `https://hold.example.com` becomes `did:web:hold.example.com`. The DID document is served at `/.well-known/did.json`. Tied to domain ownership — if you lose the domain, you lose the identity.
133
134**did:plc (portable)** — Set `database.did_method: plc` in config. Registered with plc.directory. Survives domain changes. Requires a rotation key (auto-generated at `{database.path}/rotation.key`). Use `database.did` to adopt an existing DID for recovery or migration.
135
136## Verification
137
138After starting your hold, verify it's working:
139
140```bash
141# Health check — should return {"version":"..."}
142curl https://hold.example.com/xrpc/_health
143
144# DID document — should return valid JSON with service endpoints
145curl https://hold.example.com/.well-known/did.json
146
147# Captain record — should show your owner DID
148curl "https://hold.example.com/xrpc/com.atproto.repo.listRecords?repo=HOLD_DID&collection=io.atcr.hold.captain"
149
150# Crew records
151curl "https://hold.example.com/xrpc/com.atproto.repo.listRecords?repo=HOLD_DID&collection=io.atcr.hold.crew"
152```
153
154Replace `HOLD_DID` with your hold's DID (from the `/.well-known/did.json` response).
155
156## Docker Compose
157
158```yaml
159services:
160 atcr-hold:
161 build:
162 context: .
163 dockerfile: Dockerfile.hold
164 command: ["serve", "--config", "/config.yaml"]
165 volumes:
166 - ./config-hold.yaml:/config.yaml:ro
167 - atcr-hold-data:/var/lib/atcr-hold
168 ports:
169 - "8080:8080"
170 healthcheck:
171 test: ["CMD", "/healthcheck", "http://localhost:8080/xrpc/_health"]
172 interval: 30s
173 timeout: 10s
174 retries: 3
175 start_period: 30s
176
177volumes:
178 atcr-hold-data:
179```
180
181For production with TLS termination, see [`deploy/docker-compose.prod.yml`](../deploy/docker-compose.prod.yml) which includes a Caddy reverse proxy.
182
183## Further Reading
184
185- [`config-hold.example.yaml`](../config-hold.example.yaml) — Complete configuration reference with inline comments
186- [BYOS.md](BYOS.md) — Bring Your Own Storage architecture and authorization model
187- [HOLD_XRPC_ENDPOINTS.md](HOLD_XRPC_ENDPOINTS.md) — XRPC endpoint reference
188- [BILLING.md](BILLING.md) — Stripe billing integration
189- [QUOTAS.md](QUOTAS.md) — Quota management
190- [SBOM_SCANNING.md](SBOM_SCANNING.md) — Vulnerability scanning