Download and install Docker Desktop.
Open a terminal andclone this sample application.
$ git clone https://github.com/dockersamples/todo-list-appNavigate into the todo-list-app directory:
$ cd todo-list-appInside this directory, you'll find a file named Dockerfile with the following content:
FROM node:20-alpineWORKDIR /appCOPY . .RUN yarn install --productionEXPOSE 3000CMD ["node", "./src/index.js"]Execute the following command to build the Docker image:
$ docker build .Here’s the result of the build process:
[+] Building 20.0s (10/10) FINISHEDThe first line indicates that the entire build process took 20.0 seconds. The first build may take some time as it installs dependencies.
Rebuild without making changes.
Now, re-run the docker build command without making any change in the source code or Dockerfile as shown:
$ docker build .Subsequent builds after the initial are faster due to the caching mechanism, as long as the commands and context remain unchanged. Docker caches the intermediate layers generated during the build process. When you rebuild the image without making any changes to the Dockerfile or the source code, Docker can reuse the cached layers, significantly speeding up the build process.
[+] Building 1.0s (9/9) FINISHEDdocker:desktop-linux => [internal] load build definition from Dockerfile0.0s => => transferring dockerfile: 187B0.0s ... => [internal] load build context0.0s => => transferring context: 8.16kB 0.0s => CACHED [2/4] WORKDIR /app0.0s => CACHED [3/4] COPY . .0.0s => CACHED [4/4] RUN yarn install --production 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => exporting manifestThe subsequent build was completed in just 1.0 second by leveraging the cached layers. No need to repeat time-consuming steps like installing dependencies.
StepsDescriptionTime Taken(1st Run)Time Taken (2nd Run)1Load build definition from Dockerfile0.0 seconds0.0 seconds2Load metadata for docker.io/library/node:20-alpine2.7 seconds0.9 seconds3Load .dockerignore0.0 seconds0.0 seconds4Load build context(Context size: 4.60MB)
0.1 seconds0.0 seconds5Set the working directory (WORKDIR)0.1 seconds0.0 seconds6Copy the local code into the container0.0 seconds0.0 seconds7Run yarn install --production10.0 seconds0.0 seconds8Exporting layers2.2 seconds0.0 seconds9Exporting the final image3.0 seconds0.0 secondsGoing back to the docker image history output, you see that each command in the Dockerfile becomes a new layer in the image. You might remember that when you made a change to the image, the yarn dependencies had to be reinstalled. Is there a way to fix this? It doesn't make much sense to reinstall the same dependencies every time you build, right?
To fix this, restructure your Dockerfile so that the dependency cache remains valid unless it really needs to be invalidated. For Node-based applications, dependencies are defined in the package.json file. You'll want to reinstall the dependencies if that file changes, but use cached dependencies if the file is unchanged. So, start by copying only that file first, then install the dependencies, and finally copy everything else. Then, you only need to recreate the yarn dependencies if there was a change to the package.json file.
Update the Dockerfile to copy in the package.json file first, install dependencies, and then copy everything else in.
FROM node:20-alpineWORKDIR /appCOPY package.json yarn.lock ./RUN yarn install --production COPY . . EXPOSE 3000CMD ["node", "src/index.js"]Create a file named .dockerignore in the same folder as the Dockerfile with the following contents.
node_modulesBuild the new image:
$ docker build .You'll then see output similar to the following:
[+] Building 16.1s (10/10) FINISHED=> [internal] load build definition from Dockerfile0.0s=> => transferring dockerfile: 175B0.0s=> [internal] load .dockerignore 0.0s=> => transferring context: 2B0.0s=> [internal] load metadata for docker.io/library/node:21-alpine 0.0s=> [internal] load build context 0.8s=> => transferring context: 53.37MB0.8s=> [1/5] FROM docker.io/library/node:21-alpine0.0s=> CACHED [2/5] WORKDIR /app 0.0s=> [3/5] COPY package.json yarn.lock ./0.2s=> [4/5] RUN yarn install --production14.0s=> [5/5] COPY . . 0.5s=> exporting to image 0.6s=> => exporting layers0.6s=> => writing image sha256:d6f819013566c54c50124ed94d5e66c452325327217f4f04399b45f94e37d250.0s=> => naming to docker.io/library/node-app:2.0 0.0sYou'll see that all layers were rebuilt. Perfectly fine since you changed the Dockerfile quite a bit.
Now, make a change to the src/static/index.html file (like change the title to say "The Awesome Todo App").
Build the Docker image. This time, your output should look a little different.
$ docker build -t node-app:3.0 .You'll then see output similar to the following:
[+] Building 1.2s (10/10) FINISHED => [internal] load build definition from Dockerfile0.0s=> => transferring dockerfile: 37B0.0s=> [internal] load .dockerignore 0.0s=> => transferring context: 2B0.0s=> [internal] load metadata for docker.io/library/node:21-alpine 0.0s => [internal] load build context 0.2s=> => transferring context: 450.43kB 0.2s=> [1/5] FROM docker.io/library/node:21-alpine0.0s=> CACHED [2/5] WORKDIR /app 0.0s=> CACHED [3/5] COPY package.json yarn.lock ./0.0s=> CACHED [4/5] RUN yarn install --production 0.0s=> [5/5] COPY . . 0.5s => exporting to image 0.3s=> => exporting layers0.3s=> => writing image sha256:91790c87bcb096a83c2bd4eb512bc8b134c757cda0bdee4038187f98148e2eda0.0s=> => naming to docker.io/library/node-app:3.0 0.0sFirst off, you should notice that the build was much faster. You'll see that several steps are using previously cached layers. That's good news; you're using the build cache. Pushing and pulling this image and updates to it will be much faster as well.