World map

 

BM: Baca dalam Bahasa Melayu

World D2: Recipes

Multi-stage builds

You will learn: to cook in one stage and pack only the finished dish.

Checked with Docker Engine 29 on 31 Oct 2026

The problem

Your Kedai Kancil page is built with Node. Your one-stage image is 244MB, for one HTML page! What takes the space?

Pick one answer.

Watch

The recipe has two FROM lines. Steps marked build run in node:24-alpine. The last stage starts from nginx and copies /src/dist from the build stage. Docker transcript: $ cat Dockerfile FROM node:24-alpine AS build WORKDIR /src COPY package.json package-lock.json ./ RUN npm ci COPY index.html . RUN npm run build FROM nginx:1.30-alpine COPY --from=build /src/dist/ /usr/share/nginx/html/ $ docker build -t kedai:1.0 . [+] Building 2.5s (14/14) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 241B 0.0s => [internal] load metadata for docker.io/library/node:24-alpine 0.0s => [internal] load metadata for docker.io/library/nginx:1.30-alpine 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 58B 0.0s => [build 1/6] FROM docker.io/library/node:24-alpine@sha256:8a32c8cf649a4 0.0s => => resolve docker.io/library/node:24-alpine@sha256:8a32c8cf649a49915a6 0.0s => [build 2/6] WORKDIR /src 0.0s => [internal] load build context 0.0s => => transferring context: 649B 0.0s => [build 3/6] COPY package.json package-lock.json ./ 0.0s => [build 4/6] RUN npm ci 1.2s => [build 5/6] COPY index.html . 0.0s => [build 6/6] RUN npm run build 1.2s => [stage-1 1/2] FROM docker.io/library/nginx:1.30-alpine@sha256:0e52bd05 0.0s => => resolve docker.io/library/nginx:1.30-alpine@sha256:0e52bd054126cdc0 0.0s => [stage-1 2/2] COPY --from=build /src/dist/ /usr/share/nginx/html/ 0.0s => exporting to image 0.1s => => exporting layers 0.0s => => exporting manifest sha256:3b7b615514d04c88d3e2c28410193368d61225c5d 0.0s => => exporting config sha256:ef2a3699bc2a7565ae894c58b5cf8671e027dc3c02b 0.0s => => exporting attestation manifest sha256:ff1f01a5f3560a8e72609ca69ecba 0.0s => => exporting manifest list sha256:662fb001df1cbe95c693093857d746ef5b21 0.0s => => naming to docker.io/library/kedai:1.0 0.0s => => unpacking to docker.io/library/kedai:1.0 0.0s

Your laptop~/kedai

  • No open hatches

Lunchboxes (containers)

  • No containers yet

Frozen packs (images)

  • nginx:1.30-alpine8 layers · 93.5MB
  • node:24-alpine4 layers · 244MB

0 containers (0 running), 2 images, 0 lockers.

A multi-stage build: Cook in one stage, pack in another. Only the finished dish goes in the final image. has two kitchens. The build stage cooks with Node. The last stage starts fresh from nginx. COPY --from=build carries over only the dish: dist/.

Try it

Put this two-stage recipe in order.

First line at the top. Put these in order. Use the arrows, or drag.

  • FROM node:24-alpine AS build
  • COPY --from=build /src/dist/ /usr/share/nginx/html/
  • FROM nginx:1.30-alpine
  • RUN npm run build
  • RUN npm ci

Try it

Build only the cooking stage as kedai:build. Then build the full recipe as kedai:1.0. Compare their sizes.

Your laptop~/kedai

  • No open hatches

Lunchboxes (containers)

  • No containers yet

Frozen packs (images)

  • nginx:1.30-alpine8 layers · 93.5MB
  • node:24-alpine4 layers · 244MB

0 containers (0 running), 2 images, 0 lockers.

Predict

Is npm inside kedai:1.0?

Pick one answer.

Check

