Skip to content
How Multiple FROM Statements Works In Dockerfile

How Multiple FROM Statements Works In Dockerfile

August 16, 2026·anupam
anupam

What actually happens under the hood when you write multiple FROM statements in the Dockerfile. I was surprised — you shouldn’t be, but after reading this…


Multiple FROM in Dockerfile

The Default Behaviour

Let’s get start with a simple Dockerfile:

FROM alpine:3.22

RUN apk update && apk add curl

FROM alpine:3.22

RUN apk update && apk add netcat-openbsd

Let’s build my-alpine:v1:

docker build -t my-alpine:v1 .

What do you think will — the final image have both curl and netcat package?

If you see the output of docker build:

[+] Building 8.7s (6/6) FINISHED                                                                                                 docker:default
 => [internal] load build definition from Dockerfile                                                                                       0.0s
 => => transferring dockerfile: 147B                                                                                                       0.0s
 => [internal] load metadata for docker.io/library/alpine:3.22                                                                             2.2s
 => [internal] load .dockerignore                                                                                                          0.0s
 => => transferring context: 2B                                                                                                            0.0s
 => CACHED [stage-1 1/2] FROM docker.io/library/alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce        0.1s
 => => resolve docker.io/library/alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce                       0.0s
 => [stage-1 2/2] RUN apk update && apk add netcat-openbsd                                                                                 5.7s
 => exporting to image                                                                                                                     0.6s
 => => exporting layers                                                                                                                    0.3s
 => => exporting manifest sha256:f451f57682edcb82798288e155f06178fb05318036ed6507242029c938fb4ba5                                          0.0s
 => => exporting config sha256:489192ef75146adeda056fc5ed540faa02710e110d063fae061f048ba686be38                                            0.0s
 => => exporting attestation manifest sha256:ffec97ac00ba36cf1101e81f28033ce8c9f6cbc947bf463ed84367dc3cf059d6                              0.0s
 => => exporting manifest list sha256:8341e1edbaba177b282d318be6180e0af70d79d68f751f99f6fe2c36446403b0                                     0.0s
 => => naming to docker.io/library/my-alpine:v1                                                                                            0.0s
 => => unpacking to docker.io/library/my-alpine:v1                                                                                         0.1s

Only RUN apk update && apk add netcat-openbsd ( the second one) was executed — RUN apk update && apk add curl was never executed. This means curl should not be available in our my-apline:v1 image but nc should be.

Check it yourself:

docker container run --rm my-alpine:v1 which curl nc

Ok we only have netcat or nc to be specific:

/usr/bin/nc

But the real question is Why this happens:

Roughly speaking, BuildKit figures out which build stages depend on each other. Since my last FROM stage doesn’t reference or depend on the first stage, BuildKit simply ignores the first stage when building the final image.

How To Reference Another Stage?

Since we got the idea, let’s reference the first stage from the second one:

FROM alpine:3.22 AS base

RUN apk update && apk add curl

FROM base

RUN apk update && apk add netcat-openbsd

We change two things:

  1. First FROM is given base as name so that later one could reference it
  2. Second FROM references or uses first stage as it’s base (referenced to by base)

Note

Calling it base is not mandatory. You can name it anything you like — or anything that Docker allows you to name.

Let’s bulid my-apline image this time v2:

docker build --no-cahe -t my-alpine:v2 .

Now if you examine the output:

[+] Building 14.7s (7/7) FINISHED                                                                                                docker:default
 => [internal] load build definition from Dockerfile                                                                                       0.0s
 => => transferring dockerfile: 148B                                                                                                       0.0s
 => [internal] load metadata for docker.io/library/alpine:3.22                                                                             2.2s
 => [internal] load .dockerignore                                                                                                          0.0s
 => => transferring context: 2B                                                                                                            0.0s
 => CACHED [base 1/2] FROM docker.io/library/alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce           0.1s
 => => resolve docker.io/library/alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce                       0.1s
 => [base 2/2] RUN apk update && apk add curl                                                                                              8.7s
 => [stage-1 1/1] RUN apk update && apk add netcat-openbsd                                                                                 2.5s
 => exporting to image                                                                                                                     1.0s
 => => exporting layers                                                                                                                    0.7s
 => => exporting manifest sha256:9a0f1ec02f041d4cb3d7a1033f2cb28204f742060f5b5c572cfbf694ecb3917e                                          0.0s
 => => exporting config sha256:13fdc4ab4f5cbdf994a46e72612fbf5ad768e141f75696aa99cf5ef3e50b7f00                                            0.0s
 => => exporting attestation manifest sha256:84aaf5fbb7ad5ffe56cbd228bbb5ae191e98cd926797a0188f47d0b411c29a9f                              0.0s
 => => exporting manifest list sha256:30dfeb760a3582663a94413a9436f4497bde8df0bdb5a00cdc169ae5d1c4f3a1                                     0.0s
 => => naming to docker.io/library/my-alpine:v2                                                                                            0.0s
 => => unpacking to docker.io/library/my-alpine:v2                                                                                         0.2s
sonu-nigam@Ansible-Controller:~/docker/multiple-from-statements$

You can see both RUN statement executed which means both curl and nc should be the part of final image. Let’s not assume when we can verify:

docker container run --rm my-alpine:v2 which curl nc

And yes we will see both listing:

/usr/bin/curl
/usr/bin/nc

Build Only The Specific Stage

Suppose you only want the curl utility, you don’t have to write the seperate Dockerfile for that. It’s already there in the first stage of the current Dockerfile — you just have to tell the Docker Buildkit: I want to build only the first stage and it will do rest of the job. You can tell it so with --target option:

docker build --target base -t alpine-with-curl:latest .

The --target base tells the Buildkit only to build base stage and nothing more.

Build and run the container yourself and see only curl will be there and not nc. Do it yourself.

Reference Only The Artifacts Of Another Stage

If you reference another stage directly within FROM, the whole stage gets executed but you may not want to run the whole stage but to reference or get only the artifacts (files) build by the other stage. This is common pattern in multi stage build — you build in one stage, get file in another stage to release in production.

Take a simple hello.c:

#include<stdio.h>
int main(){
	printf("Hello I am C file!\n");
} 

And the Dockerfile alongside it:

# -----------------------
# Development Stage
# -----------------------
FROM alpine:3.22 AS dev

WORKDIR /myapp

RUN apk update && apk add gcc musl-dev

COPY hello.c .

RUN gcc hello.c -o sayhello

# -----------------------
# Production Stage
# -----------------------
FROM alpine:3.22 AS prod

WORKDIR /myapp

COPY --from=dev /myapp/sayhello .

USER nobody

CMD ["./sayhello"]

Notice the change here — you reference of bring the artifact built by dev (the sayhello program) onto the prod stage using the COPY --from=dev statement. This means you are not executing the whole dev stage, you just bring what you needed.

Let’s bulid the image and then talk:

docker build -t hello-app .

If you run the container using this hello-app image:

docker container run --rm hello-app:latest

You will get the output of /myapp/sayhello binary:

Hello I am C file!

You can verify that hello-app image doesn’t have gcc at all:

docker container run --rm hello-app:latest which gcc

You will see nothing on stdout because which gcc could not find gcc — it doesn’t exists, we never bring that in our prod stage.

Last updated on