DockerDocker

failed to solve: process "/bin/sh -c ..." did not complete successfully: exit code: 1

A RUN instruction failed during the build. How to find the real error above the BuildKit summary, and debug the failing layer interactively.

easy fix6 min read

the docker error
ERROR: failed to solve: process "/bin/sh -c npm ci" did not complete successfully: exit code: 1

failed to solve: process "/bin/sh -c apt-get install -y curl" did not complete successfully: exit code: 100

ERROR: failed to solve: process "/bin/sh -c go build -o /out/server ." did not complete successfully: exit code: 2

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Show the full output rather than the collapsed summary

    docker build --progress=plain --no-cache -t myapp:1.0 .

    BuildKit collapses log lines by default, so the real error scrolls away and you are left with the summary. plain prints everything, and no-cache guarantees the failing step actually runs rather than being replayed from cache.

  2. 2

    Start a shell at the last layer that built successfully

    docker build --target builder -t debug:latest . && docker run --rm -it debug:latest sh

    Everything up to the failing RUN is already in the image, so you can run the failing command by hand and see the real output. Add a temporary stage above the failure if you do not already have one.

  3. 3

    Reproduce exactly what the build saw

    docker build --progress=plain --no-cache . 2>&1 | grep -B30 "did not complete successfully"

    The 30 lines before the summary almost always contain the actual error. The exit code itself is from the program, not from Docker, so it is worth looking up for the tool involved.

All 8 sections

This message is a summary, not the error. BuildKit is telling you which RUN failed and what it exited with. The reason is in the output above it, and BuildKit's default progress renderer collapses that output as it goes, so by the time the build fails the useful part has scrolled away.

Get the real error

docker build --progress=plain --no-cache -t myapp:1.0 .

--progress=plain prints every line instead of the collapsing TUI. --no-cache ensures the failing step actually executes rather than being replayed.

If the log is long:

docker build --progress=plain --no-cache . 2>&1 | grep -B30 "did not complete successfully"

You can also set it for the session:

export BUILDKIT_PROGRESS=plain

Read the exit code

Docker reports the program's exit code, and those are tool-specific. They narrow things down considerably:

CodeCommon meaning
1Generic failure. Read the output
2Misuse of a shell builtin, or a Go build error
100apt-get could not install
127Command not found
137Killed, usually out of memory
139Segmentation fault

127 means the command does not exist in that image, not that it failed. 137 during npm ci or a webpack build is the builder running out of memory, which is a Docker Desktop resource setting rather than a Dockerfile problem.

Debug inside the failing layer

Every layer before the failure is already built and cached, so you can get a shell in that exact state.

With a multi-stage build, target the stage before the failure:

docker build --target builder -t debug:latest .
docker run --rm -it debug:latest sh

For a single-stage Dockerfile, add a temporary stage marker just above the failing instruction:

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
FROM node:22-alpine AS debug      # temporary
# RUN npm ci                      # the failing step

Then run the command by hand and read the output properly.

BuildKit also offers this directly, if you have buildx debug:

docker buildx debug --on=error build .

That drops you into a shell at the point of failure, which is the most direct version of the same idea.

The failures that account for most of these

apt-get install with a stale package index. Exit code 100.

RUN apt-get update && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*

update and install must be in the same RUN. Split across two, the update layer is cached, and weeks later the cached index refers to package versions that no longer exist on the mirror. The install then fails with 404 Not Found. This is the single most common cause of a Dockerfile that built fine last month and fails today with nothing changed.

-y is required too, since there is no terminal to answer the prompt.

npm ci without a lockfile.

npm ci can only install packages when your package.json and package-lock.json are in sync

npm ci requires package-lock.json and refuses if it disagrees with package.json. Check .dockerignore is not excluding the lockfile, which is a surprisingly common mistake:

COPY package.json package-lock.json ./
RUN npm ci

Out of memory. Exit 137 on a webpack, tsc or Go build. Raise the memory available to the builder, or cap the tool:

ENV NODE_OPTIONS=--max-old-space-size=2048

