mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-08 08:03:28 +00:00
feat: add make watch hot-reload mode for go services via air
This commit is contained in:
@@ -14,7 +14,8 @@ PROTO_GEN_FILES := $(PROTO_DIR)/tasks.pb.go
|
||||
|
||||
.PHONY: setup-tools lint proto check-proto \
|
||||
dev sim seed reset logs status stop down tools test-seed \
|
||||
restart restart-go restart-all cache-clean
|
||||
restart restart-go restart-all cache-clean \
|
||||
watch watch-down watch-logs
|
||||
|
||||
setup-tools:
|
||||
@echo "Installing required Go tools into $(GO_BIN)"
|
||||
@@ -123,6 +124,37 @@ restart-all:
|
||||
$(DOCKER_ENV) docker compose build --parallel backend consumer worker-shared-1 tracking realtime
|
||||
docker compose up -d backend consumer worker-shared-1 tracking realtime
|
||||
|
||||
# ─── hot-reload (air) ───────────────────────────────────────────────────
|
||||
#
|
||||
# Bind-mounted source + air file watcher inside a long-running golang
|
||||
# container. Edit a .go file → air rebuilds (~2-5s) → restarts the
|
||||
# binary in place. No docker image rebuild, no container recreate.
|
||||
#
|
||||
# First boot takes ~30s to populate the warmbly_gomodcache /
|
||||
# warmbly_gocache volumes, but those persist across worktrees (the
|
||||
# volume names skip the per-project prefix), so subsequent worktrees
|
||||
# start watching almost instantly.
|
||||
#
|
||||
# The default `make dev` / `make restart-go` flow still uses the
|
||||
# production-style Dockerfiles. Watch is opt-in.
|
||||
|
||||
WATCH_COMPOSE := docker compose -f docker-compose.yml -f docker-compose.watch.yml
|
||||
WATCH_SVCS := backend consumer worker-shared-1
|
||||
|
||||
watch:
|
||||
$(DOCKER_ENV) $(WATCH_COMPOSE) up -d --build $(WATCH_SVCS)
|
||||
@echo ""
|
||||
@echo "Hot-reload running. Edit any .go file under cmd/ or internal/ to trigger a rebuild."
|
||||
@echo "Stream logs: make watch-logs"
|
||||
@echo "Stop watch: make watch-down"
|
||||
|
||||
watch-down:
|
||||
$(WATCH_COMPOSE) stop $(WATCH_SVCS)
|
||||
@echo "watch services stopped. Run 'make restart-go' to return to production-style builds."
|
||||
|
||||
watch-logs:
|
||||
$(WATCH_COMPOSE) logs -f --tail=200 $(WATCH_SVCS)
|
||||
|
||||
# Force-drop the BuildKit cache. Useful when something's gone weird
|
||||
# (corrupted cache, debugging a "works on a clean build but not after
|
||||
# a rebuild" issue). The shared Go module + build caches will rebuild
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# air config for the warmbly hot-reload dev container.
|
||||
#
|
||||
# Generic across all three Go services — the build target is read from
|
||||
# the WARMBLY_CMD env var, which is set per service in
|
||||
# docker-compose.yml (backend / consumer / worker).
|
||||
#
|
||||
# Tradeoffs:
|
||||
# - `delay = 800` debounces save-bursts. Below ~500ms editors can fire
|
||||
# two file events for one save (write + flush), which kicks two
|
||||
# builds.
|
||||
# - `kill_delay = "2s"` gives the old binary time to drain in-flight
|
||||
# work (Kafka commits, HTTP requests) before the new one starts.
|
||||
# - `include_ext` deliberately omits `.sql` — migrations are embedded
|
||||
# and require an explicit re-embed (go build) which the .go change
|
||||
# in migrate.go will trigger anyway.
|
||||
|
||||
root = "."
|
||||
tmp_dir = "tmp"
|
||||
testdata_dir = "testdata"
|
||||
|
||||
[build]
|
||||
# Built with the same flags as the production Dockerfiles so the
|
||||
# behavior under watch matches what ships. -tags musl is required
|
||||
# because we're on alpine.
|
||||
cmd = "go build -tags musl -o ./tmp/main ./cmd/${WARMBLY_CMD}"
|
||||
bin = "./tmp/main"
|
||||
full_bin = ""
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
exclude_dir = [
|
||||
"web",
|
||||
"tracking",
|
||||
"realtime",
|
||||
"docs",
|
||||
"tmp",
|
||||
"bin",
|
||||
"dist",
|
||||
"build",
|
||||
".git",
|
||||
".github",
|
||||
".agentd",
|
||||
"node_modules",
|
||||
]
|
||||
include_dir = []
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test\\.go"]
|
||||
exclude_unchanged = true
|
||||
follow_symlink = false
|
||||
log = "build-errors.log"
|
||||
delay = 800
|
||||
stop_on_error = false
|
||||
send_interrupt = true
|
||||
kill_delay = "2s"
|
||||
|
||||
[color]
|
||||
main = "magenta"
|
||||
watcher = "cyan"
|
||||
build = "yellow"
|
||||
runner = "green"
|
||||
|
||||
[log]
|
||||
time = true
|
||||
|
||||
[misc]
|
||||
clean_on_exit = true
|
||||
@@ -0,0 +1,34 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Hot-reload image for Go services. Used only by the `watch` profile in
|
||||
# docker-compose.yml — production builds still use the per-service
|
||||
# Dockerfiles next to this one.
|
||||
#
|
||||
# The container expects:
|
||||
# - Source bind-mounted at /app
|
||||
# - $GOMODCACHE / $GOCACHE on named volumes (so caches survive
|
||||
# container recreates AND restarts of the host machine)
|
||||
# - $WARMBLY_CMD set to one of: backend, consumer, worker, seed
|
||||
#
|
||||
# air watches for changes under /app and rebuilds the cmd binary into
|
||||
# /tmp/main in place, then restarts it. No docker layer pipeline, no
|
||||
# image rebuild — just `go build` against a warm cache.
|
||||
|
||||
FROM golang:1.25-alpine
|
||||
|
||||
RUN apk add --no-cache git ca-certificates gcc musl-dev librdkafka-dev curl
|
||||
|
||||
# Pin air so the dev image is reproducible. air-verse/air is the
|
||||
# successor to cosmtrek/air (same project, new org).
|
||||
RUN go install github.com/air-verse/air@v1.61.5
|
||||
|
||||
ENV GOMODCACHE=/go/pkg/mod
|
||||
ENV GOCACHE=/root/.cache/go-build
|
||||
ENV GOTMPDIR=/tmp
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY deploy/docker/air.toml /etc/air.toml
|
||||
|
||||
ENTRYPOINT ["air", "-c", "/etc/air.toml"]
|
||||
@@ -0,0 +1,87 @@
|
||||
# Hot-reload override for the Go services.
|
||||
#
|
||||
# Activated via `make watch` (or `docker compose -f docker-compose.yml
|
||||
# -f docker-compose.watch.yml up backend consumer worker-shared-1`).
|
||||
#
|
||||
# What changes vs. the default compose:
|
||||
# - backend / consumer / worker-shared-1 swap to the go-dev image
|
||||
# (golang + air + librdkafka) instead of their thin runtime alpine.
|
||||
# - Source is bind-mounted at /app so saves are visible inside the
|
||||
# container without any image rebuild.
|
||||
# - Module + build caches live on named volumes (`warmbly_gomodcache`
|
||||
# and `warmbly_gocache`) so they survive container recreates AND
|
||||
# are shared across worktrees that point at the same Docker daemon.
|
||||
# - WARMBLY_CMD tells the shared air.toml which `cmd/` to build.
|
||||
#
|
||||
# What stays the same:
|
||||
# - Every env var, depends_on, healthcheck, port mapping. All the
|
||||
# interesting wiring is in the base compose; this file only swaps
|
||||
# the build/runtime mechanics.
|
||||
#
|
||||
# Notes:
|
||||
# - First start of a watch container takes ~30s to compile the binary
|
||||
# into the cache. Every save after that is ~2-5s.
|
||||
# - We disable the existing backend healthcheck because air's restart
|
||||
# loop causes brief health flaps that depends_on doesn't tolerate.
|
||||
# The consumer + worker still wait on `service_started` (alive) so
|
||||
# dependent containers wake up at the right time anyway.
|
||||
|
||||
services:
|
||||
backend:
|
||||
image: warmbly-go-dev
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deploy/docker/go-dev.Dockerfile
|
||||
environment:
|
||||
WARMBLY_CMD: backend
|
||||
volumes:
|
||||
- .:/app
|
||||
- warmbly_gomodcache:/go/pkg/mod
|
||||
- warmbly_gocache:/root/.cache/go-build
|
||||
healthcheck:
|
||||
disable: true
|
||||
|
||||
consumer:
|
||||
image: warmbly-go-dev
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deploy/docker/go-dev.Dockerfile
|
||||
environment:
|
||||
WARMBLY_CMD: consumer
|
||||
volumes:
|
||||
- .:/app
|
||||
- warmbly_gomodcache:/go/pkg/mod
|
||||
- warmbly_gocache:/root/.cache/go-build
|
||||
# Consumer waits on backend in the base file; under watch the
|
||||
# backend has no healthcheck, so relax to service_started so
|
||||
# consumer doesn't refuse to come up.
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_started
|
||||
kafka:
|
||||
condition: service_healthy
|
||||
schema-registry:
|
||||
condition: service_started
|
||||
|
||||
worker-shared-1:
|
||||
image: warmbly-go-dev
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deploy/docker/go-dev.Dockerfile
|
||||
environment:
|
||||
WARMBLY_CMD: worker
|
||||
WORKER_TIER: shared
|
||||
volumes:
|
||||
- .:/app
|
||||
- warmbly_gomodcache:/go/pkg/mod
|
||||
- warmbly_gocache:/root/.cache/go-build
|
||||
|
||||
volumes:
|
||||
# `name:` overrides the default `<project>_<volume>` prefixing. Every
|
||||
# worktree that brings up this override file mounts the same physical
|
||||
# volume on the host, so the Go module + build cache populated in one
|
||||
# worktree is immediately warm in every other worktree.
|
||||
warmbly_gomodcache:
|
||||
name: warmbly_gomodcache
|
||||
warmbly_gocache:
|
||||
name: warmbly_gocache
|
||||
Reference in New Issue
Block a user