Repository navigation
Conversation
docker-compose.yml bind-mounts the checkout over /app, which hides the image-layer chown. On Linux the mounted files keep the host user's uid/gid, so with the user pinned to 1001:1001 at build time Air cannot write ./tmp/main whenever the host ids differ, and hot reload stops. Dockerfile.dev now takes DEV_UID/DEV_GID build args (default 1000:1000, the usual first Linux account) and docker-compose.yml passes them from the environment or .env. A gid that already names a group in the base image (macOS's 20 is `dialout` in Alpine) is reused rather than re-created. The old `|| true` is gone: it existed because /root/go does not exist in the image (so chown always failed), but it also hid addgroup/adduser failures; a taken uid now fails the build instead of leaving USER pointing at a missing account. The /root/go and /root/.cache chown targets are dropped since /root is 0700 and the dev user could never reach them. macOS is unchanged: Docker Desktop remaps bind-mount ownership. Refs LeanerCloud#158 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…compile Even with a writable /app, Air's `go build` in the dev container failed before compiling: `go mod download` runs before go.work is copied in, so it fills the root-owned module cache for the root module only, and once go.work is present at /app the build switches to workspace mode, which asks for modules that download did not fetch (github.com/envoyproxy/go-control-plane, github.com/golang-jwt/jwt). The non-root dev user cannot write the cache (nor the go_modules volume seeded from it), so every build ended in `permission denied` and hot reload never produced a binary, on Linux and macOS alike. Set GOWORK=off in Dockerfile.dev. The server builds offline from the baked cache in single-module mode. tests/e2e is a separate stdlib-only module and is still usable from its own directory. Closes LeanerCloud#158 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
Two separate problems both stop Air hot reload in the
appdev container. Both are already onmain.docker-compose.ymlbind-mounts the checkout over/app, which hides the image-layerchown. On Linux the mounted files keep the host user's uid/gid.Dockerfile.devpinned the dev user to1001:1001, so whenever the host user has a different uid, Air dies at startup withfailed to mkdir, error: mkdir /app/tmp: permission denied. The old RUN line also ended in|| true. That was needed because/root/godoes not exist in the image, so thechownfailed on every build, but it also hid anyaddgroup/adduserfailure./app(host uid == container uid), Air'sgo buildfailed before compiling.go mod downloadruns beforego.workis copied in, so it fills the module cache for the root module only. Oncego.workis present at/app,go buildswitches to workspace mode and asks for modules that download did not fetch (github.com/envoyproxy/go-control-plane,github.com/golang-jwt/jwt). The cache is root-owned, and so is thego_modulesvolume seeded from it, so the non-root user gets 29 ×permission deniedand Air never producestmp/main. The issue's acceptance criterion is that reload succeeds on a source edit, and that cannot pass without this fix too.Fix
Dockerfile.dev: newDEV_UID/DEV_GIDbuild args, both defaulting to1000(the usual first Linux account). They driveaddgroup/adduser, the/appchown and the numericUSER. If the gid already names a group in the base image, that group is reused (macOS's20isdialoutin Alpine).|| trueis gone, so a taken uid now fails the build. The/root/goand/root/.cachechown targets are dropped:/rootis0700, so the dev user could never reach them. A comment next toUSERrecords the bind-mount constraint, as the issue asks.docker-compose.yml:build.argspassesDEV_UID: ${DEV_UID:-1000}/DEV_GID: ${DEV_GID:-1000}from the shell or.env. A comment explains why.Dockerfile.dev:ENV GOWORK=off, so the server builds offline from the baked module cache in single-module mode.tests/e2eis a separate stdlib-only module and still works from its own directory.docs/DEVELOPMENT.md: new "Linux: match your uid" subsection under Hot Reload: when to set the ids, the one-liner that writes them to.env, the rebuild command, why macOS needs nothing, and a note onGOWORK=off.macOS default behavior is unchanged: Docker Desktop remaps bind-mount ownership, so the default
1000:1000works for any account.Why build args and not the issue's named-volume alternative for
/app/tmp: a named volume freezes its ownership when it is first created, so it goes stale as soon as the image's uid changes. It also hidestmp/mainfrom the host, and it does nothing for other writes into the checkout (docker compose exec app go mod tidy,migrate create ...). With matched ids, those writes succeed and the files are owned by the developer. A plainuser:override was rejected too: with a uid that has no passwd entry,HOMEbecomes/, and Go's build cache ($HOME/.cache/go-build) is not writable.Closes #158
Verification
All evidence comes from local Docker runs on a Linux VM (Docker 29.1.3, legacy builder). The host user is
uid=1001 gid=1002, so the pre-fix image (1001:1001) already mismatches the host gid. To reproduce a host whose uid differs from the container's, one throwaway copy of the source was chowned to1000:1000.docker composeis not installed on this machine, so compose was emulated withdocker build --build-arg+docker run -v <copy>:/app. The compose file itself was only checked as parsed YAML (build.args={'DEV_UID': '${DEV_UID:-1000}', 'DEV_GID': '${DEV_GID:-1000}'}), not interpolated by compose. No real database was started, so the server exits atDB_HOST must be setafter each successful build. That is expected and shows Air reachedrunning....Images:
Fail-before (pre-fix image):
1000:1000, container1001:1001failed to mkdir, error: mkdir /app/tmp: permission denied, container exits 11001:1002); uid matches, so/appis writablebuilding...→ 29 ×mkdir /go/pkg/mod/cache/download/...: permission denied→failed to build;/app/tmp/main: not found;tmp/build-errors.logowned1001:1001(not the host gid)go build -n -o /tmp/x ./cmd/serverin workspace mode vsGOWORK=off GOPROXY=offPass-after (fixed images):
platform158-dev-after(built with host ids) on a host-owned checkoutbuilding...→running...; hostls -ln tmp/main→1001 1002echo '// marker' >> cmd/server/main.godocker exec ... tail -1 /app/cmd/server/main.goshows the marker; Air logscmd/server/main.go has changed→building...→running...;tmp/mainmtime advanced (07:19:24 → 07:19:44)docker exec ... touch /app/written-by-container1001 1002on the hostplatform158-dev-default(no build args,1000:1000) on the1000:1000checkouttmp/mainowned1000 1000go_modulesvolume (fresh named volume at/go/pkg/mod, root-owned)GOWORK=off GOPROXY=off go build -nexit 0; workspace mode exit 1 with 29 × permission deniedLinters: no Go code or shell scripts changed, so Go build/vet/golangci-lint/gocyclo/shellcheck do not apply. hadolint is not installed on this machine (CI runs it through the pinned pre-commit image) and was not run. markdownlint is not installed either. All images, containers and the volume created for verification were removed.
Combined-tree verification. This branch was tested together with the other queued branches on a local, never-pushed tree:
queue/platform@936996ad= origin/maina14dc8f2plus these heads, merged in this order:fix/issue-72andfix/issue-312conflict on one line ininternal/api/handler_dashboard.go(both change thecalculateCommitmentMetricscall). The tree used #72's side with #312's validatedprovidervariable; whichever PR merges second takes that same resolution.Linux, Go 1.26.9,
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers:936996ad:go build ./...,go vet ./...,go vet -tags integration ./...and the fullgo test -race -short -count=1 ./....936996ad: golangci-lint v2.10.1 found 0 issues on every changed package, with and without--build-tags integration.tests/e2eis a separate module, so it was vetted under its owne2etag.gocyclo -over 10is silent on the changed non-test files.-tags integration -count=1, 60-minute local timeout because the VM was shared):936996ad, these pass:internal/server,internal/credentials,internal/scheduler,internal/database/postgres/migrations,internal/purchaseandinternal/api.internal/configpasses exceptTestPostgresStoreDB_ResolveStaleRIExchange_ValidatesCloudCheck, which fails the same way on unmodified main (fixed by fix(config): compare resolve checked_at at the database's microsecond precision #1252).26795376, same branches at earlier heads):internal/analytics,internal/auth,internal/commitmentopts,internal/databaseandcmd/lambda/clear-rate-limitall pass. The later fixes don't touch those packages.httpToLambdaRequestsignature change;scripts/setup-git-secrets.shpatterns in a fresh repo with an isolated HOME, found nothing in the changed files, and a planted fake key was flagged.scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.Out of scope / notes
go_modulesvolumes are root-owned and the dev user cannot write to them. WithGOWORK=offthe build needs nothing beyond the baked cache, but adding a new dependency inside the container (go get) still cannot write the cache. That was already true before this PR;docker compose down -vplus a rebuild reseeds the volume.GOWORK=offdoes not depend on the host OS.Dockerfile.devstill downloads the upstreammigratev4.17.0 tarball (audit finding indocs/audits/codebase-audit-2026-09-02.md). Unrelated, left alone.nginxservice only bind-mounts read-only, so it is unaffected. No frontend change needed.Maintainer: please mirror the issue's triage labels onto this PR:
impact/internal,type/chore,triaged,priority/p3,severity/low,urgency/eventually,effort/s.🤖 Generated with Claude Code