A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
76
fork

Configure Feed

Select the types of activity you want to include in your feed.

deployment fixes. add open graph

evan.jarrett.net 044d408c 4063544c

verified
+141 -21
+7 -2
.env.hold.example
··· 29 29 AWS_SECRET_ACCESS_KEY=your_secret_key 30 30 31 31 # S3 Region 32 - # Examples: us-east-1, us-west-2, eu-west-1 33 - # For UpCloud: us-chi1, us-nyc1, de-fra1, uk-lon1, sg-sin1 32 + # For third-party S3 providers, this is ignored when S3_ENDPOINT is set, 33 + # but must be a valid AWS region (e.g., us-east-1) to pass validation. 34 34 # Default: us-east-1 35 35 AWS_REGION=us-east-1 36 36 ··· 60 60 # Writes (pushes) always require crew membership via PDS 61 61 # Default: false 62 62 HOLD_PUBLIC=false 63 + 64 + # ATProto relay endpoint for requesting crawl on startup 65 + # This makes the hold's embedded PDS discoverable by the relay network 66 + # Default: https://bsky.network (set to empty string to disable) 67 + # HOLD_RELAY_ENDPOINT=https://bsky.network 63 68 64 69 # ============================================================================== 65 70 # Embedded PDS Configuration
+1 -1
.tangled/workflows/tests.yml
··· 6 6 7 7 engine: kubernetes 8 8 image: golang:1.25-trixie 9 - architecture: arm64 9 + architecture: amd64 10 10 11 11 steps: 12 12 - name: Download and Generate
+23 -1
Makefile
··· 2 2 # Build targets for the ATProto Container Registry 3 3 4 4 .PHONY: all build build-appview build-hold build-credential-helper build-oauth-helper \ 5 - generate test test-race test-verbose lint clean help install-credential-helper 5 + generate test test-race test-verbose lint clean help install-credential-helper \ 6 + develop develop-detached develop-down 6 7 7 8 .DEFAULT_GOAL := help 8 9 ··· 79 80 @echo "→ Installing credential helper to /usr/local/sbin..." 80 81 install -m 755 bin/docker-credential-atcr /usr/local/sbin/docker-credential-atcr 81 82 @echo "✓ Installed docker-credential-atcr to /usr/local/sbin/" 83 + 84 + ##@ Docker Targets 85 + 86 + develop: ## Build Docker images and start docker-compose for development 87 + @echo "→ Building Docker images..." 88 + docker-compose build 89 + @echo "→ Starting docker-compose..." 90 + docker-compose up 91 + 92 + develop-detached: ## Build and start docker-compose in detached mode 93 + @echo "→ Building Docker images..." 94 + docker-compose build 95 + @echo "→ Starting docker-compose (detached)..." 96 + docker-compose up -d 97 + @echo "✓ Services started in background" 98 + @echo " AppView: http://localhost:5000" 99 + @echo " Hold: http://localhost:8080" 100 + 101 + develop-down: ## Stop docker-compose services 102 + @echo "→ Stopping docker-compose..." 103 + docker-compose down 82 104 83 105 ##@ Utility Targets 84 106
+10
cmd/hold/main.go
··· 179 179 } 180 180 } 181 181 182 + // Request crawl from relay to make PDS discoverable 183 + if cfg.Server.RelayEndpoint != "" { 184 + slog.Info("Requesting crawl from relay", "relay", cfg.Server.RelayEndpoint) 185 + if err := hold.RequestCrawl(cfg.Server.RelayEndpoint, cfg.Server.PublicURL); err != nil { 186 + slog.Warn("Failed to request crawl from relay", "error", err) 187 + } else { 188 + slog.Info("Crawl requested successfully") 189 + } 190 + } 191 + 182 192 // Wait for signal or server error 183 193 select { 184 194 case err := <-serverErr:
+5 -11
deploy/.env.prod.template
··· 115 115 AWS_SECRET_ACCESS_KEY= 116 116 117 117 # S3 Region (for distribution S3 driver) 118 - # UpCloud regions: us-chi1, us-nyc1, de-fra1, uk-lon1, sg-sin1, etc. 119 - # Note: Use AWS_REGION (not S3_REGION) - this is what the hold service expects 118 + # For third-party S3 providers (UpCloud, Storj, Minio), this value is ignored 119 + # when S3_ENDPOINT is set, but must be a valid AWS region to pass validation. 120 120 # Default: us-east-1 121 - AWS_REGION=us-chi1 121 + AWS_REGION=us-east-1 122 122 123 123 # S3 Bucket Name 124 124 # Create this bucket in UpCloud Object Storage ··· 133 133 # NOTE: Use the bucket-specific endpoint, NOT a custom domain 134 134 # Custom domains break presigned URL generation 135 135 S3_ENDPOINT=https://6vmss.upcloudobjects.com 136 - 137 - # S3 Region Endpoint (alternative to S3_ENDPOINT) 138 - # Use this if your S3 driver requires region-specific endpoint format 139 - # Example: s3.us-chi1.upcloudobjects.com 140 - # S3_REGION_ENDPOINT= 141 136 142 137 # ============================================================================== 143 138 # AppView Configuration ··· 231 226 # ☐ Set HOLD_OWNER (your ATProto DID) 232 227 # ☐ Set HOLD_DATABASE_DIR (default: /var/lib/atcr-hold) - enables embedded PDS 233 228 # ☐ Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY 234 - # ☐ Set AWS_REGION (e.g., us-chi1) 235 229 # ☐ Set S3_BUCKET (created in UpCloud Object Storage) 236 - # ☐ Set S3_ENDPOINT (UpCloud endpoint or custom domain) 230 + # ☐ Set S3_ENDPOINT (UpCloud bucket endpoint, e.g., https://6vmss.upcloudobjects.com) 237 231 # ☐ Configured DNS records: 238 232 # - A record: atcr.io → server IP 239 233 # - A record: hold01.atcr.io → server IP 240 - # - CNAME: blobs.atcr.io → [bucket].us-chi1.upcloudobjects.com 234 + # - CNAME: blobs.atcr.io → [bucket].upcloudobjects.com 241 235 # ☐ Disabled Cloudflare proxy (gray cloud, not orange) 242 236 # ☐ Waited for DNS propagation (check with: dig atcr.io) 243 237 #
+1 -6
deploy/docker-compose.prod.yml
··· 109 109 # S3/UpCloud Object Storage configuration 110 110 AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-} 111 111 AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-} 112 - AWS_REGION: ${AWS_REGION:-us-chi1} 112 + AWS_REGION: ${AWS_REGION:-us-east-1} 113 113 S3_BUCKET: ${S3_BUCKET:-atcr-blobs} 114 114 S3_ENDPOINT: ${S3_ENDPOINT:-} 115 - S3_REGION_ENDPOINT: ${S3_REGION_ENDPOINT:-} 116 115 117 116 # Logging 118 117 ATCR_LOG_LEVEL: ${ATCR_LOG_LEVEL:-debug} ··· 160 159 # Preserve original host header 161 160 header_up Host {host} 162 161 header_up X-Real-IP {remote_host} 163 - header_up X-Forwarded-For {remote_host} 164 - header_up X-Forwarded-Proto {scheme} 165 162 } 166 163 167 164 # Enable compression ··· 183 180 # Preserve original host header 184 181 header_up Host {host} 185 182 header_up X-Real-IP {remote_host} 186 - header_up X-Forwarded-For {remote_host} 187 - header_up X-Forwarded-Proto {scheme} 188 183 } 189 184 190 185 # Enable compression
+1
pkg/appview/jetstream/processor_test.go
··· 70 70 platform_os TEXT, 71 71 platform_variant TEXT, 72 72 platform_os_version TEXT, 73 + is_attestation BOOLEAN DEFAULT FALSE, 73 74 reference_index INTEGER NOT NULL, 74 75 PRIMARY KEY(manifest_id, reference_index) 75 76 );
+39
pkg/appview/templates/components/head.html
··· 2 2 <meta charset="UTF-8"> 3 3 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 4 4 5 + <!-- Open Graph Meta Tags --> 6 + {{ if .ViewedUser }} 7 + <!-- User Profile Page --> 8 + <meta property="og:title" content="{{ .ViewedUser.Handle }} - ATCR"> 9 + <meta property="og:description" content="Container images by {{ .ViewedUser.Handle }} on ATCR"> 10 + {{ if .ViewedUser.Avatar }} 11 + <meta property="og:image" content="{{ .ViewedUser.Avatar }}"> 12 + {{ else }} 13 + <meta property="og:image" content="{{ .RegistryURL }}/web-app-manifest-512x512.png"> 14 + {{ end }} 15 + <meta property="og:type" content="profile"> 16 + <meta property="og:url" content="{{ .RegistryURL }}/u/{{ .ViewedUser.Handle }}"> 17 + {{ else if .Repository }} 18 + <!-- Repository Page --> 19 + <meta property="og:title" content="{{ .Owner.Handle }}/{{ .Repository.Name }} - ATCR"> 20 + {{ if .Repository.Description }} 21 + <meta property="og:description" content="{{ .Repository.Description }}"> 22 + {{ else }} 23 + <meta property="og:description" content="Container image on ATCR"> 24 + {{ end }} 25 + {{ if .Repository.IconURL }} 26 + <meta property="og:image" content="{{ .Repository.IconURL }}"> 27 + {{ else if .Owner.Avatar }} 28 + <meta property="og:image" content="{{ .Owner.Avatar }}"> 29 + {{ else }} 30 + <meta property="og:image" content="{{ .RegistryURL }}/web-app-manifest-512x512.png"> 31 + {{ end }} 32 + <meta property="og:type" content="website"> 33 + <meta property="og:url" content="{{ .RegistryURL }}/r/{{ .Owner.Handle }}/{{ .Repository.Name }}"> 34 + {{ else }} 35 + <!-- Home Page / Default --> 36 + <meta property="og:title" content="ATCR - Distributed Container Registry"> 37 + <meta property="og:description" content="Push and pull Docker images on the AT Protocol"> 38 + <meta property="og:image" content="{{ .RegistryURL }}/web-app-manifest-512x512.png"> 39 + <meta property="og:type" content="website"> 40 + <meta property="og:url" content="{{ .RegistryURL }}"> 41 + {{ end }} 42 + <meta property="og:site_name" content="ATCR"> 43 + 5 44 <!-- Favicons --> 6 45 <link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" /> 7 46 <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+54
pkg/hold/config.go
··· 6 6 package hold 7 7 8 8 import ( 9 + "bytes" 10 + "encoding/json" 9 11 "fmt" 12 + "net/http" 13 + "net/url" 10 14 "os" 11 15 "path/filepath" 12 16 "time" ··· 67 71 // DisablePresignedURLs forces proxy mode even with S3 configured (for testing) (from env: DISABLE_PRESIGNED_URLS) 68 72 DisablePresignedURLs bool `yaml:"disable_presigned_urls"` 69 73 74 + // RelayEndpoint is the ATProto relay URL to request crawl from on startup (from env: HOLD_RELAY_ENDPOINT) 75 + // If empty, no crawl request is made. Default: https://bsky.network 76 + RelayEndpoint string `yaml:"relay_endpoint"` 77 + 70 78 // ReadTimeout for HTTP requests 71 79 ReadTimeout time.Duration `yaml:"read_timeout"` 72 80 ··· 103 111 cfg.Server.Public = os.Getenv("HOLD_PUBLIC") == "true" 104 112 cfg.Server.TestMode = os.Getenv("TEST_MODE") == "true" 105 113 cfg.Server.DisablePresignedURLs = os.Getenv("DISABLE_PRESIGNED_URLS") == "true" 114 + cfg.Server.RelayEndpoint = getEnvOrDefault("HOLD_RELAY_ENDPOINT", "https://bsky.network") 106 115 cfg.Server.ReadTimeout = 5 * time.Minute // Increased for large blob uploads 107 116 cfg.Server.WriteTimeout = 5 * time.Minute // Increased for large blob uploads 108 117 ··· 180 189 } 181 190 return defaultValue 182 191 } 192 + 193 + // RequestCrawl sends a crawl request to the ATProto relay for the given hostname. 194 + // This makes the hold's PDS discoverable by the relay network. 195 + func RequestCrawl(relayEndpoint, publicURL string) error { 196 + if relayEndpoint == "" { 197 + return nil // No relay configured, skip 198 + } 199 + 200 + // Extract hostname from public URL 201 + parsed, err := url.Parse(publicURL) 202 + if err != nil { 203 + return fmt.Errorf("failed to parse public URL: %w", err) 204 + } 205 + hostname := parsed.Host 206 + 207 + // Build the request URL 208 + requestURL := relayEndpoint + "/xrpc/com.atproto.sync.requestCrawl" 209 + 210 + // Create request body 211 + body := map[string]string{"hostname": hostname} 212 + bodyJSON, err := json.Marshal(body) 213 + if err != nil { 214 + return fmt.Errorf("failed to marshal request body: %w", err) 215 + } 216 + 217 + // Make the request 218 + client := &http.Client{Timeout: 10 * time.Second} 219 + req, err := http.NewRequest("POST", requestURL, bytes.NewReader(bodyJSON)) 220 + if err != nil { 221 + return fmt.Errorf("failed to create request: %w", err) 222 + } 223 + req.Header.Set("Content-Type", "application/json") 224 + 225 + resp, err := client.Do(req) 226 + if err != nil { 227 + return fmt.Errorf("failed to send request: %w", err) 228 + } 229 + defer resp.Body.Close() 230 + 231 + if resp.StatusCode < 200 || resp.StatusCode >= 300 { 232 + return fmt.Errorf("relay returned status %d", resp.StatusCode) 233 + } 234 + 235 + return nil 236 + }