2026.11 / image platforms 084
Coolify exec format error: fix an AMD64/ARM64 image mismatch
A Coolify deployment may build successfully, replace the container, and then enter a restart loop with an error such as exec /app/server: exec format error or exec /usr/local/bin/docker-entrypoint.sh: exec format error. The usual explanation is a CPU architecture mismatch: an ARM64 executable is being started on an AMD64 server, or the reverse.
Do not assume architecture is the only possible cause. A script without a valid shebang, Windows CRLF characters in that shebang, or a corrupted executable can produce a similar startup failure. The path named in the error tells you where to begin.
Prove four identities separately: the deployment server, the image selected by Docker, the configured entrypoint, and any compiled binary the entrypoint launches.
What the error actually means
Linux asked the kernel to execute a file, but the file was not in a format that the host could start. For a compiled application, that often means the binary targets the wrong instruction set. For a shell script, it can mean the interpreter declaration is missing or malformed.
| Error path or evidence | Likely boundary | First check |
|---|---|---|
/app/server | Compiled application binary | Run file against the binary in the image |
/usr/local/bin/docker-entrypoint.sh | Base image or script format | Inspect image platform, shebang, and line endings |
| Warning says requested platform differs from host | Image manifest selection | Compare host and image OS/architecture |
| Build passes on Apple Silicon, VPS fails | Single-platform local build | Inspect the pushed registry manifest |
| Multi-stage image has a native final base but still fails | Copied build artifact | Check target arguments used by the compiler |
1. Preserve the failed deployment evidence
Before rebuilding, record the Coolify deployment identifier, source commit, image reference or digest, deployment server, build server if one is enabled, container state, restart count, and the first startup error. Use placeholders when sharing diagnostics publicly; host addresses and registry credentials are not needed.
docker inspect APP_CONTAINER \
--format '{{.Image}} {{.Path}} {{json .Args}} {{.RestartCount}}'
docker logs --tail 100 APP_CONTAINER
If the container restarts too quickly, inspect the image directly rather than repeatedly restarting production. Do not replace the entrypoint with a sleeping shell and call the deployment fixed; that only hides the application failure.
2. Prove the deployment server architecture
Run the check on the server that actually starts the application container. A separate Coolify build server can have a different architecture, so checking only the machine that built the image is insufficient.
uname -m
docker info --format '{{.OSType}}/{{.Architecture}}'
docker version --format \
'client={{.Client.Os}}/{{.Client.Arch}} server={{.Server.Os}}/{{.Server.Arch}}'
Common mappings are x86_64 to Docker’s amd64, and aarch64 to arm64. Trust the Docker server value for container scheduling. The laptop running a CLI or browser is not the deployment host.
3. Inspect the image Docker is really running
A tag is mutable and does not prove which platform or bytes are present. Start with the immutable image ID used by the failed container, then inspect its metadata:
IMAGE_ID=$(docker inspect APP_CONTAINER --format '{{.Image}}')
docker image inspect "$IMAGE_ID" \
--format 'id={{.Id}} os={{.Os}} arch={{.Architecture}} entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}}'
If the app deploys a registry tag, inspect the remote manifest as well:
docker buildx imagetools inspect registry.example/app:RELEASE_TAG
A multi-platform tag should expose a manifest list containing the platforms you intend to deploy, such as linux/amd64 and linux/arm64. When Docker pulls that tag, it selects the matching variant for the host. A single-platform tag has no alternative to select.
Compare the image digest in the registry with the container’s image data. Reusing a tag can leave different machines with different cached content. Immutable release tags or digests make this comparison unambiguous.
4. Determine whether the entrypoint or the app binary is wrong
The path in the error matters. If the image’s own entrypoint fails before your code starts, inspect that file. If a shell script starts successfully and then reports the error for /app/server, the copied application binary is the suspect.
Use a compatible diagnostic container or export the image filesystem. The following example overrides the entrypoint only for a disposable inspection run:
docker run --rm --entrypoint /bin/sh IMAGE_REFERENCE -c '
uname -m
file /app/server 2>/dev/null || true
sed -n "1p" /usr/local/bin/docker-entrypoint.sh 2>/dev/null || true
'
This works only if the image contains the requested shell and can run on the host. For a distroless or wrong-platform image, create a container without starting it, export its filesystem, and examine the relevant file from a separate trusted tool environment.
For scripts, verify that the first line names an interpreter that exists in the image, for example #!/bin/sh, and that it has Unix line endings. A shebang ending in an invisible carriage return can make Linux search for a nonexistent interpreter. Also check that the file is executable; a missing execute bit normally produces “permission denied”, which is a different signal.
5. Find where the wrong platform entered the release
Architecture drift usually appears at one of these boundaries:
- A local image was built on Apple Silicon and pushed as a single ARM64 tag. The AMD64 deployment server later pulls exactly that image.
- A CI workflow hard-codes one platform. Its build command or action configuration publishes only
linux/arm64or onlylinux/amd64. - A remote Coolify build server differs from the deployment server. Coolify’s documentation recommends matching their CPU architectures unless the build deliberately produces the deployment target.
- A multi-stage build cross-compiles incorrectly. The final base image matches the host, but a binary copied from the builder was compiled for the builder rather than the target.
- A Compose service forces
platform:. The override may select a variant that the production host cannot execute natively. - An imported vendor binary has a fixed architecture. The base image can be correct while a downloaded executable is not.
Inspect the exact Git commit and deployment configuration. If the source files are not the ones expected by the Dockerfile, solve that separately with the Coolify Docker build-context checklist.
6. Choose a single-platform or multi-platform repair
If the application will run only on one AMD64 VPS, the simplest repair is often to build explicitly for linux/amd64. Replace the target with linux/arm64 for an ARM deployment server:
docker buildx build \
--platform linux/amd64 \
--tag registry.example/app:RELEASE_TAG \
--push .
If the same image tag must work on both architectures, publish both variants under one manifest list:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag registry.example/app:RELEASE_TAG \
--push .
Do not add platform: linux/amd64 to Compose merely to silence image selection on an ARM server. That requires correctly installed emulation and carries performance and compatibility costs. Native images are preferable for normal production workloads.
7. Make compiled artifacts target-aware
A multi-platform base image does not automatically make a locally downloaded or compiled binary multi-platform. BuildKit exposes BUILDPLATFORM, TARGETPLATFORM, TARGETOS, and TARGETARCH so a multi-stage build can compile for the selected final target.
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="$TARGETARCH" \
go build -o /out/server ./cmd/server
FROM alpine
COPY --from=build /out/server /usr/local/bin/server
ENTRYPOINT ["/usr/local/bin/server"]
The builder runs natively, while the compiler emits the requested target. Other languages and native dependencies have their own cross-compilation requirements. If a build needs C libraries or CGO, use a target-compatible toolchain and verify the resulting binary instead of assuming environment variables are enough.
For downloaded tools, construct and verify architecture-specific download URLs deliberately. Do not derive them from untrusted runtime input, and verify publisher checksums or signatures before putting the binary into the image.
8. Align Coolify’s build and deployment boundary
By default, Coolify can build and deploy on the same server. With a dedicated build server, the completed image is pushed to a registry and pulled by the deployment server. That boundary makes the registry manifest part of the release contract.
For a straightforward single-platform deployment, keep build and deployment servers on the same CPU architecture. If they differ intentionally, make the Docker build target explicit and ensure the builder has a supported native, cross-compilation, or emulation strategy. Record the expected platform in CI rather than relying on whichever runner happens to execute the job.
After correcting the Dockerfile or workflow, use a new immutable release tag. Confirm the tag’s remote manifest before telling Coolify to deploy it. If the deployment also appears to use an old commit or tag, use the Coolify stale-deployment checklist rather than repeatedly pressing redeploy.
9. Test both the image and the actual Coolify release
Before production, run the exact platform variant on compatible infrastructure and assert application behaviour:
docker pull --platform linux/amd64 registry.example/app:RELEASE_TAG
docker image inspect registry.example/app:RELEASE_TAG \
--format '{{.Os}}/{{.Architecture}} {{.Id}}'
docker run --rm --platform linux/amd64 \
registry.example/app:RELEASE_TAG --version
Then trigger one Coolify deployment and poll it to a terminal state. Verify that the replacement container has a new immutable ID, the expected image digest, a stable start time, no restart loop, and a healthy status. Request a real application route and assert unique release content—not only HTTP 200.
If using a multi-platform tag, test each advertised variant in CI or on a native runner. A manifest list can be published even when one constituent image is broken.
Fixes that create more uncertainty
- Forcing a platform without checking the host: this can exchange a clear mismatch for slow or unavailable emulation.
- Reusing
latest: mutable tags make cache and digest comparisons harder during an incident. - Rebuilding only the base image: a copied application binary can still target the wrong architecture.
- Installing QEMU directly on production as the first response: build the native variant unless emulation is a reviewed requirement.
- Replacing the entrypoint with
sleep: a running container is not a working application. - Editing the live container: the next Coolify recreation discards the change and preserves no trustworthy release history.
Compact recovery order
- Capture the exact container, image ID, error path, source commit, and deployment record.
- Prove the Docker server OS and CPU architecture.
- Inspect the local image metadata and remote registry manifest.
- Identify whether the failed file is an entrypoint script or compiled binary.
- Trace the mismatch to local build, CI, Coolify build server, Compose override, or Dockerfile stage.
- Publish the correct single-platform image or a tested multi-platform manifest under a new tag.
- Deploy once, then verify digest, restart count, health, logs, and unique application content.
Authoritative references: Docker’s official multi-platform build guide explains manifest lists, host compatibility, emulation, native nodes, and cross-compilation; the docker image inspect reference documents image and platform inspection. Coolify’s build-server guide describes the registry hand-off and recommends matching build and deployment server architecture, while its Dockerfile build-pack guide documents source and build configuration.