A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
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! 🚀