Table of Contents
Overview
Docker pull or docker build failing with:
x509: certificate signed by unknown authority
means a TLS certificate in the chain can’t be verified against the trust store used by the Docker Engine or the image you’re building from. Common causes:
- Private registry with a self-signed or privately issued CA
- TLS inspection proxy (corporate MITM) re-signing traffic
- Incomplete certificate chain on the registry
- Hostname mismatch (CN/SAN) or clock skew
- Minimal base images missing CA bundles
This guide shows how to fix it for pulls and builds.
Quickstart: trust your registry’s CA on the Docker host
Use this when docker pull fails from a private registry.
Obtain the registry’s CA certificate as a PEM file (base64 text) named ca.crt. Do not use the server certificate; use the issuing CA. If intermediates are used, include the full chain.
Place ca.crt where the Docker Engine looks for per-registry CAs, then restart Docker.
- Linux (Docker Engine):
- Path: /etc/docker/certs.d/<registry-host>:<port>/ca.crt
- Example:
sudo mkdir -p /etc/docker/certs.d/registry.example.com:443
sudo cp ca.crt /etc/docker/certs.d/registry.example.com:443/ca.crt
sudo systemctl restart docker
- Docker Desktop (macOS/Linux/Windows):
- Path: ~/.docker/certs.d/<registry-host>:<port>/ca.crt (Windows: %USERPROFILE%.docker\certs.d<registry-host>_<port>\ca.crt if your shell doesn’t like colons)
- Example (macOS/Linux):
mkdir -p ~/.docker/certs.d/registry.example.com:443
cp ca.crt ~/.docker/certs.d/registry.example.com:443/ca.crt
# Restart Docker Desktop from the UI or CLI
- Test:
docker login registry.example.com
docker pull registry.example.com/my-team/app:latest
If this works, your host trust is fixed.
Minimal working example: build with a private CA inside the image
If the Dockerfile needs to fetch over HTTPS (apk/apt, curl, git) through a proxy or to a service signed by a private CA, add that CA to the image trust store.
Dockerfile (Alpine example):
# syntax=docker/dockerfile:1
FROM alpine:3.20
# Copy corporate CA into the distro trust store, then update.
COPY corp-ca.crt /usr/local/share/ca-certificates/corp-ca.crt
RUN apk add --no-cache ca-certificates \
&& update-ca-certificates \
&& wget -qO- https://internal.example.com/health || true
CMD ["sh", "-c", "ls -l /etc/ssl/certs | wc -l"]
Build and run:
# Put your PEM CA in the build context as corp-ca.crt
DOCKER_BUILDKIT=1 docker build -t ca-demo .
docker run --rm ca-demo
Tip (BuildKit secret, avoids baking CA into layers during build steps):
# syntax=docker/dockerfile:1.6
FROM alpine:3.20
RUN --mount=type=secret,id=corpca,dst=/usr/local/share/ca-certificates/corp-ca.crt \
apk add --no-cache ca-certificates && update-ca-certificates
Build with secret:
DOCKER_BUILDKIT=1 docker build \
--secret id=corpca,src=corp-ca.crt \
-t ca-demo-secret .
Note: If your application needs the CA at runtime, ensure it remains in the final image or set SSL_CERT_FILE/SSL_CERT_DIR accordingly.
Step-by-step troubleshooting checklist
- Verify the hostname and SAN
- The certificate must include the registry’s host (and port if SNI is used). Test with:
openssl s_client -connect registry.example.com:443 -servername registry.example.com -showcerts </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
- If the SANs don’t include the hostname, fix the certificate.
- Verify the chain is complete
- The server must present intermediates up to a trusted root. Validate:
openssl s_client -connect registry.example.com:443 -showcerts </dev/null
- If intermediates are missing, configure the registry with a full chain (often fullchain.pem), then restart it.
- Check clock skew
date
- If the system time is off, certificates may appear not yet valid or expired. Sync NTP.
- Corporate proxy/TLS inspection
- If a proxy re-signs TLS, install the proxy’s CA both on the Docker host and inside images that need outbound HTTPS.
- During builds, set proxies and CA:
ARG https_proxy
ENV HTTPS_PROXY=${https_proxy}
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
- Minimal base images lack CA bundles
- Debian/Ubuntu images: install ca-certificates
- Alpine: apk add ca-certificates; update-ca-certificates
- Distroless/scratch: copy a CA bundle from a builder stage.
- Test with curl/wget inside a debug container
docker run --rm -it alpine:3.20 sh -lc \
"apk add --no-cache ca-certificates curl >/dev/null; update-ca-certificates; curl -vI https://registry.example.com/v2/"
Trust store locations and commands
| Distro/base | Trust dir/file | Update command |
|---|---|---|
| Debian/Ubuntu | /usr/local/share/ca-certificates/*.crt | update-ca-certificates |
| RHEL/CentOS/Fedora | /etc/pki/ca-trust/source/anchors/*.crt | update-ca-trust extract |
| Alpine | /usr/local/share/ca-certificates/*.crt | update-ca-certificates |
| Distroless | Provide SSL_CERT_FILE to a copied bundle | n/a |
Docker build specifics
- Use BuildKit for better caching and secrets management: set DOCKER_BUILDKIT=1.
- If only apt/apk needs the CA during build, mount it as a secret to avoid bloating the image. If the app needs it at runtime, COPY it in the final stage.
- For git clones over HTTPS with a private CA, either add the CA to the system trust or configure Git:
RUN git config --system http.sslcainfo /etc/ssl/certs/ca-certificates.crt
Registry and daemon configuration
Preferred: install the CA as shown in Quickstart.
Temporary (not recommended for production): mark the registry as insecure.
daemon.json example (Linux):
{
"insecure-registries": ["registry.example.com:5000"]
}
Apply and restart:
sudo mkdir -p /etc/docker
echo '{"insecure-registries":["registry.example.com:5000"]}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
Use only when you cannot deploy a proper certificate.
Pitfalls to avoid
- Wrong directory name: include the port (e.g., registry.example.com:443)
- Wrong filename: must be ca.crt for Docker Engine trust
- Using the server cert instead of the issuing CA (use the CA or full chain)
- Forgetting to restart Docker after adding host CAs
- Fixing trust in the host but not in the image that needs outbound HTTPS
- Missing SANs in the certificate (CN alone is insufficient in modern TLS)
- Overriding SSL_CERT_FILE to a path that doesn’t exist
Performance notes
- Installing ca-certificates adds a small image size overhead (a few MB). Keep it in a single, cached layer to avoid rebuild costs.
- Use multi-stage builds: install tools and CA in a builder stage; copy only needed bundles into the final image.
- Enable BuildKit for parallelism and better cache reuse during build steps that fetch over HTTPS.
- Avoid disabling TLS verification; it can mask real issues and reduce security with negligible performance benefit.
FAQ
Q: curl works on my host, but docker pull fails. Why? A: Docker Engine has its own per-registry trust at /etc/docker/certs.d; the host OS trust store doesn’t automatically apply to the daemon.
Q: Do I need the full chain or just the root CA? A: Provide the full chain if your registry presents intermediates. Many clients require the chain to validate properly.
Q: Can I fix this with an environment variable? A: For builds, SSL_CERT_FILE or SSL_CERT_DIR can help inside the image. For docker pull, you must configure the daemon trust store.
Q: Is using insecure-registries safe? A: It disables TLS verification and is not recommended. Prefer installing the proper CA or using a valid certificate.