How Multiple FROM Statements Works In Dockerfile
What actually happens under the hood when you write multiple
FROMstatements in theDockerfile. 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-openbsdLet’s build my-alpine:v1:
docker build -t my-alpine:v1 .What do you think will — the final image have both
curlandnetcatpackage?
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.1sOnly 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 ncOk we only have netcat or nc to be specific:
/usr/bin/ncBut the real question is Why this happens:
Roughly speaking, BuildKit figures out which build stages depend on each other. Since my last
FROMstage 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-openbsdWe change two things:
- First
FROMis givenbaseas name so that later one could reference it - Second
FROMreferences or uses first stage as it’s base (referenced to bybase)
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 ncAnd yes we will see both listing:
/usr/bin/curl
/usr/bin/ncBuild 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 basetells the Buildkit only to buildbasestage 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:latestYou 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 gccYou will see nothing on stdout because which gcc could not find gcc — it doesn’t exists, we never bring that in our prod stage.
