The most surprising thing about Docker images is that they aren’t just files; they’re layered filesystems, and each layer is immutable. This immutability is key to how Docker caches and shares image layers, but it also has a direct impact on how you manage private registries.

Let’s see this in action. Imagine you have a simple Dockerfile:

FROM alpine:latest
RUN echo "Hello, private world!" > /hello.txt
CMD ["cat", "/hello.txt"]

Normally, docker build would pull alpine:latest from Docker Hub. But what if you want to use an image from your own private registry, say my-private-registry.com/my-base-image:v1.0?

Your Dockerfile would look like this:

FROM my-private-registry.com/my-base-image:v1.0
RUN echo "Hello from a private base!" > /app.txt
CMD ["cat", "/app.txt"]

When you run docker build -t my-app:latest ., Docker needs to fetch my-private-registry.com/my-base-image:v1.0. If this image isn’t already present locally, Docker will attempt to pull it.

To authenticate with your private registry, you first need to log in. This is typically done with the docker login command:

docker login my-private-registry.com

You’ll be prompted for your username and password (or an access token, depending on your registry’s setup). This command stores your credentials in ~/.docker/config.json.

Now, when docker build encounters FROM my-private-registry.com/my-base-image:v1.0, it consults its local configuration. If it finds valid credentials for my-private-registry.com, it uses them to authenticate the pull request to your registry. The image is then pulled, layer by layer, and stored locally.

The build process continues, executing the subsequent RUN and CMD instructions. Each instruction creates a new layer on top of the base image. The magic of Docker’s layered filesystem means that if a layer from your private registry (or any base image) already exists locally and hasn’t changed, Docker won’t re-download it, speeding up subsequent builds.

The mental model here is that your Dockerfile is a recipe. The FROM instruction is the first step: "go get this specific ingredient (the base image) from this specific pantry (the registry)." If the pantry requires a key (authentication), you provide it beforehand. Then, you perform further steps (RUN, COPY, etc.) using that ingredient, creating new, unique ingredients (layers) for your final dish (the custom image).

The docker build command implicitly handles the authentication for pulling base images if your docker login was successful. It’s not about explicitly telling docker build to use credentials; it’s about ensuring the Docker daemon has access to them.

When you push your newly built image, say my-app:latest, to your private registry using docker push my-private-registry.com/my-app:latest, the same authentication mechanism applies. Docker uses the stored credentials to upload your image layers.

A common misconception is that you need to explicitly pass credentials during the docker build command itself. This isn’t the case for pulling base images. The docker login command is the standard and secure way to provide credentials to the Docker daemon for all registry interactions, including pulls and pushes. The daemon then uses these credentials transparently.

The next hurdle you’ll likely encounter is managing image security and vulnerability scanning for images stored in your private registry.

Want structured learning?

Take the full Docker course →