← Back home

2026.11 / Docker contexts 083

Coolify Docker COPY failed: fix “file not found” in the build context

A Coolify deployment can fail at a Dockerfile line such as COPY package.json ./, COPY dist/ /usr/share/nginx/html/, or COPY apps/web . even though the file exists on your laptop. BuildKit may report “failed to calculate checksum”, “not found”, or “failed to compute cache key”.

Those messages usually mean that the source does not exist inside the build context presented to Docker. The file may be uncommitted, outside Coolify’s base directory, removed by .dockerignore, named with different letter case, or expected to be generated in a stage where it was never created.

COPY cannot see your workstation or arbitrary paths around the Dockerfile. It can see only the selected context, prior build stages, named contexts, and images.

Read the failing source as Docker reads it

In a normal instruction, the source path is relative to the root of the build context—not relative to the Dockerfile’s own directory. Coolify’s Dockerfile build pack combines its Base Directory and Dockerfile Location settings to select these boundaries. A valid Dockerfile path does not prove that its sibling files are in the context.

EvidenceLikely causeRepair boundary
File exists locally but not in git showUntracked, ignored, or not pushedGit commit
File is in Git but above the Coolify base directoryContext is too narrowCoolify base directory or repository layout
Build log transfers an unexpectedly tiny context.dockerignore excluded required inputIgnore rules
Works on a case-insensitive laptop onlyFilename case differsCommitted path and Dockerfile spelling
COPY --from=build failsWrong stage, source path, or missing build outputMulti-stage build
Correct commit still builds the wrong directoryCoolify source settings do not match the repositoryApplication configuration

1. Capture the exact failed deployment

Start with the deployment record, not a broad cache purge. Record the checked-out commit, branch, repository, build pack, base directory, Dockerfile location, failing Dockerfile line, and full error. Keep the deployment identifier as returned; it need not look like a conventional UUID.

The source operand matters. These instructions have different namespaces:

COPY package.json ./
COPY --from=build /app/dist/ /usr/share/nginx/html/
COPY --from=nginx:alpine /etc/nginx/nginx.conf /tmp/default.conf

The first reads from the build context, the second from a named earlier stage, and the third from an image. Do not troubleshoot all three as if they were missing Git files.

2. Prove the file is in the deployed Git commit

A local editor tab is not deployment evidence. Ask Git whether the exact commit contains the exact path and case:

git show --stat --oneline EXPECTED_COMMIT

git cat-file -e EXPECTED_COMMIT:package.json
git cat-file -e EXPECTED_COMMIT:apps/web/package.json

git ls-tree -r --name-only EXPECTED_COMMIT

Use the path relevant to your project. If git cat-file fails, check git status --short, .gitignore, the pushed branch, submodules, and large-file handling. Commit a required source file; do not generate an empty placeholder in production merely to make COPY pass.

Also verify filename case. Linux treats Package.json, package.json, and package.JSON as different names. If a case-only rename is not being recorded on a case-insensitive workstation, perform an explicit two-step git mv and inspect the staged diff.

3. Draw the context boundary

Suppose a monorepo looks like this:

repository/
├── package-lock.json
├── packages/shared/
└── apps/web/
    ├── Dockerfile
    ├── package.json
    └── src/

If Coolify’s base directory is /apps/web, Docker can see package.json and src, but it cannot copy the repository-root lockfile or packages/shared. Moving the Dockerfile does not widen that context. Likewise, COPY ../package-lock.json ./ cannot be used to escape it.

There are two sound designs:

Choose based on the project’s real dependency boundary. Widening the context to the repository root is not automatically wrong, but pair it with a reviewed .dockerignore so unrelated artifacts and local credentials are not transmitted to the builder.

4. Reproduce Coolify’s context locally

Run the same Dockerfile and context combination. The final argument is the context; -f selects the Dockerfile:

# Repository-root context, nested Dockerfile
docker build --progress=plain \
  -f apps/web/Dockerfile \
  -t web-context-check .

# Narrow app-only context
docker build --progress=plain \
  -f apps/web/Dockerfile \
  -t web-context-check apps/web

These two commands can use the same Dockerfile while exposing different files to COPY. Reproduce from a clean clone or detached worktree of the expected commit so untracked local files cannot create a false pass.

