A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
at loom 728 lines 19 kB view raw view rendered
1# Development Workflow for ATCR 2 3## The Problem 4 5**Current development cycle with Docker:** 61. Edit CSS, JS, template, or Go file 72. Run `docker compose build` (rebuilds entire image) 83. Run `docker compose up` (restart container) 94. Wait **2-3 minutes** for changes to appear 105. Test, find issue, repeat... 11 12**Why it's slow:** 13- All assets embedded via `embed.FS` at compile time 14- Multi-stage Docker build compiles everything from scratch 15- No development mode exists 16- Final image uses `scratch` base (no tools, no hot reload) 17 18## The Solution 19 20**Development setup combining:** 211. **Dockerfile.devel** - Development-focused container (golang base, not scratch) 222. **Volume mounts** - Live code editing (changes appear instantly in container) 233. **DirFS** - Skip embed, read templates/CSS/JS from filesystem 244. **Air** - Auto-rebuild on Go code changes 25 26**Results:** 27- CSS/JS/Template changes: **Instant** (0 seconds, just refresh browser) 28- Go code changes: **2-5 seconds** (vs 2-3 minutes) 29- Production builds: **Unchanged** (still optimized with embed.FS) 30 31## How It Works 32 33### Architecture Flow 34 35``` 36┌─────────────────────────────────────────────────────┐ 37│ Your Editor (VSCode, etc) │ 38│ Edit: style.css, app.js, *.html, *.go files │ 39└─────────────────┬───────────────────────────────────┘ 40 │ (files saved to disk) 41 42┌─────────────────────────────────────────────────────┐ 43│ Volume Mount (docker-compose.dev.yml) │ 44│ volumes: │ 45│ - .:/app (entire codebase mounted) │ 46└─────────────────┬───────────────────────────────────┘ 47 │ (changes appear instantly in container) 48 49┌─────────────────────────────────────────────────────┐ 50│ Container (golang:1.25.2 base, has all tools) │ 51│ │ 52│ ┌──────────────────────────────────────┐ │ 53│ │ Air (hot reload tool) │ │ 54│ │ Watches: *.go, *.html, *.css, *.js │ │ 55│ │ │ │ 56│ │ On change: │ │ 57│ │ - *.go → rebuild binary (2-5s) │ │ 58│ │ - templates/css/js → restart only │ │ 59│ └──────────────────────────────────────┘ │ 60│ │ │ 61│ ▼ │ 62│ ┌──────────────────────────────────────┐ │ 63│ │ ATCR AppView (ATCR_DEV_MODE=true) │ │ 64│ │ │ │ 65│ │ ui.go checks DEV_MODE: │ │ 66│ │ if DEV_MODE: │ │ 67│ │ templatesFS = os.DirFS("...") │ │ 68│ │ staticFS = os.DirFS("...") │ │ 69│ │ else: │ │ 70│ │ use embed.FS (production) │ │ 71│ │ │ │ 72│ │ Result: Reads from mounted files │ │ 73│ └──────────────────────────────────────┘ │ 74└─────────────────────────────────────────────────────┘ 75``` 76 77### Change Scenarios 78 79#### Scenario 1: Edit CSS/JS/Templates 80``` 811. Edit pkg/appview/static/css/style.css in VSCode 822. Save file 833. Change appears in container via volume mount (instant) 844. App uses os.DirFS → reads new file from disk (instant) 855. Refresh browser → see changes 86``` 87**Time:** **Instant** (0 seconds) 88**No rebuild, no restart!** 89 90#### Scenario 2: Edit Go Code 91``` 921. Edit pkg/appview/handlers/home.go 932. Save file 943. Air detects .go file change 954. Air runs: go build -o ./tmp/atcr-appview ./cmd/appview 965. Air kills old process and starts new binary 976. App runs with new code 98``` 99**Time:** **2-5 seconds** 100**Fast incremental build!** 101 102## Implementation 103 104### Step 1: Create Dockerfile.devel 105 106Create `Dockerfile.devel` in project root: 107 108```dockerfile 109# Development Dockerfile with hot reload support 110FROM golang:1.25.2-trixie 111 112# Install Air for hot reload 113RUN go install github.com/cosmtrek/air@latest 114 115# Install SQLite (required for CGO in ATCR) 116RUN apt-get update && apt-get install -y \ 117 sqlite3 \ 118 libsqlite3-dev \ 119 && rm -rf /var/lib/apt/lists/* 120 121WORKDIR /app 122 123# Copy dependency files and download (cached layer) 124COPY go.mod go.sum ./ 125RUN go mod download 126 127# Note: Source code comes from volume mount 128# (no COPY . . needed - that's the whole point!) 129 130# Air will handle building and running 131CMD ["air", "-c", ".air.toml"] 132``` 133 134### Step 2: Create docker-compose.dev.yml 135 136Create `docker-compose.dev.yml` in project root: 137 138```yaml 139version: '3.8' 140 141services: 142 atcr-appview: 143 build: 144 context: . 145 dockerfile: Dockerfile.devel 146 volumes: 147 # Mount entire codebase (live editing) 148 - .:/app 149 # Cache Go modules (faster rebuilds) 150 - go-cache:/go/pkg/mod 151 # Persist SQLite database 152 - atcr-ui-dev:/var/lib/atcr 153 environment: 154 # Enable development mode (uses os.DirFS) 155 ATCR_DEV_MODE: "true" 156 157 # AppView configuration 158 ATCR_HTTP_ADDR: ":5000" 159 ATCR_BASE_URL: "http://localhost:5000" 160 ATCR_DEFAULT_HOLD_DID: "did:web:hold01.atcr.io" 161 162 # Database 163 ATCR_UI_DATABASE_PATH: "/var/lib/atcr/ui.db" 164 165 # Auth 166 ATCR_AUTH_KEY_PATH: "/var/lib/atcr/auth/private-key.pem" 167 168 # UI 169 ATCR_UI_ENABLED: "true" 170 171 # Jetstream (optional) 172 # JETSTREAM_URL: "wss://jetstream2.us-east.bsky.network/subscribe" 173 # ATCR_BACKFILL_ENABLED: "false" 174 ports: 175 - "5000:5000" 176 networks: 177 - atcr-dev 178 179 # Add other services as needed (postgres, hold, etc) 180 # atcr-hold: 181 # ... 182 183networks: 184 atcr-dev: 185 driver: bridge 186 187volumes: 188 go-cache: 189 atcr-ui-dev: 190``` 191 192### Step 3: Create .air.toml 193 194Create `.air.toml` in project root: 195 196```toml 197# Air configuration for hot reload 198# https://github.com/cosmtrek/air 199 200root = "." 201testdata_dir = "testdata" 202tmp_dir = "tmp" 203 204[build] 205 # Arguments to pass to binary (AppView needs "serve") 206 args_bin = ["serve"] 207 208 # Where to output the built binary 209 bin = "./tmp/atcr-appview" 210 211 # Build command 212 cmd = "go build -o ./tmp/atcr-appview ./cmd/appview" 213 214 # Delay before rebuilding (ms) - debounce rapid saves 215 delay = 1000 216 217 # Directories to exclude from watching 218 exclude_dir = [ 219 "tmp", 220 "vendor", 221 "bin", 222 ".git", 223 "node_modules", 224 "testdata" 225 ] 226 227 # Files to exclude from watching 228 exclude_file = [] 229 230 # Regex patterns to exclude 231 exclude_regex = ["_test\\.go"] 232 233 # Don't rebuild if file content unchanged 234 exclude_unchanged = false 235 236 # Follow symlinks 237 follow_symlink = false 238 239 # Full command to run (leave empty to use cmd + bin) 240 full_bin = "" 241 242 # Directories to include (empty = all) 243 include_dir = [] 244 245 # File extensions to watch 246 include_ext = ["go", "html", "css", "js"] 247 248 # Specific files to watch 249 include_file = [] 250 251 # Delay before killing old process (s) 252 kill_delay = "0s" 253 254 # Log file for build errors 255 log = "build-errors.log" 256 257 # Use polling instead of fsnotify (for Docker/VM) 258 poll = false 259 poll_interval = 0 260 261 # Rerun binary if it exits 262 rerun = false 263 rerun_delay = 500 264 265 # Send interrupt signal instead of kill 266 send_interrupt = false 267 268 # Stop on build error 269 stop_on_error = false 270 271[color] 272 # Colorize output 273 app = "" 274 build = "yellow" 275 main = "magenta" 276 runner = "green" 277 watcher = "cyan" 278 279[log] 280 # Show only app logs (not build logs) 281 main_only = false 282 283 # Add timestamp to logs 284 time = false 285 286[misc] 287 # Clean tmp directory on exit 288 clean_on_exit = false 289 290[screen] 291 # Clear screen on rebuild 292 clear_on_rebuild = false 293 294 # Keep scrollback 295 keep_scroll = true 296``` 297 298### Step 4: Modify pkg/appview/ui.go 299 300Add conditional filesystem loading to `pkg/appview/ui.go`: 301 302```go 303package appview 304 305import ( 306 "embed" 307 "html/template" 308 "io/fs" 309 "log" 310 "net/http" 311 "os" 312) 313 314// Embedded assets (used in production) 315//go:embed templates/**/*.html 316var embeddedTemplatesFS embed.FS 317 318//go:embed static 319var embeddedStaticFS embed.FS 320 321// Actual filesystems used at runtime (conditional) 322var templatesFS fs.FS 323var staticFS fs.FS 324 325func init() { 326 // Development mode: read from filesystem for instant updates 327 if os.Getenv("ATCR_DEV_MODE") == "true" { 328 log.Println("🔧 DEV MODE: Using filesystem for templates and static assets") 329 templatesFS = os.DirFS("pkg/appview/templates") 330 staticFS = os.DirFS("pkg/appview/static") 331 } else { 332 // Production mode: use embedded assets 333 log.Println("📦 PRODUCTION MODE: Using embedded assets") 334 templatesFS = embeddedTemplatesFS 335 staticFS = embeddedStaticFS 336 } 337} 338 339// Templates returns parsed HTML templates 340func Templates() *template.Template { 341 tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html") 342 if err != nil { 343 log.Fatalf("Failed to parse templates: %v", err) 344 } 345 return tmpl 346} 347 348// StaticHandler returns a handler for static files 349func StaticHandler() http.Handler { 350 sub, err := fs.Sub(staticFS, "static") 351 if err != nil { 352 log.Fatalf("Failed to create static sub-filesystem: %v", err) 353 } 354 return http.FileServer(http.FS(sub)) 355} 356``` 357 358**Important:** Update the `Templates()` function to NOT cache templates in dev mode: 359 360```go 361// Templates returns parsed HTML templates 362func Templates() *template.Template { 363 // In dev mode, reparse templates on every request (instant updates) 364 // In production, this could be cached 365 tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html") 366 if err != nil { 367 log.Fatalf("Failed to parse templates: %v", err) 368 } 369 return tmpl 370} 371``` 372 373If you're caching templates, wrap it with a dev mode check: 374 375```go 376var templateCache *template.Template 377 378func Templates() *template.Template { 379 // Development: reparse every time (instant updates) 380 if os.Getenv("ATCR_DEV_MODE") == "true" { 381 tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html") 382 if err != nil { 383 log.Printf("Template parse error: %v", err) 384 return template.New("error") 385 } 386 return tmpl 387 } 388 389 // Production: use cached templates 390 if templateCache == nil { 391 tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html") 392 if err != nil { 393 log.Fatalf("Failed to parse templates: %v", err) 394 } 395 templateCache = tmpl 396 } 397 return templateCache 398} 399``` 400 401### Step 5: Add to .gitignore 402 403Add Air's temporary directory to `.gitignore`: 404 405``` 406# Air hot reload 407tmp/ 408build-errors.log 409``` 410 411## Usage 412 413### Starting Development Environment 414 415```bash 416# Build and start dev container 417docker compose -f docker-compose.dev.yml up --build 418 419# Or run in background 420docker compose -f docker-compose.dev.yml up -d 421 422# View logs 423docker compose -f docker-compose.dev.yml logs -f atcr-appview 424``` 425 426You should see Air starting: 427 428``` 429atcr-appview | 🔧 DEV MODE: Using filesystem for templates and static assets 430atcr-appview | 431atcr-appview | __ _ ___ 432atcr-appview | / /\ | | | |_) 433atcr-appview | /_/--\ |_| |_| \_ , built with Go 434atcr-appview | 435atcr-appview | watching . 436atcr-appview | !exclude tmp 437atcr-appview | building... 438atcr-appview | running... 439``` 440 441### Development Workflow 442 443#### 1. Edit Templates/CSS/JS (Instant Updates) 444 445```bash 446# Edit any template, CSS, or JS file 447vim pkg/appview/templates/pages/home.html 448vim pkg/appview/static/css/style.css 449vim pkg/appview/static/js/app.js 450 451# Save file → changes appear instantly 452# Just refresh browser (Cmd+R / Ctrl+R) 453``` 454 455**No rebuild, no restart!** Air might restart the app, but it's instant since no compilation is needed. 456 457#### 2. Edit Go Code (Fast Rebuild) 458 459```bash 460# Edit any Go file 461vim pkg/appview/handlers/home.go 462 463# Save file → Air detects change 464# Air output shows: 465# building... 466# build successful in 2.3s 467# restarting... 468 469# Refresh browser to see changes 470``` 471 472**2-5 second rebuild** instead of 2-3 minutes! 473 474### Stopping Development Environment 475 476```bash 477# Stop containers 478docker compose -f docker-compose.dev.yml down 479 480# Stop and remove volumes (fresh start) 481docker compose -f docker-compose.dev.yml down -v 482``` 483 484## Production Builds 485 486**Production builds are completely unchanged:** 487 488```bash 489# Production uses normal Dockerfile (embed.FS, scratch base) 490docker compose build 491 492# Or specific service 493docker compose build atcr-appview 494 495# Run production 496docker compose up 497``` 498 499**Why it works:** 500- Production doesn't set `ATCR_DEV_MODE=true` 501- `ui.go` defaults to embedded assets when env var is unset 502- Production Dockerfile still uses multi-stage build to scratch 503- No development dependencies in production image 504 505## Comparison 506 507| Change Type | Before (docker compose) | After (dev setup) | Improvement | 508|-------------|------------------------|-------------------|-------------| 509| Edit CSS | 2-3 minutes | **Instant (0s)** | ♾️x faster | 510| Edit JS | 2-3 minutes | **Instant (0s)** | ♾️x faster | 511| Edit Template | 2-3 minutes | **Instant (0s)** | ♾️x faster | 512| Edit Go Code | 2-3 minutes | **2-5 seconds** | 24-90x faster | 513| Production Build | Same | **Same** | No change | 514 515## Advanced: Local Development (No Docker) 516 517For even faster development, run locally without Docker: 518 519```bash 520# Set environment variables 521export ATCR_DEV_MODE=true 522export ATCR_HTTP_ADDR=:5000 523export ATCR_BASE_URL=http://localhost:5000 524export ATCR_DEFAULT_HOLD_DID=did:web:hold01.atcr.io 525export ATCR_UI_DATABASE_PATH=/tmp/atcr-ui.db 526export ATCR_AUTH_KEY_PATH=/tmp/atcr-auth-key.pem 527export ATCR_UI_ENABLED=true 528 529# Or use .env file 530source .env.appview 531 532# Run with Air 533air -c .air.toml 534 535# Or run directly (no hot reload) 536go run ./cmd/appview serve 537``` 538 539**Advantages:** 540- Even faster (no Docker overhead) 541- Native debugging with delve 542- Direct filesystem access 543- Full IDE integration 544 545**Disadvantages:** 546- Need to manage dependencies locally (SQLite, etc) 547- May differ from production environment 548 549## Troubleshooting 550 551### Air Not Rebuilding 552 553**Problem:** Air doesn't detect changes 554 555**Solution:** 556```bash 557# Check if Air is actually running 558docker compose -f docker-compose.dev.yml logs atcr-appview 559 560# Check .air.toml include_ext includes your file type 561# Default: ["go", "html", "css", "js"] 562 563# Restart container 564docker compose -f docker-compose.dev.yml restart atcr-appview 565``` 566 567### Templates Not Updating 568 569**Problem:** Template changes don't appear 570 571**Solution:** 572```bash 573# Check ATCR_DEV_MODE is set 574docker compose -f docker-compose.dev.yml exec atcr-appview env | grep DEV_MODE 575 576# Should output: ATCR_DEV_MODE=true 577 578# Check templates aren't cached (see Step 4 above) 579# Templates() should reparse in dev mode 580``` 581 582### Go Build Failing 583 584**Problem:** Air shows build errors 585 586**Solution:** 587```bash 588# Check build logs 589docker compose -f docker-compose.dev.yml logs atcr-appview 590 591# Or check build-errors.log in container 592docker compose -f docker-compose.dev.yml exec atcr-appview cat build-errors.log 593 594# Fix the Go error, save file, Air will retry 595``` 596 597### Volume Mount Not Working 598 599**Problem:** Changes don't appear in container 600 601**Solution:** 602```bash 603# Verify volume mount 604docker compose -f docker-compose.dev.yml exec atcr-appview ls -la /app 605 606# Should show your source files 607 608# On Windows/Mac, check Docker Desktop file sharing settings 609# Settings → Resources → File Sharing → add project directory 610``` 611 612### Permission Errors 613 614**Problem:** Cannot write to /var/lib/atcr 615 616**Solution:** 617```bash 618# In Dockerfile.devel, add: 619RUN mkdir -p /var/lib/atcr && chmod 777 /var/lib/atcr 620 621# Or use named volumes (already in docker-compose.dev.yml) 622volumes: 623 - atcr-ui-dev:/var/lib/atcr 624``` 625 626### Slow Builds Even with Air 627 628**Problem:** Air rebuilds slowly 629 630**Solution:** 631```bash 632# Use Go module cache volume (already in docker-compose.dev.yml) 633volumes: 634 - go-cache:/go/pkg/mod 635 636# Increase Air delay to debounce rapid saves 637# In .air.toml: 638delay = 2000 # 2 seconds 639 640# Or check if CGO is slowing builds 641# AppView needs CGO for SQLite, but you can try: 642CGO_ENABLED=0 go build # (won't work for ATCR, but good to know) 643``` 644 645## Tips & Tricks 646 647### Browser Auto-Reload (LiveReload) 648 649Add LiveReload for automatic browser refresh: 650 651```bash 652# Install browser extension 653# Chrome: https://chrome.google.com/webstore/detail/livereload 654# Firefox: https://addons.mozilla.org/en-US/firefox/addon/livereload-web-extension/ 655 656# Add livereload to .air.toml (future Air feature) 657# Or use a separate tool like browsersync 658``` 659 660### Database Resets 661 662Development database is in a named volume: 663 664```bash 665# Reset database (fresh start) 666docker compose -f docker-compose.dev.yml down -v 667docker compose -f docker-compose.dev.yml up 668 669# Or delete specific volume 670docker volume rm atcr_atcr-ui-dev 671``` 672 673### Multiple Environments 674 675Run dev and production side-by-side: 676 677```bash 678# Development on port 5000 679docker compose -f docker-compose.dev.yml up -d 680 681# Production on port 5001 682docker compose up -d 683 684# Now you can compare behavior 685``` 686 687### Debugging with Delve 688 689Add delve to Dockerfile.devel: 690 691```dockerfile 692RUN go install github.com/go-delve/delve/cmd/dlv@latest 693 694# Change CMD to use delve 695CMD ["dlv", "debug", "./cmd/appview", "--headless", "--listen=:2345", "--api-version=2", "--accept-multiclient", "--", "serve"] 696``` 697 698Then connect with VSCode or GoLand. 699 700## Summary 701 702**Development Setup (One-Time):** 7031. Create `Dockerfile.devel` 7042. Create `docker-compose.dev.yml` 7053. Create `.air.toml` 7064. Modify `pkg/appview/ui.go` for conditional DirFS 7075. Add `tmp/` to `.gitignore` 708 709**Daily Development:** 710```bash 711# Start 712docker compose -f docker-compose.dev.yml up 713 714# Edit files in your editor 715# Changes appear instantly (CSS/JS/templates) 716# Or in 2-5 seconds (Go code) 717 718# Stop 719docker compose -f docker-compose.dev.yml down 720``` 721 722**Production (Unchanged):** 723```bash 724docker compose build 725docker compose up 726``` 727 728**Result:** 100x faster development iteration! 🚀