Why does bind-mounting source into a Docker container not reload a compiled service such as a .NET app?
answer
- The mount changes inputs, not the running program
- Ask what the process is actually executing
- The slim shipping image has no compiler
- Something inside must rebuild and restart
- Dev target plus a watch tool
basics
~20 sThe container runs a compiled artefact produced at build time, and a bind mount only changes source on disk. Nothing recompiles it: a runtime-only image has no compiler and nothing is watching. You need a dev stage with the SDK plus a watch process.
solid answer
~50 sA bind mount is a file-visibility mechanism, not a build system. For an interpreted app the mounted file *is* what gets executed on the next request or reload, so the loop appears to work by itself. For a compiled service the running process is executing output produced during `docker build` — the published assemblies — and editing a source file underneath it changes nothing. Two pieces are missing. First, a **toolchain**: the slim runtime image that ships deliberately has no compiler, so you build a separate dev target from the SDK-bearing stage and run *that* locally. Second, a **watcher**: a process inside the container that notices the change, recompiles and restarts the app (`dotnet watch`, or the equivalent for the language). Without both, the honest inner loop for a compiled service is `docker build` plus recreate — and that is sometimes the right answer.
code
bash · 2 linesdocker build --target dev -t recon:dev .
docker run --rm -it -p 8080:8080 -v "$PWD":/src -v /src/bin -v /src/obj recon:devgo deeper
Know the distinction that causes the confusion: an interpreted app runs the file you edited, a compiled one runs output made earlier by a compiler. A mount makes files visible; it never compiles anything.
Explain what has to be in the container for the loop to work — the SDK, because the shipping image has none, and a watch process that recompiles and restarts — and how a dev build target gives you that without a second Dockerfile.
Show the per-change judgement: which edits a watcher handles and which are genuinely rebuilds, roughly what each cycle costs, and how you keep the dev target from drifting away from the image that actually ships.
Own the tradeoff between loop speed and fidelity. Decide when a team maintains a dev target at all, how parity with the shipping image is enforced before merge, and what local iteration is allowed to assume about the production image.
## Why interpreted and compiled apps behave differently The inner-loop trick — mount the working copy over the app directory — hides an assumption: that the file on disk is the thing that runs. For an interpreted runtime that is true; the next request, or the framework's reload, reads the edited file and you see the change. For a compiled service it is false. The container's PID 1 is executing a binary or an assembly set that `docker build` produced, and your edited source file is an *input* to a compiler that is not running. The bind mount did its job perfectly and nothing happened. Make this concrete with a .NET service that runs a payments reconciliation batch. The image is built in two stages: an SDK stage restores and publishes, and a runtime stage copies the published output in. The runtime image is the one that ships, and it is deliberately minimal — no SDK, no compiler, often no shell. Bind-mount the repository over the app directory in *that* image and you get the worst of both worlds: the published assemblies are shadowed by source the container cannot compile, and the app may not even start. ## The two missing pieces **A stage that still has the toolchain.** The local container must be built from the stage that carries the SDK and the dev-time dependencies, not the slim stage that ships. `docker build --target dev` gives you that image without duplicating the Dockerfile, and the shipping stage is untouched. **A process that watches and rebuilds.** Inside that dev container the command is not the published binary; it is the language's watch tool, run against the mounted source: ```dockerfile FROM mcr.microsoft.com/dotnet/sdk:8.0 AS dev WORKDIR /src COPY *.csproj ./ RUN dotnet restore COPY . . CMD ["dotnet", "watch", "run", "--urls", "http://0.0.0.0:8080"] ``` Now the loop is: save a file on the host, the watcher inside the container sees the change, recompiles incrementally and restarts the app. The compile happens *in the container*, with the container's SDK, against the container's platform — which is the whole point, and why a host-built `bin/`/`obj/` tree should be kept out of the way rather than mounted in. ## Rebuild versus reload — the actual economics Even with a watcher, some changes still need a full `docker build`: * a new or upgraded package (the restore step is a build step); * any Dockerfile change — base image, env, entrypoint; * a native dependency or OS package the app links against; * anything you must prove works in the *shipping* image rather than the dev one. So the decision is per-change, not per-project. Compare the costs honestly. A watch cycle is an incremental compile plus process restart; for this reconciliation service the process alone takes about 6 seconds to come up before it accepts work, so a reload is roughly that 6 seconds plus a second or two of compilation. A rebuild adds the build-context transfer, the restore step whenever the manifest layer is invalidated, the publish, the image commit and container recreation — usually tens of seconds even with a warm cache, and much worse cold. The reload wins for source edits by a wide margin, which is exactly why the dev target exists; the rebuild wins whenever the change is one the watcher cannot express, and pretending otherwise produces the confusing state where the container is running something no file on disk describes. ## The parity risk nobody mentions until it bites The dev target is not the artefact. It has a different base image, extra tools, different environment and a different entrypoint, and it runs code compiled on the fly rather than the published output. A change can work all afternoon in the dev container and fail in the shipping image — trimmed assemblies, a missing culture or timezone database, a file the publish step never copied, a non-root user without write access to a path the code assumes. The discipline is to build and run the real target before merging, and to keep both stages in one Dockerfile so the shared parts cannot drift apart. ## What good answers include Name the mechanism (the running process executes build output, not source), name the two requirements (toolchain in the image, watcher in the container), and be explicit that the loop has limits — dependency and Dockerfile changes are rebuilds, and the shipping image still has to be exercised before the change leaves your machine.
- Why keep the host's build-output directories out of the container instead of letting them be mounted in?Because they are platform- and SDK-specific. Output compiled by the host toolchain can confuse an incremental build inside the container, and the two builds fight over the same files. Mounting an empty volume over each output directory gives the container its own, and stops the container writing build artefacts back into your working copy where they end up in diffs or owned by the wrong user.
- When would you skip the watch loop entirely and just rebuild the image for every change?When changes are infrequent or structural — dependency upgrades, Dockerfile edits, anything that has to be verified in the shipping image — and when the build is fast enough that a dev target is not worth maintaining. A rarely touched service with a cached build measured in seconds does not need a second image target and the drift risk that comes with it.
- What is the risk of doing all your local work in the dev target?It is not what ships. Different base image, extra tooling, a different entrypoint and different compiled output mean a change can pass locally and fail in the runtime image — a trimmed dependency, a missing locale or timezone file, a path the non-root user cannot write. Build and run the shipping target before merging rather than trusting the dev container alone.
saying these in an interview costs you the question
- Expects mounted source to run without any compile step
- Thinks a runtime-only image can build the code
- Believes docker restart recompiles the application
- Mounts host build output into the container unchanged
- Never runs the shipping image before merging
- Claims rebuilds are always slower than a reload