Exercise

  1. Modify the pong server code from the previous exercise. For example, you can add an instruction that logs a string.

  2. Build a new image and tag it as pong:1.1

  3. What do you observe in the build command output?

  4. Modify the Dockerfile to ensure that dependencies aren’t rebuilt when a change is made to the code. Create image pong:1.2 from this new Dockerfile.

  5. Modify the application code once again and create image pong:1.3. Observe how the cache is used.


Solution
  1. Here we modify the nodejs server that we created in the previous exercise.

The following code is the new content of the pong.js file:

pong.js
var express = require('express');
var app = express();
app.get('/ping', function(req, res) {
    console.log("received");
    res.setHeader('Content-Type', 'text/plain');
    console.log("pong"); // <-- added comment
    res.end("PONG");
});
app.listen(80);
  1. We build the pong:1.1 image with the following command:
docker image build -t pong:1.1 .
  1. We observe that each build step was performed again. It would be better to ensure that a simple source code modification doesn’t trigger the rebuilding of dependencies.

  2. A good practice to follow when writing a Dockerfile is to ensure that elements that are modified most often (application code for example) are positioned lower than elements that are modified less frequently (dependency list).

We can modify the Dockerfile as follows:

Dockerfile
FROM node:16-alpine
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 80
CMD ["npm", "start"]

The approach followed here is:

  • copy the package.json file that contains dependencies
  • build dependencies
  • copy source code

We then rebuild the image once again, giving it the tag pong:1.2

docker image build -t pong:1.2 .

A cache is created for each build step.

  1. We make another modification to the pong.js file:
var express = require('express');
var app = express();
app.get('/ping', function(req, res) {
    console.log("received");
    res.setHeader('Content-Type', 'text/plain');
    console.log("ping-pong"); // <-- modified comment
    res.end("PONG");
});
app.listen(80);

We then launch the build once again, naming the image pong:1.3:

docker image build -t pong:1.3 .

We can then observe that the cache is used up to step 4. It is then invalidated at step 5 (COPY instruction) because the Docker daemon detected the code change we made. Cache usage often allows saving significant time during the build phase.