A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
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.7 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│ │ publicFS = 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/public/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.7-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 # Jetstream (optional)
169 # JETSTREAM_URL: "wss://jetstream2.us-east.bsky.network/subscribe"
170 # ATCR_BACKFILL_ENABLED: "false"
171 ports:
172 - "5000:5000"
173 networks:
174 - atcr-dev
175
176 # Add other services as needed (postgres, hold, etc)
177 # atcr-hold:
178 # ...
179
180networks:
181 atcr-dev:
182 driver: bridge
183
184volumes:
185 go-cache:
186 atcr-ui-dev:
187```
188
189### Step 3: Create .air.toml
190
191Create `.air.toml` in project root:
192
193```toml
194# Air configuration for hot reload
195# https://github.com/cosmtrek/air
196
197root = "."
198testdata_dir = "testdata"
199tmp_dir = "tmp"
200
201[build]
202 # Arguments to pass to binary (AppView needs "serve")
203 args_bin = ["serve"]
204
205 # Where to output the built binary
206 bin = "./tmp/atcr-appview"
207
208 # Build command
209 cmd = "go build -o ./tmp/atcr-appview ./cmd/appview"
210
211 # Delay before rebuilding (ms) - debounce rapid saves
212 delay = 1000
213
214 # Directories to exclude from watching
215 exclude_dir = [
216 "tmp",
217 "vendor",
218 "bin",
219 ".git",
220 "node_modules",
221 "testdata"
222 ]
223
224 # Files to exclude from watching
225 exclude_file = []
226
227 # Regex patterns to exclude
228 exclude_regex = ["_test\\.go"]
229
230 # Don't rebuild if file content unchanged
231 exclude_unchanged = false
232
233 # Follow symlinks
234 follow_symlink = false
235
236 # Full command to run (leave empty to use cmd + bin)
237 full_bin = ""
238
239 # Directories to include (empty = all)
240 include_dir = []
241
242 # File extensions to watch
243 include_ext = ["go", "html", "css", "js"]
244
245 # Specific files to watch
246 include_file = []
247
248 # Delay before killing old process (s)
249 kill_delay = "0s"
250
251 # Log file for build errors
252 log = "build-errors.log"
253
254 # Use polling instead of fsnotify (for Docker/VM)
255 poll = false
256 poll_interval = 0
257
258 # Rerun binary if it exits
259 rerun = false
260 rerun_delay = 500
261
262 # Send interrupt signal instead of kill
263 send_interrupt = false
264
265 # Stop on build error
266 stop_on_error = false
267
268[color]
269 # Colorize output
270 app = ""
271 build = "yellow"
272 main = "magenta"
273 runner = "green"
274 watcher = "cyan"
275
276[log]
277 # Show only app logs (not build logs)
278 main_only = false
279
280 # Add timestamp to logs
281 time = false
282
283[misc]
284 # Clean tmp directory on exit
285 clean_on_exit = false
286
287[screen]
288 # Clear screen on rebuild
289 clear_on_rebuild = false
290
291 # Keep scrollback
292 keep_scroll = true
293```
294
295### Step 4: Modify pkg/appview/ui.go
296
297Add conditional filesystem loading to `pkg/appview/ui.go`:
298
299```go
300package appview
301
302import (
303 "embed"
304 "html/template"
305 "io/fs"
306 "log"
307 "net/http"
308 "os"
309)
310
311// Embedded assets (used in production)
312//go:embed templates/**/*.html
313var embeddedTemplatesFS embed.FS
314
315//go:embed static
316var embeddedpublicFS embed.FS
317
318// Actual filesystems used at runtime (conditional)
319var templatesFS fs.FS
320var publicFS fs.FS
321
322func init() {
323 // Development mode: read from filesystem for instant updates
324 if os.Getenv("ATCR_DEV_MODE") == "true" {
325 log.Println("🔧 DEV MODE: Using filesystem for templates and static assets")
326 templatesFS = os.DirFS("pkg/appview/templates")
327 publicFS = os.DirFS("pkg/appview/static")
328 } else {
329 // Production mode: use embedded assets
330 log.Println("📦 PRODUCTION MODE: Using embedded assets")
331 templatesFS = embeddedTemplatesFS
332 publicFS = embeddedpublicFS
333 }
334}
335
336// Templates returns parsed HTML templates
337func Templates() *template.Template {
338 tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
339 if err != nil {
340 log.Fatalf("Failed to parse templates: %v", err)
341 }
342 return tmpl
343}
344
345// StaticHandler returns a handler for static files
346func StaticHandler() http.Handler {
347 sub, err := fs.Sub(publicFS, "static")
348 if err != nil {
349 log.Fatalf("Failed to create static sub-filesystem: %v", err)
350 }
351 return http.FileServer(http.FS(sub))
352}
353```
354
355**Important:** Update the `Templates()` function to NOT cache templates in dev mode:
356
357```go
358// Templates returns parsed HTML templates
359func Templates() *template.Template {
360 // In dev mode, reparse templates on every request (instant updates)
361 // In production, this could be cached
362 tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
363 if err != nil {
364 log.Fatalf("Failed to parse templates: %v", err)
365 }
366 return tmpl
367}
368```
369
370If you're caching templates, wrap it with a dev mode check:
371
372```go
373var templateCache *template.Template
374
375func Templates() *template.Template {
376 // Development: reparse every time (instant updates)
377 if os.Getenv("ATCR_DEV_MODE") == "true" {
378 tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
379 if err != nil {
380 log.Printf("Template parse error: %v", err)
381 return template.New("error")
382 }
383 return tmpl
384 }
385
386 // Production: use cached templates
387 if templateCache == nil {
388 tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
389 if err != nil {
390 log.Fatalf("Failed to parse templates: %v", err)
391 }
392 templateCache = tmpl
393 }
394 return templateCache
395}
396```
397
398### Step 5: Add to .gitignore
399
400Add Air's temporary directory to `.gitignore`:
401
402```
403# Air hot reload
404tmp/
405build-errors.log
406```
407
408## Usage
409
410### Starting Development Environment
411
412```bash
413# Build and start dev container
414docker compose -f docker-compose.dev.yml up --build
415
416# Or run in background
417docker compose -f docker-compose.dev.yml up -d
418
419# View logs
420docker compose -f docker-compose.dev.yml logs -f atcr-appview
421```
422
423You should see Air starting:
424
425```
426atcr-appview | 🔧 DEV MODE: Using filesystem for templates and static assets
427atcr-appview |
428atcr-appview | __ _ ___
429atcr-appview | / /\ | | | |_)
430atcr-appview | /_/--\ |_| |_| \_ , built with Go
431atcr-appview |
432atcr-appview | watching .
433atcr-appview | !exclude tmp
434atcr-appview | building...
435atcr-appview | running...
436```
437
438### Development Workflow
439
440#### 1. Edit Templates/CSS/JS (Instant Updates)
441
442```bash
443# Edit any template, CSS, or JS file
444vim pkg/appview/templates/pages/home.html
445vim pkg/appview/public/css/style.css
446vim pkg/appview/public/js/app.js
447
448# Save file → changes appear instantly
449# Just refresh browser (Cmd+R / Ctrl+R)
450```
451
452**No rebuild, no restart!** Air might restart the app, but it's instant since no compilation is needed.
453
454#### 2. Edit Go Code (Fast Rebuild)
455
456```bash
457# Edit any Go file
458vim pkg/appview/handlers/home.go
459
460# Save file → Air detects change
461# Air output shows:
462# building...
463# build successful in 2.3s
464# restarting...
465
466# Refresh browser to see changes
467```
468
469**2-5 second rebuild** instead of 2-3 minutes!
470
471### Stopping Development Environment
472
473```bash
474# Stop containers
475docker compose -f docker-compose.dev.yml down
476
477# Stop and remove volumes (fresh start)
478docker compose -f docker-compose.dev.yml down -v
479```
480
481## Production Builds
482
483**Production builds are completely unchanged:**
484
485```bash
486# Production uses normal Dockerfile (embed.FS, scratch base)
487docker compose build
488
489# Or specific service
490docker compose build atcr-appview
491
492# Run production
493docker compose up
494```
495
496**Why it works:**
497- Production doesn't set `ATCR_DEV_MODE=true`
498- `ui.go` defaults to embedded assets when env var is unset
499- Production Dockerfile still uses multi-stage build to scratch
500- No development dependencies in production image
501
502## Comparison
503
504| Change Type | Before (docker compose) | After (dev setup) | Improvement |
505|-------------|------------------------|-------------------|-------------|
506| Edit CSS | 2-3 minutes | **Instant (0s)** | ♾️x faster |
507| Edit JS | 2-3 minutes | **Instant (0s)** | ♾️x faster |
508| Edit Template | 2-3 minutes | **Instant (0s)** | ♾️x faster |
509| Edit Go Code | 2-3 minutes | **2-5 seconds** | 24-90x faster |
510| Production Build | Same | **Same** | No change |
511
512## Advanced: Local Development (No Docker)
513
514For even faster development, run locally without Docker:
515
516```bash
517# Set environment variables
518export ATCR_DEV_MODE=true
519export ATCR_HTTP_ADDR=:5000
520export ATCR_BASE_URL=http://localhost:5000
521export ATCR_DEFAULT_HOLD_DID=did:web:hold01.atcr.io
522export ATCR_UI_DATABASE_PATH=/tmp/atcr-ui.db
523export ATCR_AUTH_KEY_PATH=/tmp/atcr-auth-key.pem
524
525# Or use .env file
526source .env.appview
527
528# Run with Air
529air -c .air.toml
530
531# Or run directly (no hot reload)
532go run ./cmd/appview serve
533```
534
535**Advantages:**
536- Even faster (no Docker overhead)
537- Native debugging with delve
538- Direct filesystem access
539- Full IDE integration
540
541**Disadvantages:**
542- Need to manage dependencies locally (SQLite, etc)
543- May differ from production environment
544
545## Troubleshooting
546
547### Air Not Rebuilding
548
549**Problem:** Air doesn't detect changes
550
551**Solution:**
552```bash
553# Check if Air is actually running
554docker compose -f docker-compose.dev.yml logs atcr-appview
555
556# Check .air.toml include_ext includes your file type
557# Default: ["go", "html", "css", "js"]
558
559# Restart container
560docker compose -f docker-compose.dev.yml restart atcr-appview
561```
562
563### Templates Not Updating
564
565**Problem:** Template changes don't appear
566
567**Solution:**
568```bash
569# Check ATCR_DEV_MODE is set
570docker compose -f docker-compose.dev.yml exec atcr-appview env | grep DEV_MODE
571
572# Should output: ATCR_DEV_MODE=true
573
574# Check templates aren't cached (see Step 4 above)
575# Templates() should reparse in dev mode
576```
577
578### Go Build Failing
579
580**Problem:** Air shows build errors
581
582**Solution:**
583```bash
584# Check build logs
585docker compose -f docker-compose.dev.yml logs atcr-appview
586
587# Or check build-errors.log in container
588docker compose -f docker-compose.dev.yml exec atcr-appview cat build-errors.log
589
590# Fix the Go error, save file, Air will retry
591```
592
593### Volume Mount Not Working
594
595**Problem:** Changes don't appear in container
596
597**Solution:**
598```bash
599# Verify volume mount
600docker compose -f docker-compose.dev.yml exec atcr-appview ls -la /app
601
602# Should show your source files
603
604# On Windows/Mac, check Docker Desktop file sharing settings
605# Settings → Resources → File Sharing → add project directory
606```
607
608### Permission Errors
609
610**Problem:** Cannot write to /var/lib/atcr
611
612**Solution:**
613```bash
614# In Dockerfile.devel, add:
615RUN mkdir -p /var/lib/atcr && chmod 777 /var/lib/atcr
616
617# Or use named volumes (already in docker-compose.dev.yml)
618volumes:
619 - atcr-ui-dev:/var/lib/atcr
620```
621
622### Slow Builds Even with Air
623
624**Problem:** Air rebuilds slowly
625
626**Solution:**
627```bash
628# Use Go module cache volume (already in docker-compose.dev.yml)
629volumes:
630 - go-cache:/go/pkg/mod
631
632# Increase Air delay to debounce rapid saves
633# In .air.toml:
634delay = 2000 # 2 seconds
635
636# Or check if CGO is slowing builds
637# AppView needs CGO for SQLite, but you can try:
638CGO_ENABLED=0 go build # (won't work for ATCR, but good to know)
639```
640
641## Tips & Tricks
642
643### Browser Auto-Reload (LiveReload)
644
645Add LiveReload for automatic browser refresh:
646
647```bash
648# Install browser extension
649# Chrome: https://chrome.google.com/webstore/detail/livereload
650# Firefox: https://addons.mozilla.org/en-US/firefox/addon/livereload-web-extension/
651
652# Add livereload to .air.toml (future Air feature)
653# Or use a separate tool like browsersync
654```
655
656### Database Resets
657
658Development database is in a named volume:
659
660```bash
661# Reset database (fresh start)
662docker compose -f docker-compose.dev.yml down -v
663docker compose -f docker-compose.dev.yml up
664
665# Or delete specific volume
666docker volume rm atcr_atcr-ui-dev
667```
668
669### Multiple Environments
670
671Run dev and production side-by-side:
672
673```bash
674# Development on port 5000
675docker compose -f docker-compose.dev.yml up -d
676
677# Production on port 5001
678docker compose up -d
679
680# Now you can compare behavior
681```
682
683### Debugging with Delve
684
685Add delve to Dockerfile.devel:
686
687```dockerfile
688RUN go install github.com/go-delve/delve/cmd/dlv@latest
689
690# Change CMD to use delve
691CMD ["dlv", "debug", "./cmd/appview", "--headless", "--listen=:2345", "--api-version=2", "--accept-multiclient", "--", "serve"]
692```
693
694Then connect with VSCode or GoLand.
695
696## Summary
697
698**Development Setup (One-Time):**
6991. Create `Dockerfile.devel`
7002. Create `docker-compose.dev.yml`
7013. Create `.air.toml`
7024. Modify `pkg/appview/ui.go` for conditional DirFS
7035. Add `tmp/` to `.gitignore`
704
705**Daily Development:**
706```bash
707# Start
708docker compose -f docker-compose.dev.yml up
709
710# Edit files in your editor
711# Changes appear instantly (CSS/JS/templates)
712# Or in 2-5 seconds (Go code)
713
714# Stop
715docker compose -f docker-compose.dev.yml down
716```
717
718**Production (Unchanged):**
719```bash
720docker compose build
721docker compose up
722```
723
724**Result:** 100x faster development iteration! 🚀