From fa59c35befd871ee1c48db1fe10d8b55af0a00e4 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Fri, 10 Oct 2025 14:14:35 -0500 Subject: [PATCH] add deployment scripts --- deploy/.env.prod.template | 167 +++++++++++ deploy/README.md | 503 +++++++++++++++++++++++++++++++++ deploy/docker-compose.prod.yml | 194 +++++++++++++ deploy/init-upcloud.sh | 263 +++++++++++++++++ 4 files changed, 1127 insertions(+) create mode 100644 deploy/.env.prod.template create mode 100644 deploy/README.md create mode 100644 deploy/docker-compose.prod.yml create mode 100644 deploy/init-upcloud.sh diff --git a/deploy/.env.prod.template b/deploy/.env.prod.template new file mode 100644 index 0000000..584bc8a --- /dev/null +++ b/deploy/.env.prod.template @@ -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 diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..97d0f67 --- /dev/null +++ b/deploy/README.md @@ -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 diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml new file mode 100644 index 0000000..8381f72 --- /dev/null +++ b/deploy/docker-compose.prod.yml @@ -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 + } + } + } diff --git a/deploy/init-upcloud.sh b/deploy/init-upcloud.sh new file mode 100644 index 0000000..7cbb948 --- /dev/null +++ b/deploy/init-upcloud.sh @@ -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."