A file that is not in the build context. .dockerignore is applied before the build starts, so a COPY of an ignored file fails, or copies nothing and the next step fails.

docker build --progress=plain . 2>&1 | grep transferring

A context of a few kilobytes when you expected megabytes means .dockerignore is excluding more than you meant.

The build works locally and fails in CI

Three usual reasons:

Architecture. CI runners are amd64; an Apple Silicon Mac is arm64. A dependency with native bindings can build on one and not the other.

Cache. Your local build replays cached layers and never runs the failing step. docker build --no-cache locally reproduces what CI sees.

Context. .dockerignore interacts with what your checkout actually contains. A file present locally but gitignored is not in CI.

docker build --no-cache --progress=plain --platform linux/amd64 .

That single command eliminates all three.

Keeping the failure visible

A RUN with a pipeline only reports the last command's exit status by default, so a failure mid-pipeline is silently ignored:

RUN curl -fsSL https://example.com/install.sh | sh

If curl fails, sh still succeeds on empty input and the layer is committed. Turn that off:

SHELL ["/bin/sh", "-euxo", "pipefail", "-c"]

-e exits on error, -u on an undefined variable, -x echoes each command, and pipefail makes a pipeline fail if any stage does. Note pipefail is not available in the default sh on Alpine, where you need bash installed or /bin/ash.

A checklist

  1. --progress=plain --no-cache. The summary is never the error.
  2. Look up the exit code. 100 is apt, 127 is not found, 137 is OOM.
  3. Build the stage before the failure and run the command by hand.
  4. apt-get update && apt-get install in one RUN, always.
  5. npm ci → confirm the lockfile is copied and not in .dockerignore.
  6. Exit 137 → give the builder more memory.
  7. Works locally, fails in CI → --no-cache --platform linux/amd64.
  8. Add SHELL ["/bin/sh", "-euxo", "pipefail", "-c"] so failures cannot hide.

Frequently Asked Questions

Where is the actual error in a "failed to solve" message?

Above it. The failed to solve: process ... did not complete successfully line is BuildKit's summary naming which instruction failed, and the real output came from the command itself further up. BuildKit's default progress renderer collapses output as steps complete, so by the time the build fails that text is gone from the screen. Rebuild with --progress=plain --no-cache to see every line, and read the thirty or so lines immediately before the summary.

Why does my Dockerfile suddenly fail when nothing changed?

Almost always a cached apt-get update layer. If update and install are in separate RUN instructions, Docker caches the package index and reuses it indefinitely. Weeks later the mirror has moved on, the cached index refers to versions that no longer exist, and the install fails with a 404 and exit code 100. Combining them into a single RUN apt-get update && apt-get install -y ... means the index is always refreshed alongside the install, which is why it is the standard idiom.

What does exit code 137 mean during a docker build?

The process was killed with SIGKILL, and during a build that almost always means it ran out of memory. Webpack, TypeScript and Go builds are the usual victims. Raise the memory available to the Docker builder, which on Docker Desktop is a setting in Resources, and consider capping the tool itself with something like ENV NODE_OPTIONS=--max-old-space-size=2048. In CI, the runner's memory is the limit, and a larger runner class is sometimes the only fix.

Why does the build succeed locally but fail in CI?

Three reasons, in order of likelihood. Cache: your local build replays the failing step from cache and never runs it, which --no-cache reproduces. Architecture: CI runners are amd64 while an Apple Silicon Mac is arm64, so a native dependency can build on one and not the other, which --platform linux/amd64 reproduces. Context: a file present locally may be gitignored and therefore absent in CI. Running docker build --no-cache --progress=plain --platform linux/amd64 . locally tests all three at once.

How do I get a shell inside a failing build step?

Every layer before the failing instruction is already built and cached, so build up to that point and run it. With a multi-stage Dockerfile use docker build --target <stage> and then docker run --rm -it <image> sh. With a single stage, temporarily add a FROM ... AS debug marker just above the failing RUN. If your Docker version has it, docker buildx debug --on=error build . does this automatically and drops you into a shell at the exact point of failure.

Reference and practice

Learn the underlying concept

Other Docker errors