Skip to content

chore(dev): dev container uid cannot match host, breaking Air hot reload on Linux bind mounts #158

Description

@cristim

Surfaced by CodeRabbit on PR LeanerCloud/cloud-commitments-cli#1700. The finding is real, but it predates that PR, so it is filed separately rather than folded in.

What

docker-compose.yml bind-mounts the checkout at .:/app for the app (dev) service, which hides the image-layer ownership Dockerfile.dev sets with chown. .air.toml has root = ".", tmp_dir = "tmp", bin = "./tmp/main", so Air must write /app/tmp/main inside that bind mount on every reload.

On Linux, the mounted files carry the host user's uid/gid. The container user's uid is fixed at image build time and has no relationship to whoever runs docker compose up. When they do not match, /app is not writable, Air cannot build, and the hot-reload loop stops silently.

Why this is pre-existing, not new

It is tempting to attribute this to PR LeanerCloud/cloud-commitments-cli#1700 pinning the dev user to 1001:1001. It is not:

Name-form vs numeric-form USER is irrelevant here: USER devuser resolves to the same fixed uid baked at build time. Neither form adapts to the host.

Not reproducible on macOS

On Docker Desktop for macOS the bind mount is ownership-remapped, so the container sees /app owned by whatever uid it runs as and the write always succeeds. Confirmed both ways:

original image (uid 100):  ls -ldn /app -> 100  101   ... /app WRITABLE, tmp write OK
LeanerCloud/cloud-commitments-cli#1700 image  (uid 1001):   ls -ldn /app -> 1001 1001  ... /app WRITABLE, tmp write OK

So a macOS developer cannot observe this, and a macOS test cannot be used as evidence either way. It needs a Linux host (or a Linux CI runner) to reproduce.

Fix direction

CodeRabbit's suggestion is the standard one and is sound:

  • Accept DEV_UID / DEV_GID build args in Dockerfile.dev, use them in addgroup/adduser, and have docker-compose.yml pass the host's ids (user: "${DEV_UID:-1000}:${DEV_GID:-1000}" or via build.args fed from an .env).
  • Alternatively, mount Air's output on a named volume so /app/tmp is container-owned and never touches host ownership. This is the smaller change and avoids per-developer configuration, at the cost of the built binary not being visible on the host.

The named-volume option is probably the better default: it needs no .env and no per-machine setup, and nothing outside the container consumes tmp/main.

Acceptance

  • On a Linux host with a checkout owned by a uid other than the container's, docker compose up app reloads successfully on a source edit.
  • Whichever mechanism is chosen is documented next to it, so the next person changing USER in Dockerfile.dev knows the bind mount is a constraint.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions