#233 added COPY .git ./.git to the backend build stage. Portainer's build context does not contain .git, so every stack deploy now fails before anything else runs:
failed to deploy a stack: compose build operation failed: failed to solve:
failed to compute cache key: failed to calculate checksum of ref ...: "/.git": not found
This is the exact failure #233 set out to avoid. That issue says in as many words that a version stamp must never be the thing that stops a deploy, and the resolution code honours it — every unreadable-.git path returns unknown and warns. But the guard was put in the wrong layer: COPY fails hard at image-build time, long before any of that code executes. Graceful degradation in the application is worth nothing if the Dockerfile has already refused to build.
The assumption came from the local build context, where there is no .dockerignore and .git is therefore present. It was verified with a local docker build, which passed, and never against Portainer — the only environment that matters for this.
Immediate fix
Remove the COPY .git ./.git line. Nothing else in #233 can fail a build: writeBuildInfo is guarded, finds no .git, warns, and writes a stamp with commit: "unknown" and a real builtAt.
That leaves the build timestamp working in deployed environments, which is worth more than it sounds. Portainer already shows which commit it cloned; what it cannot tell you is whether the running container is actually that build. The timestamp closes precisely that gap — and a stale timestamp is exactly what the QA incident on 2026-08-29 would have shown.
The commit will read unknown in QA and production until it can be sourced another way.
The commit, separately
Getting the real commit into a Portainer-built image needs a different mechanism, and it is worth deciding deliberately rather than guessing again:
A GIT_COMMIT build arg fed from a stack variable — works, but has to be updated by hand every deploy, which defeats the purpose.
A version file written by Gitea Actions on merge to main — automatic, at the cost of a bot commit on every merge.
Building and pushing an image from CI and having Portainer pull it rather than build — the most correct answer and the largest change to how deployment works.
None of these should be attempted without checking them against Portainer first. The lesson from this issue is that a local docker build passing proves nothing about the environment that actually deploys.
#233 added `COPY .git ./.git` to the backend build stage. Portainer's build context does not contain `.git`, so every stack deploy now fails before anything else runs:
```
failed to deploy a stack: compose build operation failed: failed to solve:
failed to compute cache key: failed to calculate checksum of ref ...: "/.git": not found
```
**This is the exact failure #233 set out to avoid.** That issue says in as many words that a version stamp must never be the thing that stops a deploy, and the resolution code honours it — every unreadable-`.git` path returns `unknown` and warns. But the guard was put in the wrong layer: `COPY` fails hard at image-build time, long before any of that code executes. Graceful degradation in the application is worth nothing if the Dockerfile has already refused to build.
The assumption came from the local build context, where there is no `.dockerignore` and `.git` is therefore present. It was verified with a local `docker build`, which passed, and never against Portainer — the only environment that matters for this.
## Immediate fix
Remove the `COPY .git ./.git` line. Nothing else in #233 can fail a build: `writeBuildInfo` is guarded, finds no `.git`, warns, and writes a stamp with `commit: "unknown"` and a real `builtAt`.
That leaves the **build timestamp working in deployed environments**, which is worth more than it sounds. Portainer already shows which commit it cloned; what it cannot tell you is whether the running container is actually that build. The timestamp closes precisely that gap — and a stale timestamp is exactly what the QA incident on 2026-08-29 would have shown.
The commit will read `unknown` in QA and production until it can be sourced another way.
## The commit, separately
Getting the real commit into a Portainer-built image needs a different mechanism, and it is worth deciding deliberately rather than guessing again:
- A `GIT_COMMIT` build arg fed from a stack variable — works, but has to be updated by hand every deploy, which defeats the purpose.
- A version file written by Gitea Actions on merge to `main` — automatic, at the cost of a bot commit on every merge.
- Building and pushing an image from CI and having Portainer pull it rather than build — the most correct answer and the largest change to how deployment works.
None of these should be attempted without checking them against Portainer first. The lesson from this issue is that a local `docker build` passing proves nothing about the environment that actually deploys.
Follows #233.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
#233 added
COPY .git ./.gitto the backend build stage. Portainer's build context does not contain.git, so every stack deploy now fails before anything else runs:This is the exact failure #233 set out to avoid. That issue says in as many words that a version stamp must never be the thing that stops a deploy, and the resolution code honours it — every unreadable-
.gitpath returnsunknownand warns. But the guard was put in the wrong layer:COPYfails hard at image-build time, long before any of that code executes. Graceful degradation in the application is worth nothing if the Dockerfile has already refused to build.The assumption came from the local build context, where there is no
.dockerignoreand.gitis therefore present. It was verified with a localdocker build, which passed, and never against Portainer — the only environment that matters for this.Immediate fix
Remove the
COPY .git ./.gitline. Nothing else in #233 can fail a build:writeBuildInfois guarded, finds no.git, warns, and writes a stamp withcommit: "unknown"and a realbuiltAt.That leaves the build timestamp working in deployed environments, which is worth more than it sounds. Portainer already shows which commit it cloned; what it cannot tell you is whether the running container is actually that build. The timestamp closes precisely that gap — and a stale timestamp is exactly what the QA incident on 2026-08-29 would have shown.
The commit will read
unknownin QA and production until it can be sourced another way.The commit, separately
Getting the real commit into a Portainer-built image needs a different mechanism, and it is worth deciding deliberately rather than guessing again:
GIT_COMMITbuild arg fed from a stack variable — works, but has to be updated by hand every deploy, which defeats the purpose.main— automatic, at the cost of a bot commit on every merge.None of these should be attempted without checking them against Portainer first. The lesson from this issue is that a local
docker buildpassing proves nothing about the environment that actually deploys.Follows #233.