which npm prints nothing in kedai:1.0 and exits with code 1. In kedai:build it prints /usr/local/bin/npm. Docker transcript: $ docker build -t kedai:1.0 . [+] Building 2.5s (14/14) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 241B 0.0s => [internal] load metadata for docker.io/library/node:24-alpine 0.0s => [internal] load metadata for docker.io/library/nginx:1.30-alpine 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 58B 0.0s => [build 1/6] FROM docker.io/library/node:24-alpine@sha256:8a32c8cf649a4 0.0s => => resolve docker.io/library/node:24-alpine@sha256:8a32c8cf649a49915a6 0.0s => [build 2/6] WORKDIR /src 0.0s => [internal] load build context 0.0s => => transferring context: 649B 0.0s => [build 3/6] COPY package.json package-lock.json ./ 0.0s => [build 4/6] RUN npm ci 1.2s => [build 5/6] COPY index.html . 0.0s => [build 6/6] RUN npm run build 1.2s => [stage-1 1/2] FROM docker.io/library/nginx:1.30-alpine@sha256:0e52bd05 0.0s => => resolve docker.io/library/nginx:1.30-alpine@sha256:0e52bd054126cdc0 0.0s => [stage-1 2/2] COPY --from=build /src/dist/ /usr/share/nginx/html/ 0.0s => exporting to image 0.1s => => exporting layers 0.0s => => exporting manifest sha256:b6170ae2cca287d1cfd093d147dcd0cbd726d4935 0.0s => => exporting config sha256:93f86126b8a273dc01166858398633f1510777c52c6 0.0s => => exporting attestation manifest sha256:b7fdc79b423faa069eb80d989f764 0.0s => => exporting manifest list sha256:02f32a2a17b285d903ae428e07cb0471f91d 0.0s => => naming to docker.io/library/kedai:1.0 0.0s => => unpacking to docker.io/library/kedai:1.0 0.0s $ docker run --rm kedai:1.0 which npm $ docker build --target build -t kedai:build . [+] Building 0.1s (11/11) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 241B 0.0s => [internal] load metadata for docker.io/library/node:24-alpine 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 58B 0.0s => [build 1/6] FROM docker.io/library/node:24-alpine@sha256:8a32c8cf649a4 0.0s => => resolve docker.io/library/node:24-alpine@sha256:8a32c8cf649a49915a6 0.0s => CACHED [build 2/6] WORKDIR /src 0.0s => [internal] load build context 0.0s => => transferring context: 649B 0.0s => CACHED [build 3/6] COPY package.json package-lock.json ./ 0.0s => CACHED [build 4/6] RUN npm ci 0.0s => CACHED [build 5/6] COPY index.html . 0.0s => CACHED [build 6/6] RUN npm run build 0.0s => exporting to image 0.1s => => exporting layers 0.0s => => exporting manifest sha256:e61898031a5a7fb12428235d8f997b84668086645 0.0s => => exporting config sha256:57b36c9339cf71550e7b7f91a35c06f3ec538999b20 0.0s => => exporting attestation manifest sha256:237b6d641f10b568a71505d1cf518 0.0s => => exporting manifest list sha256:f1106e278ff1080160aed1a1670a7ba40809 0.0s => => naming to docker.io/library/kedai:build 0.0s => => unpacking to docker.io/library/kedai:build 0.0s $ docker run --rm kedai:build which npm /usr/local/bin/npm

Your laptop~/kedai

  • No open hatches

Lunchboxes (containers)

  • No containers yet

Frozen packs (images)

  • nginx:1.30-alpine8 layers · 93.5MB
  • node:24-alpine4 layers · 244MB

0 containers (0 running), 2 images, 0 lockers.

which npm finds nothing in kedai:1.0. In the build stage, npm is there. Diners never see the kitchen.

You did it!

You can now cook in one stage and ship only the dish.

Go deeper (optional)

Stages without a name are called by number: COPY --from=0. Names are clearer. COPY --from can also copy from any image, like --from=nginx:1.30-alpine. Fewer tools in the final image also means fewer security holes.

Real mission

  1. docker images

0 of 3 steps done · Your ticks are saved.