hold

seamark.cr hold

ATCR Hold Service - Bring Your Own Storage component for ATCR

4 23 pulls 6 Updated 2h
linux/amd64 linux/arm64 Multi-arch
docker pull atcr.io/seamark.cr/hold:05b856b
Image Size Layers
36.9 MB 8
Pushed 2h
Vulnerabilities
Loading...
Pulls
23 total
Last pull 2h

ATCR Hold Service

Hold 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.

Hold Service is one component of the ATCR ecosystem:

  1. AppView — Registry API + web interface
  2. Hold Service (this component) — Storage backend with embedded PDS
  3. Credential Helper — Client-side tool for ATProto OAuth authentication
Docker Client --> AppView (resolves identity) --> User's PDS (stores manifest)
                      |
                Hold Service (generates presigned URL)
                      |
                S3/Storj/etc. (client uploads/downloads directly)

Manifests (small JSON metadata) live in users’ ATProto PDS. Blobs (large binary layers) live in hold services. AppView orchestrates the routing.

When to Run Your Own Hold

Most users can push to the default hold at https://hold01.atcr.io — you don’t need to run your own.

Run your own hold if you want to:

  • Control where your container layer data is stored (own S3 bucket, geographic region)
  • Manage access for a team or organization via crew membership
  • Run a shared hold for a community or project
  • Use a CDN pull zone for faster downloads

Prerequisites: S3-compatible storage with a bucket already created, and a domain with TLS for production.

Quick Start

1. Generate Configuration

# Build the hold binary
go build -o bin/atcr-hold ./cmd/hold

# Generate a fully-commented config file with all defaults
./bin/atcr-hold config init config-hold.yaml

Or generate config from Docker without building locally:

docker run --rm -i $(docker build -q -f Dockerfile.hold .) config init > config-hold.yaml

The generated file documents every option with inline comments. Edit only what you need.

2. Minimal Configuration

Only three things need to be set — everything else has sensible defaults:

storage:
  access_key: "YOUR_S3_ACCESS_KEY"
  secret_key: "YOUR_S3_SECRET_KEY"
  bucket: "your-bucket-name"
  endpoint: "https://gateway.storjshare.io"  # omit for AWS S3

server:
  public_url: "https://hold.example.com"

registration:
  owner_did: "did:plc:your-did-here"
  • server.public_url — Your hold’s public HTTPS URL. This becomes the hold’s did:web identity.
  • storage.bucket — S3 bucket name (must already exist).
  • 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

3. Build and Run with Docker

# Build the image
docker build -f Dockerfile.hold -t atcr-hold:latest .

# Run it
docker run -d \
  --name atcr-hold \
  -p 8080:8080 \
  -v $(pwd)/config-hold.yaml:/config.yaml:ro \
  -v atcr-hold-data:/var/lib/atcr-hold \
  atcr-hold:latest serve --config /config.yaml
  • /var/lib/atcr-hold — Persistent volume for the embedded PDS (carstore database + signing keys). Back this up.
  • Port 8080 — Default listen address. Put a reverse proxy (Caddy, nginx) in front for TLS.
  • The image is built FROM scratch — the binary includes SQLite statically linked.

Configuration

Config loads in layers: defaults → YAML file → environment variables. Later layers override earlier ones.

All 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.

S3 credentials also accept standard AWS environment variable names: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, S3_BUCKET, S3_ENDPOINT.

For the complete configuration reference with all options and defaults, see config-hold.example.yaml or run atcr-hold config init.

Access Control

Setting Who can pull Who can push
server.public: true Anyone Captain + crew with blob:write
server.public: false (default) Crew with blob:read Captain + crew with blob:write
+ registration.allow_all_crew: true (per above) Any authenticated user

The captain (set via registration.owner_did) has all permissions implicitly. blob:write implies blob:read.

Authentication 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.

See BYOS.md for the full authorization model.

Optional Subsystems

Subsystem Default Config key Notes
Admin panel Enabled admin.enabled Web UI for crew, settings, and storage management
Quotas Disabled quota.tiers Tier-based storage limits (e.g., deckhand=5GB, bosun=50GB)
Garbage collection Disabled gc.enabled Nightly cleanup of orphaned blobs and records
Vulnerability scanner Disabled scanner.secret, scanner.rescan_interval Requires separate scanner service; see SBOM_SCANNING.md
Labeler Disabled labeler.did, labeler.subscribe_url Consumes takedown labels from an ATProto labeler; purges affected records and gates GC blob cleanup
Bluesky posts Disabled registration.enable_bluesky_posts Posts push notifications from hold’s identity

Hold Identity

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.

did:plc (portable) — Set database.did_method: plc in config. Registered with plc.directory. Survives domain changes. Requires a rotation key. If database.rotation_key is not set, a new K-256 key is generated in memory only and logged once via slog.Warn — it is never written to disk. You must copy it from the startup logs into database.rotation_key in your config immediately, or you will lose the ability to update or recover the DID. Only the signing key (database.key_path, default {database.path}/signing.key) is persisted to disk automatically. Use database.did to adopt an existing DID for recovery or migration.

Verification

After starting your hold, verify it’s working:

# Health check — should return {"version":"..."}
curl https://hold.example.com/xrpc/_health

# DID document — should return valid JSON with service endpoints
curl https://hold.example.com/.well-known/did.json

# Captain record — should show your owner DID
curl "https://hold.example.com/xrpc/com.atproto.repo.listRecords?repo=HOLD_DID&collection=io.atcr.hold.captain"

# Crew records
curl "https://hold.example.com/xrpc/com.atproto.repo.listRecords?repo=HOLD_DID&collection=io.atcr.hold.crew"

Replace HOLD_DID with your hold’s DID (from the /.well-known/did.json response).

Docker Compose

services:
  atcr-hold:
    build:
      context: .
      dockerfile: Dockerfile.hold
    command: ["serve", "--config", "/config.yaml"]
    volumes:
      - ./config-hold.yaml:/config.yaml:ro
      - atcr-hold-data:/var/lib/atcr-hold
    ports:
      - "8080:8080"
    healthcheck:
      test: ["CMD", "/healthcheck", "http://localhost:8080/xrpc/_health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

volumes:
  atcr-hold-data:

For production with TLS termination, see deploy/docker-compose.prod.yml which includes a Caddy reverse proxy.

Further Reading