A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
1#!/bin/bash
2#
3# ATCR UpCloud Initialization Script for Rocky Linux
4#
5# This script sets up ATCR on a fresh Rocky Linux instance.
6# Paste this into UpCloud's "User data" field when creating a server.
7#
8# What it does:
9# - Updates system packages
10# - Creates 2GB swap file (for 1GB RAM instances)
11# - Installs Docker and Docker Compose
12# - Creates directory structure
13# - Clones ATCR repository
14# - Creates systemd service for auto-start
15# - Builds and starts containers
16#
17# Post-deployment:
18# 1. Edit /opt/atcr/.env with your configuration
19# 2. Run: systemctl restart atcr
20# 3. Check logs: docker logs atcr-hold (for OAuth URL)
21# 4. Complete hold registration via OAuth
22
23set -euo pipefail
24
25# Configuration
26ATCR_DIR="/opt/atcr"
27ATCR_REPO="https://tangled.org/@evan.jarrett.net/at-container-registry" # UPDATE THIS
28ATCR_BRANCH="main"
29
30# Simple logging without colors (for cloud-init log compatibility)
31log_info() {
32 echo "[INFO] $1"
33}
34
35log_warn() {
36 echo "[WARN] $1"
37}
38
39log_error() {
40 echo "[ERROR] $1"
41}
42
43# Function to check if command exists
44command_exists() {
45 command -v "$1" >/dev/null 2>&1
46}
47
48log_info "Starting ATCR deployment on Rocky Linux..."
49
50# Update system packages
51log_info "Updating system packages..."
52dnf update -y
53
54# Install required packages
55log_info "Installing prerequisites..."
56dnf install -y \
57 git \
58 wget \
59 curl \
60 nano \
61 vim
62
63log_info "Required ports: HTTP (80), HTTPS (443), SSH (22)"
64
65# Create swap file for instances with limited RAM
66if [ ! -f /swapfile ]; then
67 log_info "Creating 2GB swap file (allows builds on 1GB RAM instances)..."
68 dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress
69 chmod 600 /swapfile
70 mkswap /swapfile
71 swapon /swapfile
72
73 # Make swap permanent
74 echo '/swapfile none swap sw 0 0' >> /etc/fstab
75
76 log_info "Swap file created and enabled"
77 free -h
78else
79 log_info "Swap file already exists"
80fi
81
82# Install Docker
83if ! command_exists docker; then
84 log_info "Installing Docker..."
85
86 # Add Docker repository
87 dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
88
89 # Install Docker
90 dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
91
92 # Start and enable Docker
93 systemctl enable --now docker
94
95 log_info "Docker installed successfully"
96else
97 log_info "Docker already installed"
98fi
99
100# Verify Docker Compose
101if ! docker compose version >/dev/null 2>&1; then
102 log_error "Docker Compose plugin not found. Please install manually."
103 exit 1
104fi
105
106log_info "Docker Compose version: $(docker compose version)"
107
108# Create ATCR directory
109log_info "Creating ATCR directory: $ATCR_DIR"
110mkdir -p "$ATCR_DIR"
111cd "$ATCR_DIR"
112
113# Clone repository or create minimal structure
114if [ -n "$ATCR_REPO" ] && [ "$ATCR_REPO" != "https://tangled.org/@evan.jarrett.net/at-container-registry" ]; then
115 log_info "Cloning ATCR repository..."
116 git clone -b "$ATCR_BRANCH" "$ATCR_REPO" .
117else
118 log_warn "ATCR_REPO not configured. You'll need to manually copy files to $ATCR_DIR"
119 log_warn "Required files:"
120 log_warn " - deploy/docker-compose.prod.yml"
121 log_warn " - deploy/.env.prod.template"
122 log_warn " - Dockerfile.appview"
123 log_warn " - Dockerfile.hold"
124fi
125
126# Create .env file from template if it doesn't exist
127if [ -f "deploy/.env.prod.template" ] && [ ! -f "$ATCR_DIR/.env" ]; then
128 log_info "Creating .env file from template..."
129 cp deploy/.env.prod.template "$ATCR_DIR/.env"
130 log_warn "IMPORTANT: Edit $ATCR_DIR/.env with your configuration!"
131fi
132
133# Create systemd services (caddy, appview, hold)
134log_info "Creating systemd services..."
135
136# Caddy service (reverse proxy for both appview and hold)
137cat > /etc/systemd/system/atcr-caddy.service <<'EOF'
138[Unit]
139Description=ATCR Caddy Reverse Proxy
140Requires=docker.service
141After=docker.service network-online.target
142Wants=network-online.target
143
144[Service]
145Type=oneshot
146RemainAfterExit=yes
147WorkingDirectory=/opt/atcr
148EnvironmentFile=/opt/atcr/.env
149
150# Start caddy container
151ExecStart=/usr/bin/docker compose -f /opt/atcr/deploy/docker-compose.prod.yml up -d caddy
152
153# Stop caddy container
154ExecStop=/usr/bin/docker compose -f /opt/atcr/deploy/docker-compose.prod.yml stop caddy
155
156# Restart caddy container
157ExecReload=/usr/bin/docker compose -f /opt/atcr/deploy/docker-compose.prod.yml restart caddy
158
159# Always restart on failure
160Restart=on-failure
161RestartSec=10
162
163[Install]
164WantedBy=multi-user.target
165EOF
166
167# AppView service (registry + web UI)
168cat > /etc/systemd/system/atcr-appview.service <<'EOF'
169[Unit]
170Description=ATCR AppView (Registry + Web UI)
171Requires=docker.service atcr-caddy.service
172After=docker.service network-online.target atcr-caddy.service
173Wants=network-online.target
174
175[Service]
176Type=oneshot
177RemainAfterExit=yes
178WorkingDirectory=/opt/atcr
179EnvironmentFile=/opt/atcr/.env
180
181# Start appview container
182ExecStart=/usr/bin/docker compose -f /opt/atcr/deploy/docker-compose.prod.yml up -d atcr-appview
183
184# Stop appview container
185ExecStop=/usr/bin/docker compose -f /opt/atcr/deploy/docker-compose.prod.yml stop atcr-appview
186
187# Restart appview container
188ExecReload=/usr/bin/docker compose -f /opt/atcr/deploy/docker-compose.prod.yml restart atcr-appview
189
190# Always restart on failure
191Restart=on-failure
192RestartSec=10
193
194[Install]
195WantedBy=multi-user.target
196EOF
197
198# Hold service (storage backend)
199cat > /etc/systemd/system/atcr-hold.service <<'EOF'
200[Unit]
201Description=ATCR Hold (Storage Service)
202Requires=docker.service atcr-caddy.service
203After=docker.service network-online.target atcr-caddy.service
204Wants=network-online.target
205
206[Service]
207Type=oneshot
208RemainAfterExit=yes
209WorkingDirectory=/opt/atcr
210EnvironmentFile=/opt/atcr/.env
211
212# Start hold container
213ExecStart=/usr/bin/docker compose -f /opt/atcr/deploy/docker-compose.prod.yml up -d atcr-hold
214
215# Stop hold container
216ExecStop=/usr/bin/docker compose -f /opt/atcr/deploy/docker-compose.prod.yml stop atcr-hold
217
218# Restart hold container
219ExecReload=/usr/bin/docker compose -f /opt/atcr/deploy/docker-compose.prod.yml restart atcr-hold
220
221# Always restart on failure
222Restart=on-failure
223RestartSec=10
224
225[Install]
226WantedBy=multi-user.target
227EOF
228
229# Reload systemd
230log_info "Reloading systemd daemon..."
231systemctl daemon-reload
232
233# Enable all services (but don't start yet - user needs to configure .env)
234systemctl enable atcr-caddy.service
235systemctl enable atcr-appview.service
236systemctl enable atcr-hold.service
237
238log_info "Systemd services created and enabled"
239
240# Create helper scripts
241log_info "Creating helper scripts..."
242
243# Script to rebuild and restart
244cat > "$ATCR_DIR/rebuild.sh" <<'EOF'
245#!/bin/bash
246set -e
247cd /opt/atcr
248docker compose -f deploy/docker-compose.prod.yml build
249docker compose -f deploy/docker-compose.prod.yml up -d
250docker compose -f deploy/docker-compose.prod.yml logs -f
251EOF
252chmod +x "$ATCR_DIR/rebuild.sh"
253
254# Script to view logs
255cat > "$ATCR_DIR/logs.sh" <<'EOF'
256#!/bin/bash
257cd /opt/atcr
258docker compose -f deploy/docker-compose.prod.yml logs -f "$@"
259EOF
260chmod +x "$ATCR_DIR/logs.sh"
261
262log_info "Helper scripts created in $ATCR_DIR"
263
264# Print completion message
265cat <<'EOF'
266
267================================================================================
268ATCR Installation Complete!
269================================================================================
270
271NEXT STEPS:
272
2731. Configure environment variables:
274 nano /opt/atcr/.env
275
276 Required settings:
277 - AWS_ACCESS_KEY_ID (UpCloud S3 credentials)
278 - AWS_SECRET_ACCESS_KEY
279
280 Pre-configured (verify these are correct):
281 - APPVIEW_DOMAIN=atcr.io
282 - HOLD_DOMAIN=hold01.atcr.io
283 - HOLD_OWNER=did:plc:pddp4xt5lgnv2qsegbzzs4xg
284 - S3_BUCKET=atcr
285 - S3_ENDPOINT=https://blobs.atcr.io
286
2872. Configure UpCloud Cloud Firewall (in control panel):
288 Allow: TCP 22 (SSH)
289 Allow: TCP 80 (HTTP)
290 Allow: TCP 443 (HTTPS)
291 Drop: Everything else
292
2933. Configure DNS (Cloudflare - DNS-only mode):
294EOF
295
296echo " A atcr.io → $(curl -s ifconfig.me || echo '[server-ip]') (gray cloud)"
297echo " A hold01.atcr.io → $(curl -s ifconfig.me || echo '[server-ip]') (gray cloud)"
298echo " CNAME blobs.atcr.io → atcr.us-chi1.upcloudobjects.com (gray cloud)"
299
300cat <<'EOF'
301
3024. Start ATCR services:
303 systemctl start atcr-caddy atcr-appview atcr-hold
304
3055. Check status:
306 systemctl status atcr-caddy
307 systemctl status atcr-appview
308 systemctl status atcr-hold
309 docker ps
310 /opt/atcr/logs.sh
311
312Helper Scripts:
313 /opt/atcr/rebuild.sh - Rebuild and restart containers
314 /opt/atcr/logs.sh [service] - View logs (e.g., logs.sh atcr-hold)
315
316Service Management:
317 systemctl start atcr-caddy - Start Caddy reverse proxy
318 systemctl start atcr-appview - Start AppView (registry + UI)
319 systemctl start atcr-hold - Start Hold (storage service)
320
321 systemctl stop atcr-appview - Stop AppView only
322 systemctl stop atcr-hold - Stop Hold only
323 systemctl stop atcr-caddy - Stop all (stops reverse proxy)
324
325 systemctl restart atcr-appview - Restart AppView
326 systemctl restart atcr-hold - Restart Hold
327
328 systemctl status atcr-caddy - Check Caddy status
329 systemctl status atcr-appview - Check AppView status
330 systemctl status atcr-hold - Check Hold status
331
332Documentation:
333 https://tangled.org/@evan.jarrett.net/at-container-registry
334
335IMPORTANT:
336 - Edit /opt/atcr/.env with S3 credentials before starting!
337 - Configure UpCloud cloud firewall (see step 2)
338 - DNS must be configured and propagated
339 - Cloudflare proxy must be DISABLED (gray cloud)
340 - Complete hold OAuth registration before first push
341
342EOF
343
344log_info "Installation complete. Follow the next steps above."