Bookmark this page

Guided Exercise: Create Images with Containerfiles

Create a basic Containerfile for an example Node.js application. Throughout the exercise, you gradually improve the Containerfile and the resulting container image.

Outcomes

You should be able to:

  • Create a Containerfile by using an appropriate base image.

  • Use common Containerfile instructions, such as FROM, COPY, RUN, and CMD.

  • Optimize instructions to reduce image size and number of layers.

As the student user on the workstation machine, use the lab command to prepare your system for this exercise.

[student@workstation ~]$ lab start custom-containerfiles

Instructions

  1. Create a Containerfile for the hello-server Node.js application.

    Important

    The Containerfile you create in this initial step does not follow Red Hat guidance for creating container images. You improve this Containerfile throughout the exercise.

    1. Change into the ~/DO188/labs/custom-containerfiles/hello-server directory.

      [student@workstation ~]$ cd ~/DO188/labs/custom-containerfiles/hello-server
      no output expected
    2. Create a file called Containerfile with the following contents:

      FROM registry.ocp4.example.com:8443/redhattraining/podman-ubi8.6
      
      COPY . /tmp/hello-server
      
      RUN dnf module enable -y nodejs:16
      
      RUN dnf install -y nodejs
      
      RUN cd /tmp/hello-server && npm install
      
      CMD cd /tmp/hello-server && npm start

      Note

      This exercise uses a customized UBI image that replaces the default UBI repositories with an internal classroom repository to eliminate the impact of network errors.

      By default, Red Hat serves UBI repositories at the https://cdn-ubi.redhat.com/content/public/ubi/ public CDN. Classroom environments might experience problems when connecting to this CDN.

      Under reliable network conditions, use the default UBI image.

      For similar reasons, the npm install command uses an internal NPM registry, specified in the .npmrc file.

    3. Build a container image tagged hello-server:bad by using the Containerfile you created.

      [student@workstation hello-server]$ podman build -t hello-server:bad .
      ...output omitted...
      Successfully tagged localhost/hello-server:bad
      ...output omitted...
    4. Create a container called hello-bad that uses the hello-server:bad image and binds the container port 3000 to the host port 3000.

      [student@workstation hello-server]$ podman run -d --rm --name hello-bad \
      -p 3000:3000 hello-server:bad
      ecb8...b5af

      The command prints the ID of the running container, which differs on your machine.

    5. Make a request to the running container by using the curl command.

      [student@workstation hello-server]$ curl http://localhost:3000/greet ;echo
      {"hello":"world"}
    6. Stop the hello-bad container.

      [student@workstation hello-server]$ podman stop hello-bad
      hello-bad
  2. Update the Containerfile to reduce the number of image layers it produces and simplify directory usage.

    1. Inspect the number of image layers in the hello-server:bad image.

      [student@workstation hello-server]$ podman image tree hello-server:bad
      ...output omitted...
      Image Layers
      ├── ID: b4e347eee7c8 Size: 214.8MB Top Layer of: [registry.ocp4.example.com:8443/ubi8/ubi:8.6]
      ├── ID: 0ccddeb165f4 Size:  5.12kB Top Layer of: [registry.ocp4.example.com:8443/redhattraining/podman-ubi8.6:latest]
      ├── ID: bb66b6696d7c Size: 33.28kB
      ├── ID: 2ff1e1e2de71 Size: 24.71MB
      ├── ID: c47cd082174e Size: 177.7MB
      └── ID: 2d8e19786ce3 Size:   151MB Top Layer of: [localhost/hello-server:bad]

      The layer IDs and sizes differ in your environment from the preceding example.

      Notice that the Containerfile creates several layers on top of the Red Hat UBI base image.

    2. Open the Containerfile that you created and refactor the file to use a WORKDIR instruction. Update the paths in the rest of the file to match.

      FROM registry.ocp4.example.com:8443/redhattraining/podman-ubi8.6
      
      WORKDIR /tmp/hello-server
      
      COPY . .
      
      RUN dnf module enable -y nodejs:16
      
      RUN dnf install -y nodejs
      
      RUN npm install
      
      CMD npm start

      By using WORKDIR, the other instructions no longer need to change directories. This makes reading the Containerfile easier and reduces potential mistakes in file paths.

      The COPY . . instruction copies the content of the current directory (~/DO188/labs/custom-containerfiles/hello-server) into the current directory in the container (/tmp/hello-server).

    3. Update the Containerfile by merging the RUN instructions into a single instruction. The final Containerfile contains the following instructions:

      FROM registry.ocp4.example.com:8443/redhattraining/podman-ubi8.6
      
      WORKDIR /tmp/hello-server
      
      COPY . .
      
      RUN dnf module enable -y nodejs:16 && \
          dnf install -y nodejs && \
          npm install
      
      CMD npm start
    4. Build the container image by using the updated Containerfile. Use the hello-server:better image tag.

      [student@workstation hello-server]$ podman build -t hello-server:better .
      ...output omitted...
      Successfully tagged localhost/hello-server:better
      ...output omitted...
    5. Inspect the number of image layers in the hello-server:better image.

      [student@workstation hello-server]$ podman image tree hello-server:better
      ...output omitted...
      Image Layers
      ├── ID: b4e347eee7c8 Size: 214.8MB Top Layer of: [registry.ocp4.example.com:8443/ubi8/ubi:8.6]
      ├── ID: 0ccddeb165f4 Size:  5.12kB Top Layer of: [registry.ocp4.example.com:8443/redhattraining/podman-ubi8.6:latest]
      ├── ID: eab1a860d5d1 Size: 33.28kB
      └── ID: c17e4e5072d4 Size: 350.7MB Top Layer of: [localhost/hello-server:better]

      Notice that the Containerfile creates two layers on top of the podman-ubi8.6 image. Reducing the number of RUN instructions also reduces the number of resulting image layers.

    6. Run and test the container to verify that it works.

      [student@workstation hello-server]$ podman run -d --rm --name hello-better \
      -p 3000:3000 hello-server:better
      d6c3...9f21
      [student@workstation hello-server]$ curl http://localhost:3000/greet ;echo
      {"hello":"world"}
      [student@workstation hello-server]$ podman stop hello-better
      hello-better
  3. Update the Containerfile to use a better base image and use environment variables to adjust server settings.

    1. Open the Containerfile and change the FROM instruction to use the Node.js runtime image provided by Red Hat. This image is based on the Red Hat Universal Base Image (UBI) and includes the Node.js runtime.

      FROM registry.ocp4.example.com:8443/ubi8/nodejs-16:1

      Note

      Using the ubi8/nodejs-16:1 image reduces the container image build time and complexity. To optimize for overall image size, you might use the ubi8/nodejs-16-minimal or ubi8/ubi-minimal image instead.

      Choosing a base image is a complex topic that involves competing concerns. Organizations choose a base image that is optimized for their main concern, such as image size, complexity, or the speed of removing runtime vulnerabilities.

    2. Because the base image includes the Node.js runtime, remove the dnf module enable -y nodejs:16 and dnf install -y nodejs part of the RUN instruction.

      COPY . .
      
      RUN npm install
      
      CMD npm start

      This update does not change the number of image layers because the number of RUN instructions remains the same.

    3. Add ENV instructions to set environment variables within the container that the application uses.

      FROM registry.ocp4.example.com:8443/ubi8/nodejs-16:1
      
      ENV SERVER_PORT=3000
      ENV NODE_ENV="production"
      
      WORKDIR /tmp/hello-server

      Note

      Setting the NODE_ENV environment variable to production instructs NPM to ignore the dependencies listed in the devDependencies section of the package.json file.

      Because the devDependencies packages are not necessary to run the application, setting the NODE_ENV variable to production reduces the image size.

      The application uses the SERVER_PORT environment variable to determine the port to which it binds.

    4. Update the working directory to /opt/app-root/src, which is a better location than /tmp/hello-server.

      WORKDIR /opt/app-root/src
  4. Apply appropriate metadata to the image.

    1. Add a LABEL instruction to indicate who is responsible for maintaining the image.

      FROM registry.ocp4.example.com:8443/ubi8/nodejs-16:1
      
      LABEL org.opencontainers.image.authors="Your Name"
      
      ENV SERVER_PORT=3000
    2. Add further LABEL instructions to provide hints as to the version and intended usage of the container image.

      LABEL org.opencontainers.image.authors="Your Name"
      LABEL com.example.environment="production"
      LABEL com.example.version="0.0.1"
      
      ENV SERVER_PORT=3000
    3. Add an EXPOSE instruction to indicate that the application within the container binds to the port defined in the SERVER_PORT environment variable.

      ENV SERVER_PORT=3000
      ENV NODE_ENV="production"
      
      EXPOSE $SERVER_PORT
      
      WORKDIR /opt/app-root/src

      The EXPOSE instruction serves for documentation purposes. It does not bind the port on the host running the container.

    4. The final Containerfile contains the following instructions:

      FROM registry.ocp4.example.com:8443/ubi8/nodejs-16:1
      
      LABEL org.opencontainers.image.authors="Your Name"
      LABEL com.example.environment="production"
      LABEL com.example.version="0.0.1"
      
      ENV SERVER_PORT=3000
      ENV NODE_ENV="production"
      
      EXPOSE $SERVER_PORT
      
      WORKDIR /opt/app-root/src
      
      COPY . .
      
      RUN npm install
      
      CMD npm start
    5. Build the container image by using the updated Containerfile. Use the hello-server:best image tag.

      [student@workstation hello-server]$ podman build -t hello-server:best .
      ...output omitted...
      Successfully tagged localhost/hello-server:best
      ...output omitted...
    6. Inspect the container image to observe the added metadata.

      [student@workstation hello-server]$ podman inspect hello-server:best \
      -f '{{.Config.Env}}'
      ...output omitted... SERVER_PORT=3000 NODE_ENV=production]
    7. Verify that the application works.

      [student@workstation hello-server]$ podman run -d --rm --name hello-best \
      -p 3000:3000 hello-server:best
      d6c3...9f21
      [student@workstation hello-server]$ curl http://localhost:3000/greet ;echo
      {"hello":"world"}
      [student@workstation hello-server]$ podman stop hello-best
      hello-best
    8. Change into the home directory.

      [student@workstation hello-server]$ cd
      no output expected

Finish

On the workstation machine, use the lab command to complete this exercise. This is important to ensure that resources from previous exercises do not impact upcoming exercises.

[student@workstation ~]$ lab finish custom-containerfiles

Revision: do188-4.14-8c43a16