# Development Workflow for ATCR ## The Problem **Current development cycle with Docker:** 1. Edit CSS, JS, template, or Go file 2. Run `docker compose build` (rebuilds entire image) 3. Run `docker compose up` (restart container) 4. Wait **2-3 minutes** for changes to appear 5. Test, find issue, repeat... **Why it's slow:** - All assets embedded via `embed.FS` at compile time - Multi-stage Docker build compiles everything from scratch - No development mode exists - Final image uses `scratch` base (no tools, no hot reload) ## The Solution **Development setup combining:** 1. **Dockerfile.devel** - Development-focused container (golang base, not scratch) 2. **Volume mounts** - Live code editing (changes appear instantly in container) 3. **DirFS** - Skip embed, read templates/CSS/JS from filesystem 4. **Air** - Auto-rebuild on Go code changes **Results:** - CSS/JS/Template changes: **Instant** (0 seconds, just refresh browser) - Go code changes: **2-5 seconds** (vs 2-3 minutes) - Production builds: **Unchanged** (still optimized with embed.FS) ## How It Works ### Architecture Flow ``` ┌─────────────────────────────────────────────────────┐ │ Your Editor (VSCode, etc) │ │ Edit: style.css, app.js, *.html, *.go files │ └─────────────────┬───────────────────────────────────┘ │ (files saved to disk) ▼ ┌─────────────────────────────────────────────────────┐ │ Volume Mount (docker-compose.dev.yml) │ │ volumes: │ │ - .:/app (entire codebase mounted) │ └─────────────────┬───────────────────────────────────┘ │ (changes appear instantly in container) ▼ ┌─────────────────────────────────────────────────────┐ │ Container (golang:1.25.2 base, has all tools) │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ Air (hot reload tool) │ │ │ │ Watches: *.go, *.html, *.css, *.js │ │ │ │ │ │ │ │ On change: │ │ │ │ - *.go → rebuild binary (2-5s) │ │ │ │ - templates/css/js → restart only │ │ │ └──────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────┐ │ │ │ ATCR AppView (ATCR_DEV_MODE=true) │ │ │ │ │ │ │ │ ui.go checks DEV_MODE: │ │ │ │ if DEV_MODE: │ │ │ │ templatesFS = os.DirFS("...") │ │ │ │ staticFS = os.DirFS("...") │ │ │ │ else: │ │ │ │ use embed.FS (production) │ │ │ │ │ │ │ │ Result: Reads from mounted files │ │ │ └──────────────────────────────────────┘ │ └─────────────────────────────────────────────────────┘ ``` ### Change Scenarios #### Scenario 1: Edit CSS/JS/Templates ``` 1. Edit pkg/appview/static/css/style.css in VSCode 2. Save file 3. Change appears in container via volume mount (instant) 4. App uses os.DirFS → reads new file from disk (instant) 5. Refresh browser → see changes ``` **Time:** **Instant** (0 seconds) **No rebuild, no restart!** #### Scenario 2: Edit Go Code ``` 1. Edit pkg/appview/handlers/home.go 2. Save file 3. Air detects .go file change 4. Air runs: go build -o ./tmp/atcr-appview ./cmd/appview 5. Air kills old process and starts new binary 6. App runs with new code ``` **Time:** **2-5 seconds** **Fast incremental build!** ## Implementation ### Step 1: Create Dockerfile.devel Create `Dockerfile.devel` in project root: ```dockerfile # Development Dockerfile with hot reload support FROM golang:1.25.2-trixie # Install Air for hot reload RUN go install github.com/cosmtrek/air@latest # Install SQLite (required for CGO in ATCR) RUN apt-get update && apt-get install -y \ sqlite3 \ libsqlite3-dev \ && rm -rf /var/lib/apt/lists/* WORKDIR /app # Copy dependency files and download (cached layer) COPY go.mod go.sum ./ RUN go mod download # Note: Source code comes from volume mount # (no COPY . . needed - that's the whole point!) # Air will handle building and running CMD ["air", "-c", ".air.toml"] ``` ### Step 2: Create docker-compose.dev.yml Create `docker-compose.dev.yml` in project root: ```yaml version: '3.8' services: atcr-appview: build: context: . dockerfile: Dockerfile.devel volumes: # Mount entire codebase (live editing) - .:/app # Cache Go modules (faster rebuilds) - go-cache:/go/pkg/mod # Persist SQLite database - atcr-ui-dev:/var/lib/atcr environment: # Enable development mode (uses os.DirFS) ATCR_DEV_MODE: "true" # AppView configuration ATCR_HTTP_ADDR: ":5000" ATCR_BASE_URL: "http://localhost:5000" ATCR_DEFAULT_HOLD_DID: "did:web:hold01.atcr.io" # Database ATCR_UI_DATABASE_PATH: "/var/lib/atcr/ui.db" # Auth ATCR_AUTH_KEY_PATH: "/var/lib/atcr/auth/private-key.pem" # UI ATCR_UI_ENABLED: "true" # Jetstream (optional) # JETSTREAM_URL: "wss://jetstream2.us-east.bsky.network/subscribe" # ATCR_BACKFILL_ENABLED: "false" ports: - "5000:5000" networks: - atcr-dev # Add other services as needed (postgres, hold, etc) # atcr-hold: # ... networks: atcr-dev: driver: bridge volumes: go-cache: atcr-ui-dev: ``` ### Step 3: Create .air.toml Create `.air.toml` in project root: ```toml # Air configuration for hot reload # https://github.com/cosmtrek/air root = "." testdata_dir = "testdata" tmp_dir = "tmp" [build] # Arguments to pass to binary (AppView needs "serve") args_bin = ["serve"] # Where to output the built binary bin = "./tmp/atcr-appview" # Build command cmd = "go build -o ./tmp/atcr-appview ./cmd/appview" # Delay before rebuilding (ms) - debounce rapid saves delay = 1000 # Directories to exclude from watching exclude_dir = [ "tmp", "vendor", "bin", ".git", "node_modules", "testdata" ] # Files to exclude from watching exclude_file = [] # Regex patterns to exclude exclude_regex = ["_test\\.go"] # Don't rebuild if file content unchanged exclude_unchanged = false # Follow symlinks follow_symlink = false # Full command to run (leave empty to use cmd + bin) full_bin = "" # Directories to include (empty = all) include_dir = [] # File extensions to watch include_ext = ["go", "html", "css", "js"] # Specific files to watch include_file = [] # Delay before killing old process (s) kill_delay = "0s" # Log file for build errors log = "build-errors.log" # Use polling instead of fsnotify (for Docker/VM) poll = false poll_interval = 0 # Rerun binary if it exits rerun = false rerun_delay = 500 # Send interrupt signal instead of kill send_interrupt = false # Stop on build error stop_on_error = false [color] # Colorize output app = "" build = "yellow" main = "magenta" runner = "green" watcher = "cyan" [log] # Show only app logs (not build logs) main_only = false # Add timestamp to logs time = false [misc] # Clean tmp directory on exit clean_on_exit = false [screen] # Clear screen on rebuild clear_on_rebuild = false # Keep scrollback keep_scroll = true ``` ### Step 4: Modify pkg/appview/ui.go Add conditional filesystem loading to `pkg/appview/ui.go`: ```go package appview import ( "embed" "html/template" "io/fs" "log" "net/http" "os" ) // Embedded assets (used in production) //go:embed templates/**/*.html var embeddedTemplatesFS embed.FS //go:embed static var embeddedStaticFS embed.FS // Actual filesystems used at runtime (conditional) var templatesFS fs.FS var staticFS fs.FS func init() { // Development mode: read from filesystem for instant updates if os.Getenv("ATCR_DEV_MODE") == "true" { log.Println("🔧 DEV MODE: Using filesystem for templates and static assets") templatesFS = os.DirFS("pkg/appview/templates") staticFS = os.DirFS("pkg/appview/static") } else { // Production mode: use embedded assets log.Println("📦 PRODUCTION MODE: Using embedded assets") templatesFS = embeddedTemplatesFS staticFS = embeddedStaticFS } } // Templates returns parsed HTML templates func Templates() *template.Template { tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html") if err != nil { log.Fatalf("Failed to parse templates: %v", err) } return tmpl } // StaticHandler returns a handler for static files func StaticHandler() http.Handler { sub, err := fs.Sub(staticFS, "static") if err != nil { log.Fatalf("Failed to create static sub-filesystem: %v", err) } return http.FileServer(http.FS(sub)) } ``` **Important:** Update the `Templates()` function to NOT cache templates in dev mode: ```go // Templates returns parsed HTML templates func Templates() *template.Template { // In dev mode, reparse templates on every request (instant updates) // In production, this could be cached tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html") if err != nil { log.Fatalf("Failed to parse templates: %v", err) } return tmpl } ``` If you're caching templates, wrap it with a dev mode check: ```go var templateCache *template.Template func Templates() *template.Template { // Development: reparse every time (instant updates) if os.Getenv("ATCR_DEV_MODE") == "true" { tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html") if err != nil { log.Printf("Template parse error: %v", err) return template.New("error") } return tmpl } // Production: use cached templates if templateCache == nil { tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html") if err != nil { log.Fatalf("Failed to parse templates: %v", err) } templateCache = tmpl } return templateCache } ``` ### Step 5: Add to .gitignore Add Air's temporary directory to `.gitignore`: ``` # Air hot reload tmp/ build-errors.log ``` ## Usage ### Starting Development Environment ```bash # Build and start dev container docker compose -f docker-compose.dev.yml up --build # Or run in background docker compose -f docker-compose.dev.yml up -d # View logs docker compose -f docker-compose.dev.yml logs -f atcr-appview ``` You should see Air starting: ``` atcr-appview | 🔧 DEV MODE: Using filesystem for templates and static assets atcr-appview | atcr-appview | __ _ ___ atcr-appview | / /\ | | | |_) atcr-appview | /_/--\ |_| |_| \_ , built with Go atcr-appview | atcr-appview | watching . atcr-appview | !exclude tmp atcr-appview | building... atcr-appview | running... ``` ### Development Workflow #### 1. Edit Templates/CSS/JS (Instant Updates) ```bash # Edit any template, CSS, or JS file vim pkg/appview/templates/pages/home.html vim pkg/appview/static/css/style.css vim pkg/appview/static/js/app.js # Save file → changes appear instantly # Just refresh browser (Cmd+R / Ctrl+R) ``` **No rebuild, no restart!** Air might restart the app, but it's instant since no compilation is needed. #### 2. Edit Go Code (Fast Rebuild) ```bash # Edit any Go file vim pkg/appview/handlers/home.go # Save file → Air detects change # Air output shows: # building... # build successful in 2.3s # restarting... # Refresh browser to see changes ``` **2-5 second rebuild** instead of 2-3 minutes! ### Stopping Development Environment ```bash # Stop containers docker compose -f docker-compose.dev.yml down # Stop and remove volumes (fresh start) docker compose -f docker-compose.dev.yml down -v ``` ## Production Builds **Production builds are completely unchanged:** ```bash # Production uses normal Dockerfile (embed.FS, scratch base) docker compose build # Or specific service docker compose build atcr-appview # Run production docker compose up ``` **Why it works:** - Production doesn't set `ATCR_DEV_MODE=true` - `ui.go` defaults to embedded assets when env var is unset - Production Dockerfile still uses multi-stage build to scratch - No development dependencies in production image ## Comparison | Change Type | Before (docker compose) | After (dev setup) | Improvement | |-------------|------------------------|-------------------|-------------| | Edit CSS | 2-3 minutes | **Instant (0s)** | ♾️x faster | | Edit JS | 2-3 minutes | **Instant (0s)** | ♾️x faster | | Edit Template | 2-3 minutes | **Instant (0s)** | ♾️x faster | | Edit Go Code | 2-3 minutes | **2-5 seconds** | 24-90x faster | | Production Build | Same | **Same** | No change | ## Advanced: Local Development (No Docker) For even faster development, run locally without Docker: ```bash # Set environment variables export ATCR_DEV_MODE=true export ATCR_HTTP_ADDR=:5000 export ATCR_BASE_URL=http://localhost:5000 export ATCR_DEFAULT_HOLD_DID=did:web:hold01.atcr.io export ATCR_UI_DATABASE_PATH=/tmp/atcr-ui.db export ATCR_AUTH_KEY_PATH=/tmp/atcr-auth-key.pem export ATCR_UI_ENABLED=true # Or use .env file source .env.appview # Run with Air air -c .air.toml # Or run directly (no hot reload) go run ./cmd/appview serve ``` **Advantages:** - Even faster (no Docker overhead) - Native debugging with delve - Direct filesystem access - Full IDE integration **Disadvantages:** - Need to manage dependencies locally (SQLite, etc) - May differ from production environment ## Troubleshooting ### Air Not Rebuilding **Problem:** Air doesn't detect changes **Solution:** ```bash # Check if Air is actually running docker compose -f docker-compose.dev.yml logs atcr-appview # Check .air.toml include_ext includes your file type # Default: ["go", "html", "css", "js"] # Restart container docker compose -f docker-compose.dev.yml restart atcr-appview ``` ### Templates Not Updating **Problem:** Template changes don't appear **Solution:** ```bash # Check ATCR_DEV_MODE is set docker compose -f docker-compose.dev.yml exec atcr-appview env | grep DEV_MODE # Should output: ATCR_DEV_MODE=true # Check templates aren't cached (see Step 4 above) # Templates() should reparse in dev mode ``` ### Go Build Failing **Problem:** Air shows build errors **Solution:** ```bash # Check build logs docker compose -f docker-compose.dev.yml logs atcr-appview # Or check build-errors.log in container docker compose -f docker-compose.dev.yml exec atcr-appview cat build-errors.log # Fix the Go error, save file, Air will retry ``` ### Volume Mount Not Working **Problem:** Changes don't appear in container **Solution:** ```bash # Verify volume mount docker compose -f docker-compose.dev.yml exec atcr-appview ls -la /app # Should show your source files # On Windows/Mac, check Docker Desktop file sharing settings # Settings → Resources → File Sharing → add project directory ``` ### Permission Errors **Problem:** Cannot write to /var/lib/atcr **Solution:** ```bash # In Dockerfile.devel, add: RUN mkdir -p /var/lib/atcr && chmod 777 /var/lib/atcr # Or use named volumes (already in docker-compose.dev.yml) volumes: - atcr-ui-dev:/var/lib/atcr ``` ### Slow Builds Even with Air **Problem:** Air rebuilds slowly **Solution:** ```bash # Use Go module cache volume (already in docker-compose.dev.yml) volumes: - go-cache:/go/pkg/mod # Increase Air delay to debounce rapid saves # In .air.toml: delay = 2000 # 2 seconds # Or check if CGO is slowing builds # AppView needs CGO for SQLite, but you can try: CGO_ENABLED=0 go build # (won't work for ATCR, but good to know) ``` ## Tips & Tricks ### Browser Auto-Reload (LiveReload) Add LiveReload for automatic browser refresh: ```bash # Install browser extension # Chrome: https://chrome.google.com/webstore/detail/livereload # Firefox: https://addons.mozilla.org/en-US/firefox/addon/livereload-web-extension/ # Add livereload to .air.toml (future Air feature) # Or use a separate tool like browsersync ``` ### Database Resets Development database is in a named volume: ```bash # Reset database (fresh start) docker compose -f docker-compose.dev.yml down -v docker compose -f docker-compose.dev.yml up # Or delete specific volume docker volume rm atcr_atcr-ui-dev ``` ### Multiple Environments Run dev and production side-by-side: ```bash # Development on port 5000 docker compose -f docker-compose.dev.yml up -d # Production on port 5001 docker compose up -d # Now you can compare behavior ``` ### Debugging with Delve Add delve to Dockerfile.devel: ```dockerfile RUN go install github.com/go-delve/delve/cmd/dlv@latest # Change CMD to use delve CMD ["dlv", "debug", "./cmd/appview", "--headless", "--listen=:2345", "--api-version=2", "--accept-multiclient", "--", "serve"] ``` Then connect with VSCode or GoLand. ## Summary **Development Setup (One-Time):** 1. Create `Dockerfile.devel` 2. Create `docker-compose.dev.yml` 3. Create `.air.toml` 4. Modify `pkg/appview/ui.go` for conditional DirFS 5. Add `tmp/` to `.gitignore` **Daily Development:** ```bash # Start docker compose -f docker-compose.dev.yml up # Edit files in your editor # Changes appear instantly (CSS/JS/templates) # Or in 2-5 seconds (Go code) # Stop docker compose -f docker-compose.dev.yml down ``` **Production (Unchanged):** ```bash docker compose build docker compose up ``` **Result:** 100x faster development iteration! 🚀