A no-cache build can help reveal stage execution, but it cannot make a missing source enter the context. Fix the source boundary first.

5. Audit .dockerignore in the context root

Docker removes matching files before sending the context. A required file excluded by .dockerignore is therefore indistinguishable from an absent file to COPY. Check the ignore file at the context root and any Dockerfile-specific ignore file.

# Risky when the image needs prebuilt output
dist

# Risky when a later COPY expects a nested lockfile
**/package-lock.json

# Broad exclusion with a deliberate exception
*
!package.json
!package-lock.json
!src/**
!public/**

Exception order matters: the last matching rule determines whether a path is included. If a directory itself is excluded, adding only a child exception may not make the directory traversable as intended. Prefer a short allowlist only when you can test every required input.

Do keep genuinely unnecessary or sensitive local material out of the context: version-control metadata where unneeded, dependency directories, test output, logs, local environment files, editor state, and deployment credentials. Never “fix” a build by removing all ignore rules and sending the whole workstation directory.

6. Separate source files from generated artifacts

A common failing pattern expects a host-built dist directory:

FROM nginx:alpine
COPY dist/ /usr/share/nginx/html/

This works locally only after a developer has run the frontend build. A clean Coolify checkout has no generated directory unless it is committed or created during the image build. For most applications, generate it in a builder stage and copy from that stage:

FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist/ /usr/share/nginx/html/

Now the final COPY reads from the filesystem produced by the build stage. If it fails, inspect the build command and output directory. Frameworks may write build, out, .output/public, or another configured path rather than dist.

7. Check multi-stage names and paths

For COPY --from=build, verify that:

Add a temporary non-secret listing immediately after the build command if the output path is uncertain. List only application directories, not the entire filesystem or environment. Remove the diagnostic once the Dockerfile contract is clear.

8. Handle monorepo manifests deliberately

Package-manager workspaces often need root manifests plus selected workspace manifests before dependency installation. A Dockerfile under apps/web can still use a repository-root context:

FROM node:22-alpine AS build
WORKDIR /workspace
COPY package.json package-lock.json ./
COPY apps/web/package.json apps/web/package.json
COPY packages/shared/package.json packages/shared/package.json
RUN npm ci
COPY apps/web apps/web
COPY packages/shared packages/shared
RUN npm --workspace apps/web run build

The exact files depend on the workspace manager, but the principle is stable: context, manifests, install root, and source layout must describe the same graph. If dependency installation then fails because the committed lockfile is inconsistent, follow the separate Coolify npm ci lockfile recovery guide rather than weakening the install.

9. Commit, deploy once, and prove the live release

After changing the repository layout, Dockerfile, ignore rules, or Coolify settings, run the production Docker build locally from a clean checkout. Inspect the diff for accidental environment files or credentials, then push the complete change.

git diff --check
git status --short
git diff -- Dockerfile .dockerignore

docker build --progress=plain -t app-context-check .

Trigger one Coolify deployment and poll it to a terminal status. In the deployment log, confirm the expected commit, base directory, Dockerfile, successful COPY step, completed build, and healthy replacement container. Finally, request a real route and assert unique content from the new release. An HTTP 200 alone can be a stale page, proxy fallback, or single-page-app fallback.

Fixes that usually make the problem worse

Compact recovery order

  1. Capture the failed commit, branch, build pack, base directory, Dockerfile location, instruction, and complete error.
  2. Identify whether the source belongs to the context, an earlier stage, or an image.
  3. Prove the exact path and case exist in the deployed commit.
  4. Map Coolify’s base directory to Docker’s context root.
  5. Reproduce the exact Dockerfile/context pair from a clean checkout.
  6. Audit root and Dockerfile-specific ignore rules.
  7. Generate build artifacts in a builder stage where appropriate.
  8. Test the image, review the diff, deploy once, and verify unique live content.

Authoritative references: Docker’s official build-context guide defines which files COPY can access and how .dockerignore removes them; the Dockerfile COPY reference documents context and --from sources; Docker’s CopyIgnoredFile check covers excluded inputs. Coolify’s official Dockerfile build-pack guide explains how Base Directory and Dockerfile Location establish the build.