Files
pirate-station/.planning/phases/01-foundation/01-RESEARCH.md
acty ccb93eda21 docs(01): create phase 1 foundation plans
Phase 01: Foundation
- 2 plans in 2 waves
- Plan 01: Go project + Dockerfile (wave 1)
- Plan 02: Dev environment + verification (wave 2)
- Ready for execution

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 16:56:50 +09:00

511 lines
18 KiB
Markdown

# Phase 1: Foundation - Research
**Researched:** 2026-02-03
**Domain:** Go backend containerization with Docker
**Confidence:** HIGH
## Summary
This phase establishes a Go HTTP backend running in an isolated Docker container with volume-mounted storage. The standard approach uses multi-stage Dockerfiles with golang:latest for building and debian:slim for runtime, deployed via Docker Compose for development and direct builds on the Raspberry Pi.
The research validates all locked decisions from the context discussion: Debian slim provides better compatibility than Alpine for cross-platform development, buildx handles multi-arch builds (x86_64 + ARM64) seamlessly, and Docker Compose with bind mounts enables fast iteration during local development. The high port (32768) avoids common service conflicts, and standard volume isolation provides adequate security without filesystem hardening overhead.
Current Go stable versions are 1.24 and 1.25 (Go 1.26 releases February 2026). Official Go project layout guidance recommends `internal/` for private packages and `cmd/` for multiple commands, with module paths matching repository URLs.
**Primary recommendation:** Use multi-stage Dockerfile with Go 1.25+, debian:slim base, buildx for multi-arch, and Air for development hot reload. Follow official Go module layout with `cmd/` and `internal/` directories.
## Standard Stack
The established libraries/tools for this domain:
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| Go | 1.25+ | Backend language | Latest stable (1.24/1.25 supported) |
| Docker Buildx | bundled | Multi-arch builds | Official Docker tool for cross-platform |
| Docker Compose | v2 | Dev orchestration | Standard for local development workflows |
| golang:1.25 | latest stable | Build stage base | Official Go image with full toolchain |
| debian:slim | 12 (bookworm) | Runtime base | Balance of size, compatibility, debugging |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| Air | latest | Hot reload | Development only - NOT for production |
| gcr.io/distroless/base | latest | Alternative runtime | When minimal image size is critical |
| alpine:latest | 3.19+ | Alternative runtime | When musl libc compatibility is acceptable |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| debian:slim | gcr.io/distroless/base | Smaller (~20MB vs ~80MB) but no shell for debugging |
| debian:slim | alpine:latest | Smaller (~5MB) but musl libc can cause unexpected behavior with net package |
| Manual builds | GitHub Actions | Automated but adds complexity for transitional dev environment |
**Installation:**
```bash
# Go toolchain
# Download from https://go.dev/dl/ or use system package manager
# Docker Buildx (included in Docker Desktop, or install separately)
docker buildx version
# Air for hot reload (development only)
go install github.com/cosmtrek/air@latest
```
## Architecture Patterns
### Recommended Project Structure
```
pirate-station/
├── cmd/
│ └── server/ # Main application entry point
│ └── main.go
├── internal/ # Private application code
│ ├── server/ # HTTP server setup
│ ├── storage/ # /data volume operations
│ └── health/ # Health check handlers
├── docker/
│ └── Dockerfile # Multi-stage build definition
├── docker-compose.yml # Development environment
├── .air.toml # Air hot reload config
├── .dockerignore # Exclude from build context
├── go.mod
└── go.sum
```
**Rationale:**
- `cmd/server/` - Single command, follows official Go layout for commands
- `internal/` - Prevents external imports, recommended for server packages
- `docker/` - Separates infrastructure from code
- Root-level compose file - Standard Docker Compose convention
### Pattern 1: Multi-Stage Dockerfile with Cross-Compilation
**What:** Build stage compiles Go binary, runtime stage creates minimal production image
**When to use:** Always - reduces image size by 95% and improves security
**Example:**
```dockerfile
# Build stage - use native platform for speed
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS builder
WORKDIR /build
# Cache dependencies separately
COPY go.mod go.sum ./
RUN go mod download
# Copy source and build for target platform
COPY . .
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -ldflags="-w -s" -o /server ./cmd/server
# Runtime stage - debian slim for debugging
FROM debian:bookworm-slim
# Run as non-root
RUN useradd -u 10001 -m appuser
USER appuser
COPY --from=builder /server /usr/local/bin/server
# Volume mount point
VOLUME /data
EXPOSE 32768
CMD ["server"]
```
**Source:** Official Docker documentation - [Multi-platform builds](https://docs.docker.com/build/building/multi-platform/)
### Pattern 2: Docker Compose Development Environment
**What:** Volume-mounted source with hot reload for rapid iteration
**When to use:** Local development only
**Example:**
```yaml
# docker-compose.yml
services:
backend:
build:
context: .
dockerfile: docker/Dockerfile
target: builder # Stop at build stage
command: air -c .air.toml # Hot reload
ports:
- "32768:32768"
volumes:
- .:/workspace:cached # Bind mount for live editing
- data:/data # Named volume for persistent data
working_dir: /workspace
environment:
- CGO_ENABLED=0
volumes:
data:
```
**Source:** Docker Compose documentation - [Development best practices](https://docs.docker.com/guides/golang/develop/)
### Pattern 3: Health Check Endpoint
**What:** HTTP endpoint returning 200 when ready, 503 when unhealthy
**When to use:** Required for production deployments, useful in development
**Example:**
```go
// internal/health/handler.go
package health
import (
"net/http"
"os"
)
func Handler(w http.ResponseWriter, r *http.Request) {
// Check data volume is accessible
if _, err := os.Stat("/data"); os.IsNotExist(err) {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte(`{"status":"unhealthy","reason":"data volume not mounted"}`))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"healthy"}`))
}
```
**Source:** Patterns from [heptiolabs/healthcheck](https://pkg.go.dev/github.com/heptiolabs/healthcheck) and [Kubernetes health checks guide](https://oneuptime.com/blog/post/2026-01-07-go-health-checks-kubernetes/view)
### Anti-Patterns to Avoid
- **Latest tag in production:** Always pin specific versions (golang:1.25-bookworm not golang:latest)
- **Single-stage Dockerfile:** Bloats image with build tools, increases attack surface
- **Root user in container:** Security risk - always create non-root user
- **Copying unnecessary files:** Use .dockerignore to exclude .git, test files, etc.
- **Running Air in production:** Hot reload tools are development-only
## Don't Hand-Roll
Problems that look simple but have existing solutions:
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Health check logic | Custom health tracking | Standard /health endpoint pattern | Kubernetes/Docker expect specific format |
| Hot reload mechanism | File watching + restart script | Air (cosmtrek/air) | Handles Go build errors, configurable, maintained |
| Build caching | Manual cache dirs | Docker BuildKit layer caching | Optimized for dependencies (go.mod) vs source |
| Multi-arch builds | Multiple Dockerfiles | docker buildx with --platform | Cross-compiles natively, single source of truth |
| Static binary linking | Complex ldflags research | CGO_ENABLED=0 + -ldflags="-w -s" | Standard Go approach, strips debug symbols |
**Key insight:** Docker's native tooling (BuildKit, buildx) and Go's built-in cross-compilation handle most complexity. Avoid custom shell scripts for build processes.
## Common Pitfalls
### Pitfall 1: Poor Layer Caching in Dockerfile
**What goes wrong:** Every code change triggers full dependency reinstall, dramatically slowing builds
**Why it happens:** Copying all source files before running `go mod download` invalidates cache when any file changes
**How to avoid:** Copy go.mod and go.sum first, download dependencies, then copy source code
**Warning signs:**
- "Downloading dependencies" on every build even when go.mod unchanged
- Multi-minute builds for trivial code changes
**Example:**
```dockerfile
# BAD - invalidates cache on any file change
COPY . .
RUN go mod download
# GOOD - cache dependencies separately
COPY go.mod go.sum ./
RUN go mod download
COPY . .
```
**Source:** Docker best practices - [Dockerfile optimization](https://medium.com/@mecreate/dockerizing-a-go-application-a-complete-guide-with-best-practices-5648d4eb362c)
### Pitfall 2: Alpine + net Package DNS Issues
**What goes wrong:** DNS lookups fail or behave unexpectedly when using Alpine Linux base images
**Why it happens:** Alpine uses musl libc instead of glibc, affecting Go's net package behavior unless compiled with CGO_ENABLED=0
**How to avoid:**
- Use Debian slim instead of Alpine (decision already made)
- Or ensure CGO_ENABLED=0 for pure Go DNS resolver
**Warning signs:**
- DNS resolution works locally but fails in Alpine container
- Intermittent connection errors to external services
**Source:** Go package documentation and community reports - [Alpine vs Debian comparison](https://mohibulalam75.medium.com/exploring-lightweight-docker-base-images-alpine-slim-and-debian-releases-bookworm-bullseye-688f88067f4b)
### Pitfall 3: CGO Enabled by Default in Cross-Compilation
**What goes wrong:** Cross-compiled binaries fail to run on target architecture with "file not found" errors despite binary existing
**Why it happens:** CGO is enabled by default, creating dynamic links to C libraries that may not exist on target system
**How to avoid:** Explicitly set CGO_ENABLED=0 in Dockerfile for static binary compilation
**Warning signs:**
- Binary works on build machine but fails on target
- "no such file or directory" error for binary that exists
- Larger binary size than expected
**Example:**
```dockerfile
# REQUIRED for portable binaries
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -o /server ./cmd/server
```
**Source:** Official Docker Go guide - [Building Go images](https://docs.docker.com/guides/golang/build-images/)
### Pitfall 4: Forgetting .dockerignore
**What goes wrong:** Huge build contexts, slow builds, sensitive files copied to image
**Why it happens:** Docker copies entire directory context by default, including .git, test data, and local builds
**How to avoid:** Create .dockerignore file early in development
**Warning signs:**
- "Sending build context" takes seconds
- Docker build transfers hundreds of MB
- Accidentally including .env files
**Example .dockerignore:**
```
.git
.gitignore
README.md
.env
.env.local
*.md
.DS_Store
.air.toml
docker-compose.yml
.planning/
```
**Source:** Docker best practices - [Common Dockerfile mistakes](https://runnable.com/blog/9-common-dockerfile-mistakes)
### Pitfall 5: Bind Mounting in Production
**What goes wrong:** Using docker-compose.yml from development in production, exposing host filesystem
**Why it happens:** Not separating development and production configurations
**How to avoid:**
- Use bind mounts only in development (docker-compose.yml)
- Use named volumes or direct mounts in production deployment
- Never bind mount sensitive host directories
**Warning signs:**
- Host files accessible from production container
- Container can modify unexpected host paths
- Security audit flags container filesystem access
**Source:** Docker security guidance - [Volume mount security](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html)
## Code Examples
Verified patterns from official sources:
### Minimal HTTP Server with Health Check
```go
// cmd/server/main.go
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
http.HandleFunc("/health", healthHandler)
http.HandleFunc("/", rootHandler)
port := ":32768"
log.Printf("Server starting on port %s", port)
if err := http.ListenAndServe(port, nil); err != nil {
log.Fatal(err)
}
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
// Verify data volume is mounted
if _, err := os.Stat("/data"); os.IsNotExist(err) {
w.WriteHeader(http.StatusServiceUnavailable)
fmt.Fprintf(w, `{"status":"unhealthy"}`)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"status":"healthy"}`)
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Pirate Station API")
}
```
**Source:** Standard Go net/http patterns from [go.dev documentation](https://go.dev/doc/)
### Multi-Arch Build Command
```bash
# Create builder (one-time setup)
docker buildx create --name pirate-builder --bootstrap --use
# Build and push multi-arch image
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag gitea.local/acty/pirate-station:latest \
--push \
-f docker/Dockerfile \
.
```
**Source:** Official Docker buildx documentation - [Multi-platform builds](https://docs.docker.com/build/building/multi-platform/)
### Air Configuration for Hot Reload
```toml
# .air.toml
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/server"
cmd = "go build -o ./tmp/server ./cmd/server"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", ".planning", "docker"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true
```
**Source:** Air documentation - [github.com/air-verse/air](https://github.com/air-verse/air)
### Gitea Registry Push
```bash
# Login to Gitea container registry
docker login gitea.local
# Tag image with Gitea format
docker tag pirate-station:latest gitea.local/acty/pirate-station:latest
# Push to registry
docker push gitea.local/acty/pirate-station:latest
```
**Source:** Gitea container registry documentation - [Gitea docs](https://docs.gitea.com/usage/packages/container)
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Alpine for minimal size | Debian slim for compatibility | Ongoing shift 2024-2026 | Better C library compatibility, easier debugging |
| Manual multi-arch builds | docker buildx with --platform | 2020+ (BuildKit) | Single command for cross-platform |
| CGO enabled by default | CGO_ENABLED=0 for containers | Standard practice now | True static binaries, better portability |
| golang:latest tag | golang:1.25-bookworm pinned | Best practice evolution | Reproducible builds |
| docker build | docker buildx build | 2020+ (BuildKit default) | Better caching, multi-platform support |
| Scratch runtime base | Distroless or Debian slim | 2022+ trend | Balance of size and debuggability |
**Deprecated/outdated:**
- **MAINTAINER instruction:** Use LABEL maintainer= instead (deprecated Docker syntax)
- **Docker Compose v1:** Use v2 with `docker compose` command (not docker-compose)
- **Go 1.21 and earlier:** Out of support - use 1.24+ (two most recent major releases supported)
## Open Questions
Things that couldn't be fully resolved:
1. **Gitea multi-arch manifest support**
- What we know: Gitea supports OCI-compliant container registry
- What's unclear: Some GitHub issues report buildx multi-arch push creating "unknown architectures"
- Recommendation: Test multi-arch push during phase implementation; fallback to building directly on Pi if issues arise
2. **Air performance in Docker on Pi**
- What we know: Air works well for development, not recommended for production
- What's unclear: Performance impact of file watching on bind-mounted volumes on Raspberry Pi
- Recommendation: Test in development, may fall back to manual rebuild if too slow
3. **Debian slim vs distroless tradeoff**
- What we know: User chose Debian slim for debugging, distroless is smaller
- What's unclear: How often shell access is needed in practice
- Recommendation: Start with Debian slim as decided, can switch to distroless later if shell not needed
## Sources
### Primary (HIGH confidence)
- Docker official documentation - [Multi-platform builds](https://docs.docker.com/build/building/multi-platform/)
- Docker official documentation - [Building Go images](https://docs.docker.com/guides/golang/build-images/)
- Go official documentation - [Module layout](https://go.dev/doc/modules/layout)
- Go official documentation - [Package names](https://go.dev/blog/package-names)
- Go version status - [endoflife.date/go](https://endoflife.date/go)
- Gitea container registry - [Official docs](https://docs.gitea.com/usage/packages/container)
### Secondary (MEDIUM confidence)
- Air hot reload tool - [github.com/air-verse/air](https://github.com/air-verse/air)
- Go project layout reference - [golang-standards/project-layout](https://github.com/golang-standards/project-layout)
- Health check patterns - [Kubernetes Go health checks](https://oneuptime.com/blog/post/2026-01-07-go-health-checks-kubernetes/view)
- Docker best practices - [Dockerizing Go applications](https://medium.com/@mecreate/dockerizing-a-go-application-a-complete-guide-with-best-practices-5648d4eb362c)
### Tertiary (LOW confidence)
- Gitea multi-arch buildx issues - Community reports from GitHub issues, marked for validation during implementation
- Alpine vs Debian tradeoffs - Multiple blog posts agree, but requires real-world testing for specific use case
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH - Official documentation and current best practices verified
- Architecture: HIGH - Official Go and Docker documentation provides clear patterns
- Pitfalls: HIGH - Well-documented common issues with official solutions
- Gitea integration: MEDIUM - Official docs available but multi-arch behavior needs testing
**Research date:** 2026-02-03
**Valid until:** 2026-04-03 (60 days - Docker and Go tooling relatively stable)