mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-18 16:24:16 +00:00
add deployment scripts
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
# ATCR Production Environment Configuration
|
||||
# Copy this file to .env and fill in your values
|
||||
#
|
||||
# Usage:
|
||||
# 1. cp deploy/.env.prod.template .env
|
||||
# 2. Edit .env with your configuration
|
||||
# 3. systemctl restart atcr
|
||||
#
|
||||
# NOTE: This file is loaded by docker-compose.prod.yml
|
||||
|
||||
# ==============================================================================
|
||||
# Domain Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Main AppView domain (registry API + web UI)
|
||||
# REQUIRED: Update with your domain
|
||||
APPVIEW_DOMAIN=atcr.io
|
||||
|
||||
# Hold service domain (presigned URL generator)
|
||||
# REQUIRED: Update with your domain
|
||||
HOLD_DOMAIN=hold01.atcr.io
|
||||
|
||||
# ==============================================================================
|
||||
# Hold Service Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Your ATProto DID (REQUIRED for hold registration)
|
||||
# Get your DID from: https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=yourhandle.bsky.social
|
||||
# Example: did:plc:abc123xyz789
|
||||
HOLD_OWNER=did:plc:pddp4xt5lgnv2qsegbzzs4xg
|
||||
|
||||
# Allow public blob reads (pulls) without authentication
|
||||
# - true: Anyone can pull images (read-only)
|
||||
# - false: Only authenticated users can pull
|
||||
# Default: false (private)
|
||||
HOLD_PUBLIC=false
|
||||
|
||||
# ==============================================================================
|
||||
# S3/UpCloud Object Storage Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Storage driver type
|
||||
# Options: s3, filesystem
|
||||
# Default: s3
|
||||
STORAGE_DRIVER=s3
|
||||
|
||||
# S3 Access Credentials
|
||||
# Get these from UpCloud Object Storage console
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
|
||||
# S3 Region
|
||||
# UpCloud regions: us-chi1, us-nyc1, de-fra1, uk-lon1, sg-sin1, etc.
|
||||
# Default: us-chi1
|
||||
AWS_REGION=us-chi1
|
||||
|
||||
# S3 Bucket Name
|
||||
# Create this bucket in UpCloud Object Storage
|
||||
# Example: atcr-blobs
|
||||
S3_BUCKET=atcr
|
||||
|
||||
# S3 Endpoint (for custom domain or UpCloud endpoint)
|
||||
# If using custom domain (blobs.atcr.io):
|
||||
# S3_ENDPOINT=https://blobs.atcr.io
|
||||
# If using UpCloud default endpoint:
|
||||
# S3_ENDPOINT=https://s3.us-chi1.upcloudobjects.com
|
||||
#
|
||||
# IMPORTANT: If using custom domain, create CNAME:
|
||||
# blobs.atcr.io → [bucket].us-chi1.upcloudobjects.com
|
||||
# (with Cloudflare proxy DISABLED - gray cloud)
|
||||
S3_ENDPOINT=https://blobs.atcr.io
|
||||
|
||||
# S3 Region Endpoint (alternative to S3_ENDPOINT)
|
||||
# Use this if your S3 driver requires region-specific endpoint format
|
||||
# Example: s3.us-chi1.upcloudobjects.com
|
||||
# S3_REGION_ENDPOINT=
|
||||
|
||||
# ==============================================================================
|
||||
# AppView Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# JWT token expiration in seconds
|
||||
# Default: 300 (5 minutes)
|
||||
ATCR_TOKEN_EXPIRATION=300
|
||||
|
||||
# Enable web UI
|
||||
# Default: true
|
||||
ATCR_UI_ENABLED=true
|
||||
|
||||
# ==============================================================================
|
||||
# Logging Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Log level: debug, info, warn, error
|
||||
# Default: info
|
||||
ATCR_LOG_LEVEL=info
|
||||
|
||||
# Log formatter: text, json
|
||||
# Default: text
|
||||
ATCR_LOG_FORMATTER=text
|
||||
|
||||
# ==============================================================================
|
||||
# Jetstream Configuration (ATProto event streaming)
|
||||
# ==============================================================================
|
||||
|
||||
# Jetstream WebSocket URL for real-time ATProto events
|
||||
# Default: wss://jetstream2.us-west.bsky.network/subscribe
|
||||
JETSTREAM_URL=wss://jetstream2.us-west.bsky.network/subscribe
|
||||
|
||||
# Enable backfill worker to sync historical records
|
||||
# Default: true (recommended for production)
|
||||
ATCR_BACKFILL_ENABLED=true
|
||||
|
||||
# ATProto relay endpoint for backfill sync API
|
||||
# Default: https://relay1.us-east.bsky.network
|
||||
ATCR_RELAY_ENDPOINT=https://relay1.us-east.bsky.network
|
||||
|
||||
# Backfill interval
|
||||
# Examples: 30m, 1h, 2h, 24h
|
||||
# Default: 1h
|
||||
ATCR_BACKFILL_INTERVAL=1h
|
||||
|
||||
# ==============================================================================
|
||||
# Optional: Filesystem Storage (alternative to S3)
|
||||
# ==============================================================================
|
||||
|
||||
# If using filesystem storage instead of S3:
|
||||
# 1. Uncomment these lines
|
||||
# 2. Comment out all S3 variables above
|
||||
# 3. Set STORAGE_DRIVER=filesystem
|
||||
|
||||
# STORAGE_DRIVER=filesystem
|
||||
# STORAGE_ROOT_DIR=/var/lib/atcr/hold
|
||||
|
||||
# ==============================================================================
|
||||
# Advanced Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Override service name (defaults to APPVIEW_DOMAIN)
|
||||
# ATCR_SERVICE_NAME=atcr.io
|
||||
|
||||
# Debug listen address (optional - for pprof debugging)
|
||||
# ATCR_DEBUG_ADDR=:5001
|
||||
|
||||
# ==============================================================================
|
||||
# CHECKLIST
|
||||
# ==============================================================================
|
||||
#
|
||||
# Before starting ATCR, ensure you have:
|
||||
#
|
||||
# ☐ Set APPVIEW_DOMAIN (e.g., atcr.io)
|
||||
# ☐ Set HOLD_DOMAIN (e.g., hold01.atcr.io)
|
||||
# ☐ Set HOLD_OWNER (your ATProto DID)
|
||||
# ☐ Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
|
||||
# ☐ Set S3_BUCKET (created in UpCloud Object Storage)
|
||||
# ☐ Set S3_ENDPOINT (UpCloud endpoint or custom domain)
|
||||
# ☐ Configured DNS records:
|
||||
# - A record: atcr.io → server IP
|
||||
# - A record: hold01.atcr.io → server IP
|
||||
# - CNAME: blobs.atcr.io → [bucket].us-chi1.upcloudobjects.com
|
||||
# ☐ Disabled Cloudflare proxy (gray cloud, not orange)
|
||||
# ☐ Waited for DNS propagation (check with: dig atcr.io)
|
||||
#
|
||||
# After starting:
|
||||
# ☐ Complete hold OAuth registration (run: /opt/atcr/get-hold-oauth.sh)
|
||||
# ☐ Test registry: docker pull atcr.io/test/image
|
||||
# ☐ Monitor logs: /opt/atcr/logs.sh
|
||||
@@ -0,0 +1,503 @@
|
||||
# ATCR UpCloud Deployment Guide
|
||||
|
||||
This guide walks you through deploying ATCR on UpCloud with Rocky Linux.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **AppView** (atcr.io) - OCI registry API + web UI
|
||||
- **Hold Service** (hold01.atcr.io) - Presigned URL generator for blob storage
|
||||
- **Caddy** - Reverse proxy with automatic HTTPS
|
||||
- **UpCloud Object Storage** (blobs.atcr.io) - S3-compatible blob storage
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### 1. UpCloud Account
|
||||
- Active UpCloud account
|
||||
- Object Storage enabled
|
||||
- Billing configured
|
||||
|
||||
### 2. Domain Names
|
||||
You need three DNS records:
|
||||
- `atcr.io` (or your domain) - AppView
|
||||
- `hold01.atcr.io` - Hold service
|
||||
- `blobs.atcr.io` - S3 storage (CNAME)
|
||||
|
||||
### 3. ATProto Account
|
||||
- Bluesky/ATProto account
|
||||
- Your DID (get from: `https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=yourhandle.bsky.social`)
|
||||
|
||||
### 4. UpCloud Object Storage Bucket
|
||||
Create an S3 bucket in UpCloud Object Storage:
|
||||
1. Go to UpCloud Console → Storage → Object Storage
|
||||
2. Create new bucket (e.g., `atcr-blobs`)
|
||||
3. Note the region (e.g., `us-chi1`)
|
||||
4. Generate access credentials (Access Key ID + Secret)
|
||||
5. Note the endpoint (e.g., `s3.us-chi1.upcloudobjects.com`)
|
||||
|
||||
## Deployment Steps
|
||||
|
||||
### Step 1: Configure DNS
|
||||
|
||||
Set up DNS records (using Cloudflare or your DNS provider):
|
||||
|
||||
```
|
||||
Type Name Value Proxy
|
||||
────────────────────────────────────────────────────────────────────────────
|
||||
A atcr.io [your-upcloud-ip] ☁️ DISABLED
|
||||
A hold01.atcr.io [your-upcloud-ip] ☁️ DISABLED
|
||||
CNAME blobs.atcr.io atcr-blobs.us-chi1.upcloudobjects.com ☁️ DISABLED
|
||||
```
|
||||
|
||||
**IMPORTANT:**
|
||||
- **DISABLE Cloudflare proxy** (gray cloud, not orange) for all three domains
|
||||
- Proxied connections break Docker registry protocol and presigned URLs
|
||||
- You'll still get HTTPS via Caddy's Let's Encrypt integration
|
||||
|
||||
Wait for DNS propagation (5-30 minutes). Verify with:
|
||||
```bash
|
||||
dig atcr.io
|
||||
dig hold01.atcr.io
|
||||
dig blobs.atcr.io
|
||||
```
|
||||
|
||||
### Step 2: Create UpCloud Server
|
||||
|
||||
1. Go to UpCloud Console → Servers → Deploy a new server
|
||||
2. Select location (match your S3 region if possible)
|
||||
3. Select **Rocky Linux 9** operating system
|
||||
4. Choose plan (minimum: 2 GB RAM, 1 CPU)
|
||||
5. Configure hostname: `atcr`
|
||||
6. Enable IPv4 public networking
|
||||
7. **Optional:** Enable IPv6
|
||||
8. **User data:** Paste contents of `deploy/init-upcloud.sh`
|
||||
- Update `ATCR_REPO` variable with your git repository URL
|
||||
- Or leave empty and manually copy files later
|
||||
9. Create SSH key or use password authentication
|
||||
10. Click **Deploy**
|
||||
|
||||
### Step 3: Wait for Initialization
|
||||
|
||||
The init script will:
|
||||
- Update system packages (~2-5 minutes)
|
||||
- Install Docker and Docker Compose
|
||||
- Configure firewall
|
||||
- Clone repository (if ATCR_REPO configured)
|
||||
- Create systemd service
|
||||
- Create helper scripts
|
||||
|
||||
Monitor progress:
|
||||
```bash
|
||||
# SSH into server
|
||||
ssh root@[your-upcloud-ip]
|
||||
|
||||
# Check cloud-init logs
|
||||
tail -f /var/log/cloud-init-output.log
|
||||
```
|
||||
|
||||
Wait for the completion message in the logs.
|
||||
|
||||
### Step 4: Configure Environment
|
||||
|
||||
Edit the environment configuration:
|
||||
|
||||
```bash
|
||||
# SSH into server
|
||||
ssh root@[your-upcloud-ip]
|
||||
|
||||
# Edit environment file
|
||||
cd /opt/atcr
|
||||
nano .env
|
||||
```
|
||||
|
||||
**Required configuration:**
|
||||
|
||||
```bash
|
||||
# Domains
|
||||
APPVIEW_DOMAIN=atcr.io
|
||||
HOLD_DOMAIN=hold01.atcr.io
|
||||
|
||||
# Your ATProto DID
|
||||
HOLD_OWNER=did:plc:your-did-here
|
||||
|
||||
# UpCloud S3 credentials
|
||||
AWS_ACCESS_KEY_ID=your-access-key-id
|
||||
AWS_SECRET_ACCESS_KEY=your-secret-access-key
|
||||
AWS_REGION=us-chi1
|
||||
S3_BUCKET=atcr-blobs
|
||||
|
||||
# S3 endpoint (choose one):
|
||||
# Option 1: Custom domain (recommended)
|
||||
S3_ENDPOINT=https://blobs.atcr.io
|
||||
# Option 2: Direct UpCloud endpoint
|
||||
# S3_ENDPOINT=https://s3.us-chi1.upcloudobjects.com
|
||||
|
||||
# Public access (optional)
|
||||
HOLD_PUBLIC=false # Set to true to allow anonymous pulls
|
||||
```
|
||||
|
||||
Save and exit (Ctrl+X, Y, Enter).
|
||||
|
||||
### Step 5: Start ATCR
|
||||
|
||||
```bash
|
||||
# Start services
|
||||
systemctl start atcr
|
||||
|
||||
# Check status
|
||||
systemctl status atcr
|
||||
|
||||
# Verify containers are running
|
||||
docker ps
|
||||
```
|
||||
|
||||
You should see three containers:
|
||||
- `atcr-caddy`
|
||||
- `atcr-appview`
|
||||
- `atcr-hold`
|
||||
|
||||
### Step 6: Complete Hold OAuth Registration
|
||||
|
||||
The hold service needs to register itself with your PDS:
|
||||
|
||||
```bash
|
||||
# Get OAuth URL from logs
|
||||
/opt/atcr/get-hold-oauth.sh
|
||||
```
|
||||
|
||||
Look for output like:
|
||||
```
|
||||
Visit this URL to authorize: https://bsky.social/oauth/authorize?...
|
||||
```
|
||||
|
||||
1. Copy the URL and open in your browser
|
||||
2. Log in with your ATProto account
|
||||
3. Authorize the hold service
|
||||
4. Return to terminal
|
||||
|
||||
The hold service will create records in your PDS:
|
||||
- `io.atcr.hold` - Hold definition
|
||||
- `io.atcr.hold.crew` - Your membership as captain
|
||||
|
||||
Verify registration:
|
||||
```bash
|
||||
docker logs atcr-hold | grep -i "success\|registered\|created"
|
||||
```
|
||||
|
||||
### Step 7: Test the Registry
|
||||
|
||||
#### Test 1: Check endpoints
|
||||
|
||||
```bash
|
||||
# AppView (should return {})
|
||||
curl https://atcr.io/v2/
|
||||
|
||||
# Hold service (should return {"status":"ok"})
|
||||
curl https://hold01.atcr.io/health
|
||||
```
|
||||
|
||||
#### Test 2: Configure Docker client
|
||||
|
||||
On your local machine:
|
||||
|
||||
```bash
|
||||
# Install credential helper
|
||||
# (Build from source or download release)
|
||||
go install atcr.io/cmd/docker-credential-atcr@latest
|
||||
|
||||
# Configure Docker
|
||||
docker-credential-atcr configure
|
||||
|
||||
# Enter your ATProto handle when prompted
|
||||
# Complete OAuth flow in browser
|
||||
```
|
||||
|
||||
#### Test 3: Push a test image
|
||||
|
||||
```bash
|
||||
# Tag an image
|
||||
docker tag alpine:latest atcr.io/yourhandle/test:latest
|
||||
|
||||
# Push to ATCR
|
||||
docker push atcr.io/yourhandle/test:latest
|
||||
|
||||
# Pull from ATCR
|
||||
docker pull atcr.io/yourhandle/test:latest
|
||||
```
|
||||
|
||||
### Step 8: Monitor and Maintain
|
||||
|
||||
#### View logs
|
||||
|
||||
```bash
|
||||
# All services
|
||||
/opt/atcr/logs.sh
|
||||
|
||||
# Specific service
|
||||
/opt/atcr/logs.sh atcr-appview
|
||||
/opt/atcr/logs.sh atcr-hold
|
||||
/opt/atcr/logs.sh atcr-caddy
|
||||
|
||||
# Or use docker directly
|
||||
docker logs -f atcr-appview
|
||||
```
|
||||
|
||||
#### Restart services
|
||||
|
||||
```bash
|
||||
# Restart all
|
||||
systemctl restart atcr
|
||||
|
||||
# Or use docker-compose
|
||||
cd /opt/atcr
|
||||
docker compose -f deploy/docker-compose.prod.yml restart
|
||||
```
|
||||
|
||||
#### Rebuild after code changes
|
||||
|
||||
```bash
|
||||
/opt/atcr/rebuild.sh
|
||||
```
|
||||
|
||||
#### Update configuration
|
||||
|
||||
```bash
|
||||
# Edit environment
|
||||
nano /opt/atcr/.env
|
||||
|
||||
# Restart services
|
||||
systemctl restart atcr
|
||||
```
|
||||
|
||||
## Architecture Details
|
||||
|
||||
### Service Communication
|
||||
|
||||
```
|
||||
Internet
|
||||
↓
|
||||
Caddy (443) ───────────┐
|
||||
├─→ atcr-appview:5000 (Registry API + Web UI)
|
||||
└─→ atcr-hold:8080 (Presigned URL generator)
|
||||
↓
|
||||
UpCloud S3 (blobs.atcr.io)
|
||||
```
|
||||
|
||||
### Data Flow: Push
|
||||
|
||||
```
|
||||
1. docker push atcr.io/user/image:tag
|
||||
2. AppView ← Docker client (manifest + blob metadata)
|
||||
3. AppView → ATProto PDS (store manifest record)
|
||||
4. Hold ← Docker client (request presigned URL)
|
||||
5. Hold → UpCloud S3 API (generate presigned URL)
|
||||
6. Hold → Docker client (return presigned URL)
|
||||
7. UpCloud S3 ← Docker client (upload blob directly)
|
||||
```
|
||||
|
||||
### Data Flow: Pull
|
||||
|
||||
```
|
||||
1. docker pull atcr.io/user/image:tag
|
||||
2. AppView ← Docker client (get manifest)
|
||||
3. AppView → ATProto PDS (fetch manifest record)
|
||||
4. AppView → Docker client (return manifest with holdEndpoint)
|
||||
5. Hold ← Docker client (request presigned URL)
|
||||
6. Hold → UpCloud S3 API (generate presigned URL)
|
||||
7. Hold → Docker client (return presigned URL)
|
||||
8. UpCloud S3 ← Docker client (download blob directly)
|
||||
```
|
||||
|
||||
**Key insight:** The hold service only generates presigned URLs. Actual data transfer happens directly between Docker clients and S3, minimizing bandwidth costs.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: "Cannot connect to registry"
|
||||
|
||||
**Check DNS:**
|
||||
```bash
|
||||
dig atcr.io
|
||||
dig hold01.atcr.io
|
||||
```
|
||||
|
||||
**Check Caddy logs:**
|
||||
```bash
|
||||
docker logs atcr-caddy
|
||||
```
|
||||
|
||||
**Check firewall:**
|
||||
```bash
|
||||
firewall-cmd --list-all
|
||||
```
|
||||
|
||||
### Issue: "Certificate errors"
|
||||
|
||||
**Verify DNS is propagated:**
|
||||
```bash
|
||||
curl -I https://atcr.io
|
||||
```
|
||||
|
||||
**Check Caddy is obtaining certificates:**
|
||||
```bash
|
||||
docker logs atcr-caddy | grep -i certificate
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
- DNS not propagated (wait 30 minutes)
|
||||
- Cloudflare proxy enabled (must be disabled)
|
||||
- Port 80/443 blocked by firewall
|
||||
|
||||
### Issue: "Presigned URLs fail"
|
||||
|
||||
**Check S3 endpoint configuration:**
|
||||
```bash
|
||||
docker exec atcr-hold env | grep S3
|
||||
```
|
||||
|
||||
**Verify custom domain CNAME:**
|
||||
```bash
|
||||
dig blobs.atcr.io CNAME
|
||||
```
|
||||
|
||||
**Test S3 connectivity:**
|
||||
```bash
|
||||
docker exec atcr-hold wget -O- https://blobs.atcr.io/
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
- Cloudflare proxy enabled on blobs.atcr.io
|
||||
- S3_ENDPOINT misconfigured
|
||||
- AWS credentials invalid
|
||||
|
||||
### Issue: "Hold registration fails"
|
||||
|
||||
**Check hold owner DID:**
|
||||
```bash
|
||||
docker exec atcr-hold env | grep HOLD_OWNER
|
||||
```
|
||||
|
||||
**Verify OAuth flow:**
|
||||
```bash
|
||||
/opt/atcr/get-hold-oauth.sh
|
||||
```
|
||||
|
||||
**Manual registration:**
|
||||
```bash
|
||||
# Get fresh OAuth URL
|
||||
docker restart atcr-hold
|
||||
docker logs -f atcr-hold
|
||||
```
|
||||
|
||||
### Issue: "High bandwidth usage"
|
||||
|
||||
Presigned URLs should eliminate hold bandwidth. If seeing high usage:
|
||||
|
||||
**Verify presigned URLs are enabled:**
|
||||
```bash
|
||||
docker logs atcr-hold | grep -i presigned
|
||||
```
|
||||
|
||||
**Check S3 driver:**
|
||||
```bash
|
||||
docker exec atcr-hold env | grep STORAGE_DRIVER
|
||||
# Should be: s3 (not filesystem)
|
||||
```
|
||||
|
||||
**Verify direct S3 access:**
|
||||
```bash
|
||||
# Push should show 307 redirects in logs
|
||||
docker logs -f atcr-hold
|
||||
# Then push an image
|
||||
```
|
||||
|
||||
## Security Hardening
|
||||
|
||||
### Firewall
|
||||
|
||||
```bash
|
||||
# Allow only necessary ports
|
||||
firewall-cmd --permanent --remove-service=cockpit
|
||||
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="your-ip" service name="ssh" accept'
|
||||
firewall-cmd --reload
|
||||
```
|
||||
|
||||
### Automatic Updates
|
||||
|
||||
```bash
|
||||
# Install automatic updates
|
||||
dnf install -y dnf-automatic
|
||||
|
||||
# Enable timer
|
||||
systemctl enable --now dnf-automatic.timer
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
```bash
|
||||
# Install monitoring tools
|
||||
dnf install -y htop iotop nethogs
|
||||
|
||||
# Monitor resources
|
||||
htop
|
||||
|
||||
# Monitor Docker
|
||||
docker stats
|
||||
```
|
||||
|
||||
### Backups
|
||||
|
||||
Critical data to backup:
|
||||
- `/opt/atcr/.env` - Configuration
|
||||
- Docker volumes:
|
||||
- `atcr-appview-data` - Auth keys, UI database, OAuth tokens
|
||||
- `caddy_data` - TLS certificates
|
||||
|
||||
```bash
|
||||
# Backup volumes
|
||||
docker run --rm \
|
||||
-v atcr-appview-data:/data \
|
||||
-v /backup:/backup \
|
||||
alpine tar czf /backup/atcr-appview-data.tar.gz /data
|
||||
```
|
||||
|
||||
## Scaling Considerations
|
||||
|
||||
### Single Server (Current Setup)
|
||||
- Suitable for: 100-1000 users
|
||||
- Bottleneck: AppView CPU (manifest queries)
|
||||
- Storage: Unlimited (S3)
|
||||
|
||||
### Multi-Server (Future)
|
||||
- Multiple AppView instances behind load balancer
|
||||
- Shared Redis for hold cache (replace in-memory cache)
|
||||
- PostgreSQL for UI database (replace SQLite)
|
||||
- Multiple hold services (geo-distributed)
|
||||
|
||||
## Cost Estimation
|
||||
|
||||
**UpCloud Server:**
|
||||
- 2 GB RAM / 1 CPU: ~$15/month
|
||||
- 4 GB RAM / 2 CPU: ~$30/month
|
||||
|
||||
**UpCloud Object Storage:**
|
||||
- Storage: $0.01/GB/month
|
||||
- Egress: $0.01/GB (first 1TB free in some regions)
|
||||
|
||||
**Example monthly cost:**
|
||||
- Server: $15
|
||||
- Storage (100GB): $1
|
||||
- Transfer (500GB): $5
|
||||
- **Total: ~$21/month**
|
||||
|
||||
**Bandwidth optimization:**
|
||||
- Presigned URLs mean hold service uses minimal bandwidth
|
||||
- Most costs are S3 storage + transfer (not server bandwidth)
|
||||
|
||||
## Support
|
||||
|
||||
- Documentation: https://tangled.org/@evan.jarrett.net/at-container-registry
|
||||
- Issues: https://github.com/your-org/atcr.io/issues
|
||||
- Bluesky: @yourhandle.bsky.social
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,194 @@
|
||||
# ATCR Production Deployment with Caddy
|
||||
# For UpCloud Rocky Linux deployment
|
||||
#
|
||||
# Usage:
|
||||
# 1. Copy .env.prod.template to .env and fill in your values
|
||||
# 2. docker compose -f deploy/docker-compose.prod.yml up -d
|
||||
#
|
||||
# Domains:
|
||||
# - atcr.io → AppView (registry API + web UI)
|
||||
# - hold01.atcr.io → Hold service (presigned URL generator)
|
||||
# - blobs.atcr.io → S3 object storage (CNAME to UpCloud S3)
|
||||
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
container_name: atcr-caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp" # HTTP/3
|
||||
environment:
|
||||
APPVIEW_DOMAIN: ${APPVIEW_DOMAIN:-atcr.io}
|
||||
HOLD_DOMAIN: ${HOLD_DOMAIN:-hold01.atcr.io}
|
||||
volumes:
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
configs:
|
||||
- source: caddyfile
|
||||
target: /etc/caddy/Caddyfile
|
||||
networks:
|
||||
- atcr-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:2019/metrics"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
atcr-appview:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile.appview
|
||||
image: atcr-appview:latest
|
||||
container_name: atcr-appview
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Server configuration
|
||||
ATCR_HTTP_ADDR: :5000
|
||||
ATCR_BASE_URL: https://${APPVIEW_DOMAIN:-atcr.io}
|
||||
ATCR_SERVICE_NAME: ${APPVIEW_DOMAIN:-atcr.io}
|
||||
|
||||
# Storage configuration
|
||||
ATCR_DEFAULT_HOLD: https://${HOLD_DOMAIN:-hold01.atcr.io}
|
||||
|
||||
# Authentication
|
||||
ATCR_AUTH_KEY_PATH: /var/lib/atcr/auth/private-key.pem
|
||||
ATCR_AUTH_CERT_PATH: /var/lib/atcr/auth/private-key.crt
|
||||
ATCR_TOKEN_EXPIRATION: ${ATCR_TOKEN_EXPIRATION:-300}
|
||||
|
||||
# UI configuration
|
||||
ATCR_UI_ENABLED: ${ATCR_UI_ENABLED:-true}
|
||||
ATCR_UI_DATABASE_PATH: /var/lib/atcr/ui.db
|
||||
|
||||
# Logging
|
||||
ATCR_LOG_LEVEL: ${ATCR_LOG_LEVEL:-info}
|
||||
ATCR_LOG_FORMATTER: ${ATCR_LOG_FORMATTER:-text}
|
||||
|
||||
# Jetstream configuration
|
||||
JETSTREAM_URL: ${JETSTREAM_URL:-wss://jetstream2.us-west.bsky.network/subscribe}
|
||||
ATCR_BACKFILL_ENABLED: ${ATCR_BACKFILL_ENABLED:-true}
|
||||
ATCR_RELAY_ENDPOINT: ${ATCR_RELAY_ENDPOINT:-https://relay1.us-east.bsky.network}
|
||||
ATCR_BACKFILL_INTERVAL: ${ATCR_BACKFILL_INTERVAL:-1h}
|
||||
volumes:
|
||||
# Persistent data: auth keys, UI database, OAuth tokens, Jetstream cache
|
||||
- atcr-appview-data:/var/lib/atcr
|
||||
networks:
|
||||
- atcr-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5000/v2/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
atcr-hold:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile.hold
|
||||
image: atcr-hold:latest
|
||||
container_name: atcr-hold
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Hold service configuration
|
||||
HOLD_PUBLIC_URL: https://${HOLD_DOMAIN:-hold01.atcr.io}
|
||||
HOLD_SERVER_ADDR: :8080
|
||||
HOLD_PUBLIC: ${HOLD_PUBLIC:-false}
|
||||
HOLD_OWNER: ${HOLD_OWNER}
|
||||
|
||||
# Storage driver
|
||||
STORAGE_DRIVER: ${STORAGE_DRIVER:-s3}
|
||||
|
||||
# S3/UpCloud Object Storage configuration
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
|
||||
AWS_REGION: ${AWS_REGION:-us-chi1}
|
||||
S3_BUCKET: ${S3_BUCKET:-atcr-blobs}
|
||||
S3_ENDPOINT: ${S3_ENDPOINT}
|
||||
S3_REGION_ENDPOINT: ${S3_REGION_ENDPOINT}
|
||||
|
||||
# Optional: Filesystem storage (comment out S3 vars above)
|
||||
# STORAGE_DRIVER: filesystem
|
||||
# STORAGE_ROOT_DIR: /var/lib/atcr/hold
|
||||
volumes:
|
||||
# Only needed for filesystem driver
|
||||
# - atcr-hold-data:/var/lib/atcr/hold
|
||||
# OAuth token storage for hold registration
|
||||
- atcr-hold-tokens:/root/.atcr
|
||||
networks:
|
||||
- atcr-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
networks:
|
||||
atcr-network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.28.0.0/24
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
driver: local
|
||||
caddy_config:
|
||||
driver: local
|
||||
atcr-appview-data:
|
||||
driver: local
|
||||
atcr-hold-data:
|
||||
driver: local
|
||||
atcr-hold-tokens:
|
||||
driver: local
|
||||
|
||||
configs:
|
||||
caddyfile:
|
||||
content: |
|
||||
# ATCR AppView - Main registry + web UI
|
||||
{$APPVIEW_DOMAIN} {
|
||||
# Reverse proxy to AppView container
|
||||
reverse_proxy atcr-appview:5000 {
|
||||
# Preserve original host header
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
|
||||
# Enable compression
|
||||
encode gzip
|
||||
|
||||
# Logging
|
||||
log {
|
||||
output file /data/logs/appview.log {
|
||||
roll_size 100mb
|
||||
roll_keep 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ATCR Hold Service - Storage presigned URL generator
|
||||
{$HOLD_DOMAIN} {
|
||||
# Reverse proxy to Hold service container
|
||||
reverse_proxy atcr-hold:8080 {
|
||||
# Preserve original host header
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
|
||||
# Enable compression
|
||||
encode gzip
|
||||
|
||||
# Logging
|
||||
log {
|
||||
output file /data/logs/hold.log {
|
||||
roll_size 100mb
|
||||
roll_keep 10
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# ATCR UpCloud Initialization Script for Rocky Linux
|
||||
#
|
||||
# This script sets up ATCR on a fresh Rocky Linux instance.
|
||||
# Paste this into UpCloud's "User data" field when creating a server.
|
||||
#
|
||||
# What it does:
|
||||
# - Updates system packages
|
||||
# - Installs Docker and Docker Compose
|
||||
# - Configures firewall (ports 80, 443, 22)
|
||||
# - Creates directory structure
|
||||
# - Clones ATCR repository
|
||||
# - Creates systemd service for auto-start
|
||||
# - Builds and starts containers
|
||||
#
|
||||
# Post-deployment:
|
||||
# 1. Edit /opt/atcr/.env with your configuration
|
||||
# 2. Run: systemctl restart atcr
|
||||
# 3. Check logs: docker logs atcr-hold (for OAuth URL)
|
||||
# 4. Complete hold registration via OAuth
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
ATCR_DIR="/opt/atcr"
|
||||
ATCR_REPO="https://tangled.org/@evan.jarrett.net/at-container-registry" # UPDATE THIS
|
||||
ATCR_BRANCH="main"
|
||||
|
||||
# Simple logging without colors (for cloud-init log compatibility)
|
||||
log_info() {
|
||||
echo "[INFO] $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo "[WARN] $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "[ERROR] $1"
|
||||
}
|
||||
|
||||
# Function to check if command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
log_info "Starting ATCR deployment on Rocky Linux..."
|
||||
|
||||
# Update system packages
|
||||
log_info "Updating system packages..."
|
||||
dnf update -y
|
||||
|
||||
# Install required packages
|
||||
log_info "Installing prerequisites..."
|
||||
dnf install -y \
|
||||
git \
|
||||
wget \
|
||||
curl \
|
||||
nano \
|
||||
vim
|
||||
|
||||
log_info "Required ports: HTTP (80), HTTPS (443), SSH (22)"
|
||||
|
||||
# Install Docker
|
||||
if ! command_exists docker; then
|
||||
log_info "Installing Docker..."
|
||||
|
||||
# Add Docker repository
|
||||
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
|
||||
|
||||
# Install Docker
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
|
||||
# Start and enable Docker
|
||||
systemctl enable --now docker
|
||||
|
||||
log_info "Docker installed successfully"
|
||||
else
|
||||
log_info "Docker already installed"
|
||||
fi
|
||||
|
||||
# Verify Docker Compose
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
log_error "Docker Compose plugin not found. Please install manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Docker Compose version: $(docker compose version)"
|
||||
|
||||
# Create ATCR directory
|
||||
log_info "Creating ATCR directory: $ATCR_DIR"
|
||||
mkdir -p "$ATCR_DIR"
|
||||
cd "$ATCR_DIR"
|
||||
|
||||
# Clone repository or create minimal structure
|
||||
if [ -n "$ATCR_REPO" ] && [ "$ATCR_REPO" != "https://tangled.org/@evan.jarrett.net/at-container-registry" ]; then
|
||||
log_info "Cloning ATCR repository..."
|
||||
git clone -b "$ATCR_BRANCH" "$ATCR_REPO" .
|
||||
else
|
||||
log_warn "ATCR_REPO not configured. You'll need to manually copy files to $ATCR_DIR"
|
||||
log_warn "Required files:"
|
||||
log_warn " - deploy/docker-compose.prod.yml"
|
||||
log_warn " - deploy/.env.prod.template"
|
||||
log_warn " - Dockerfile.appview"
|
||||
log_warn " - Dockerfile.hold"
|
||||
fi
|
||||
|
||||
# Create .env file from template if it doesn't exist
|
||||
if [ -f "deploy/.env.prod.template" ] && [ ! -f "$ATCR_DIR/.env" ]; then
|
||||
log_info "Creating .env file from template..."
|
||||
cp deploy/.env.prod.template "$ATCR_DIR/.env"
|
||||
log_warn "IMPORTANT: Edit $ATCR_DIR/.env with your configuration!"
|
||||
fi
|
||||
|
||||
# Create systemd service
|
||||
log_info "Creating systemd service..."
|
||||
cat > /etc/systemd/system/atcr.service <<'EOF'
|
||||
[Unit]
|
||||
Description=ATCR Container Registry
|
||||
Requires=docker.service
|
||||
After=docker.service network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=/opt/atcr
|
||||
EnvironmentFile=/opt/atcr/.env
|
||||
|
||||
# Start containers
|
||||
ExecStart=/usr/bin/docker compose -f /opt/atcr/deploy/docker-compose.prod.yml up -d
|
||||
|
||||
# Stop containers
|
||||
ExecStop=/usr/bin/docker compose -f /opt/atcr/deploy/docker-compose.prod.yml down
|
||||
|
||||
# Restart containers
|
||||
ExecReload=/usr/bin/docker compose -f /opt/atcr/deploy/docker-compose.prod.yml restart
|
||||
|
||||
# Always restart on failure
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Reload systemd
|
||||
log_info "Reloading systemd daemon..."
|
||||
systemctl daemon-reload
|
||||
|
||||
# Enable service (but don't start yet - user needs to configure .env)
|
||||
systemctl enable atcr.service
|
||||
|
||||
log_info "Systemd service created and enabled"
|
||||
|
||||
# Create helper scripts
|
||||
log_info "Creating helper scripts..."
|
||||
|
||||
# Script to rebuild and restart
|
||||
cat > "$ATCR_DIR/rebuild.sh" <<'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
cd /opt/atcr
|
||||
docker compose -f deploy/docker-compose.prod.yml build
|
||||
docker compose -f deploy/docker-compose.prod.yml up -d
|
||||
docker compose -f deploy/docker-compose.prod.yml logs -f
|
||||
EOF
|
||||
chmod +x "$ATCR_DIR/rebuild.sh"
|
||||
|
||||
# Script to view logs
|
||||
cat > "$ATCR_DIR/logs.sh" <<'EOF'
|
||||
#!/bin/bash
|
||||
cd /opt/atcr
|
||||
docker compose -f deploy/docker-compose.prod.yml logs -f "$@"
|
||||
EOF
|
||||
chmod +x "$ATCR_DIR/logs.sh"
|
||||
|
||||
# Script to get hold OAuth URL
|
||||
cat > "$ATCR_DIR/get-hold-oauth.sh" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo "Checking atcr-hold logs for OAuth registration URL..."
|
||||
docker logs atcr-hold 2>&1 | grep -i "oauth\|authorization\|visit\|http" | tail -20
|
||||
EOF
|
||||
chmod +x "$ATCR_DIR/get-hold-oauth.sh"
|
||||
|
||||
log_info "Helper scripts created in $ATCR_DIR"
|
||||
|
||||
# Print completion message
|
||||
cat <<'EOF'
|
||||
|
||||
================================================================================
|
||||
ATCR Installation Complete!
|
||||
================================================================================
|
||||
|
||||
NEXT STEPS:
|
||||
|
||||
1. Configure environment variables:
|
||||
nano /opt/atcr/.env
|
||||
|
||||
Required settings:
|
||||
- AWS_ACCESS_KEY_ID (UpCloud S3 credentials)
|
||||
- AWS_SECRET_ACCESS_KEY
|
||||
|
||||
Pre-configured (verify these are correct):
|
||||
- APPVIEW_DOMAIN=atcr.io
|
||||
- HOLD_DOMAIN=hold01.atcr.io
|
||||
- HOLD_OWNER=did:plc:pddp4xt5lgnv2qsegbzzs4xg
|
||||
- S3_BUCKET=atcr
|
||||
- S3_ENDPOINT=https://blobs.atcr.io
|
||||
|
||||
2. Configure UpCloud Cloud Firewall (in control panel):
|
||||
Allow: TCP 22 (SSH)
|
||||
Allow: TCP 80 (HTTP)
|
||||
Allow: TCP 443 (HTTPS)
|
||||
Drop: Everything else
|
||||
|
||||
3. Configure DNS (Cloudflare - DNS-only mode):
|
||||
EOF
|
||||
|
||||
echo " A atcr.io → $(curl -s ifconfig.me || echo '[server-ip]') (gray cloud)"
|
||||
echo " A hold01.atcr.io → $(curl -s ifconfig.me || echo '[server-ip]') (gray cloud)"
|
||||
echo " CNAME blobs.atcr.io → atcr.us-chi1.upcloudobjects.com (gray cloud)"
|
||||
|
||||
cat <<'EOF'
|
||||
|
||||
4. Start ATCR:
|
||||
systemctl start atcr
|
||||
|
||||
5. Complete Hold OAuth registration:
|
||||
/opt/atcr/get-hold-oauth.sh
|
||||
|
||||
Visit the OAuth URL in your browser to authorize the hold service.
|
||||
|
||||
6. Check status:
|
||||
systemctl status atcr
|
||||
docker ps
|
||||
/opt/atcr/logs.sh
|
||||
|
||||
Helper Scripts:
|
||||
/opt/atcr/rebuild.sh - Rebuild and restart containers
|
||||
/opt/atcr/logs.sh [service] - View logs (e.g., logs.sh atcr-hold)
|
||||
/opt/atcr/get-hold-oauth.sh - Get hold OAuth URL
|
||||
|
||||
Service Management:
|
||||
systemctl start atcr - Start ATCR
|
||||
systemctl stop atcr - Stop ATCR
|
||||
systemctl restart atcr - Restart ATCR
|
||||
systemctl status atcr - Check status
|
||||
|
||||
Documentation:
|
||||
https://tangled.org/@evan.jarrett.net/at-container-registry
|
||||
|
||||
IMPORTANT:
|
||||
- Edit /opt/atcr/.env with S3 credentials before starting!
|
||||
- Configure UpCloud cloud firewall (see step 2)
|
||||
- DNS must be configured and propagated
|
||||
- Cloudflare proxy must be DISABLED (gray cloud)
|
||||
- Complete hold OAuth registration before first push
|
||||
|
||||
EOF
|
||||
|
||||
log_info "Installation complete. Follow the next steps above."
|
||||
Reference in New Issue
Block a user