1. Mirror layering
Using the docker image history command, you can see the commands used to create each layer in the image.
1. Use the docker image history command to view the layers in the created entry image.
docker image history getting-started
You should get output like this:
IMAGE CREATED CREATED BY SIZE COMMENT
a78a40cbf866 18 seconds ago /bin/sh -c #(nop) CMD ["node" "src/index.j⦠0B
f1d1808565d6 19 seconds ago /bin/sh -c yarn install --production 85.4MB
a2c054d14948 36 seconds ago /bin/sh -c #(nop) COPY dir:5dc710ad87c789593⦠198kB
9577ae713121 37 seconds ago /bin/sh -c #(nop) WORKDIR /app 0B
b95baba1cfdb 13 days ago /bin/sh -c #(nop) CMD ["node"] 0B
<missing> 13 days ago /bin/sh -c #(nop) ENTRYPOINT ["docker-entry⦠0B
<missing> 13 days ago /bin/sh -c #(nop) COPY file:238737301d473041⦠116B
<missing> 13 days ago /bin/sh -c apk add --no-cache --virtual .bui⦠5.35MB
<missing> 13 days ago /bin/sh -c #(nop) ENV YARN_VERSION=1.21.1 0B
<missing> 13 days ago /bin/sh -c addgroup -g 1000 node && addu⦠74.3MB
<missing> 13 days ago /bin/sh -c #(nop) ENV NODE_VERSION=12.14.1 0B
<missing> 13 days ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 13 days ago /bin/sh -c #(nop) ADD file:e69d441d729412d24⦠5.59MB
Each line represents a layer in the image. The display here shows the bottom at the bottom with the latest layer at the top. Using this option, you can also quickly view the size of each layer to help diagnose large images.
2. You will notice that several lines are truncated. If you add the --no trunc flag, you will get the complete output.
docker image history --no-trunc getting-started
2. Layer cache
Now that you have seen layering in action, there is an important lesson to learn to help reduce container image build times. Once a layer changes, all downstream layers must also be recreated.
Please review the following Dockerfile you created for your starter application.
# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
Go back to the image history output and you will see that each command in the Dockerfile becomes a new layer in the image. You may remember that when you make changes to your image, you must reinstall yarn dependencies. It doesn't make much sense to ship around the same dependencies every time you build.
To fix it, you need to restructure the Dockerfile to help support caching of dependencies. For node-based applications, these dependencies are defined in the package.json file. You can first copy just that file, install the dependencies, and then copy everything else. Yarn dependencies are then only recreated if package.json changes.
1. Update the Dockerfile to first copy in package.json, install dependencies, and then copy everything else in.
# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --production
COPY . .
CMD ["node", "src/index.js"]
2. Create a file named .dockerignore in the same folder as the Dockerfile, containing the following content.
node_modules
.dockerignore files are a simple way to selectively copy only the files related to the image. You can read more about this here. In this case, the node_modules folder should be omitted in the second COPY step because otherwise, it may overwrite the files created by the command in the RUN step.
3. Use docker Build to build a new image.
docker build -t getting-started .
You should see the following output.
[+] Building 16.1s (10/10) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 175B
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [internal] load metadata for docker.io/library/node:18-alpine
=> [internal] load build context
=> => transferring context: 53.37MB
=> [1/5] FROM docker.io/library/node:18-alpine
=> CACHED [2/5] WORKDIR /app
=> [3/5] COPY package.json yarn.lock ./
=> [4/5] RUN yarn install --production
=> [5/5] COPY . .
=> exporting to image
=> => exporting layers
=> => writing image sha256:d6f819013566c54c50124ed94d5e66c452325327217f4f04399b45f94e37d25
=> => naming to docker.io/library/getting-started
4. Now, make changes to the src/static/index.html file. For example, change <title> to "the Awesome Todo App."
5. Now use Docker Build-t getting started to build the Docker image. again. This time, your output should look a little different.
[+] Building 1.2s (10/10) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 37B
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [internal] load metadata for docker.io/library/node:18-alpine
=> [internal] load build context
=> => transferring context: 450.43kB
=> [1/5] FROM docker.io/library/node:18-alpine
=> CACHED [2/5] WORKDIR /app
=> CACHED [3/5] COPY package.json yarn.lock ./
=> CACHED [4/5] RUN yarn install --production
=> [5/5] COPY . .
=> exporting to image
=> => exporting layers
=> => writing image sha256:91790c87bcb096a83c2bd4eb512bc8b134c757cda0bdee4038187f98148e2eda
=> => naming to docker.io/library/getting-started
First, you should notice that builds are much faster. And, you'll see that several steps are using previously cached layers. Pushing, pulling, and updating this image will also be much faster.
3. Multi-stage construction
Multi-stage build is a very powerful tool that helps create images using multiple stages. They have several advantages:
- Separate build-time dependencies from run-time dependencies
- Reduce overall image size by delivering only what your application needs to run
1. Maven/Tomcat example
When building a Java-based application, a JDK is required to compile the source code into Java bytecode. However, the JDK is not required in production. Additionally, you may be using tools like Maven or Gradle to help build your application. These are not needed in the final image either. Multi-stage build help.
# syntax=docker/dockerfile:1
FROM maven AS build
WORKDIR /app
COPY . .
RUN mvn package
FROM tomcat
COPY --from=build /app/target/file.war /usr/local/tomcat/webapps
In this example, you use a stage (called build) to perform the actual Java build using Maven. In the second phase (starting from FROM tomcat), files are copied from the build phase. The final image is just the last stage being created and can be overridden using the --target flag.
2. React example
When building a React application, you need a Node environment to compile JS code (usually JSX), SASS stylesheets, etc. into static HTML, JS, and CSS. Without server-side rendering, a production build doesn't even require a node environment. You can ship static resources in a static nginx container.
# syntax=docker/dockerfile:1
FROM node:18 AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
In the previous Dockerfile example, it performs the build using the node:18 image (maximizing the layer cache) and then copies the output into the nginx container.