Developing Containers with Dockerfiles

1. Overview

Docker is an open platform for developing, shipping, and running applications. With Docker, you can separate your applications from your infrastructure and treat your infrastructure like a managed application. Docker helps you ship code faster, test faster, deploy faster, and shorten the cycle between writing code and running code.

Docker does this by combining kernel containerization features with workflows and tooling that helps you manage and deploy your applications.

Docker containers can be directly used in Kubernetes, which allows them to be run in the Kubernetes Engine with ease. After learning the essentials of Docker, you will have the skillset to start developing Kubernetes and containerized applications.

What you'll learn

In this lab, you will learn how to do the following:

  • Create a Dockerfile for a sample application
  • Build an image
  • Run the image as a container locally
  • Change container behavior
  • Push the image to Artifact Registry

Prerequisites

This is an introductory level lab. Little to no prior experience with Docker and containers is assumed. Familiarity with Cloud Shell and the command line is suggested, but not required.

Self-paced environment setup

  1. Sign-in to the Google Cloud Console and create a new project or reuse an existing one. If you don't already have a Gmail or Google Workspace account, you must create one.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • The Project name is the display name for this project's participants. It is a character string not used by Google APIs, and you can update it at any time.
  • The Project ID must be unique across all Google Cloud projects and is immutable (cannot be changed after it has been set). The Cloud Console auto-generates a unique string; usually you don't care what it is. In most codelabs, you'll need to reference the Project ID (and it is typically identified as PROJECT_ID), so if you don't like it, generate another random one, or, you can try your own and see if it's available. Then it's "frozen" after the project is created.
  • There is a third value, a Project Number which some APIs use. Learn more about all three of these values in the documentation.
  1. Next, you'll need to enable billing in the Cloud Console in order to use Cloud resources/APIs. Running through this codelab shouldn't cost much, if anything at all. To shut down resources so you don't incur billing beyond this tutorial, follow any "clean-up" instructions found at the end of the codelab. New users of Google Cloud are eligible for the $300 USD Free Trial program.

2. Sample Application

A sample application has been provided to facilitate this lab. In this section you will retrieve the source code and build the application in its native form before moving on to the containerization process.

Source Code

The source code for this lab is available in the GoogleCloudPlatform/container-developer-workshop repository along with the sample application documentation.

Configure git

git config --global user.name ${USER}
git config --global user.email ${USER}@qwiklabs.net

Clone the sample application Cloud Source Repository

gcloud source repos clone sample-app ${HOME}/sample-app &&
cd ${HOME}/sample-app &&
git checkout main

Output

Cloning into '/home/student_03_49720296e995/sample-app'...
remote: Finding sources: 100% (16/16)
remote: Total 16 (delta 0), reused 16 (delta 0)
Receiving objects: 100% (16/16), 47.23 KiB | 681.00 KiB/s, done.
warning: remote HEAD refers to nonexistent ref, unable to checkout.

Project [qwiklabs-gcp-02-4327c4e03d82] repository [sample-app] was cloned to [/home/student_03_49720296e995/sample-app].
Branch 'main' set up to track remote branch 'main' from 'origin'.
Switched to a new branch 'main'

Build the sample application

cd ${HOME}/sample-app
./mvnw compile

Output

[INFO] Scanning for projects...
...
[INFO] Compiling 1 source file to /home/student_03_49720296e995/sample-app/target/classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  10.080 s
[INFO] Finished at: 2022-02-23T17:14:30Z
[INFO] ------------------------------------------------------------------------

Run the sample application

cd ${HOME}/sample-app
./mvnw exec:java

Output

[INFO] Scanning for projects...
...
Listening at http://localhost:8080

Preview the running application

  • Click the Cloud Shell Web Preview button
  • Click Preview on port 8080

When you're done

  • Press CTRL + c in Cloud Shell to stop the running application

3. Dockerfile

Containerizing the Application with a Dockerfile

One method of packaging an application into a container is with the use of a Dockerfile. The Dockerfile is similar to a script which instructs the daemon on how to assemble the container image. See the Dockerfile reference documentation) for more information.

Create an empty Dockerfile in the sample application repository.

touch ${HOME}/sample-app/Dockerfile

Open the Dockerfile in your editor of choice.

vi ${HOME}/sample-app/Dockerfile

Choose a starting image

Using the Dockerfile method to build a container requires direct knowledge about the application in order to assemble the container. The first step to creating a Dockerfile is selecting an image that will be used as the basis of your image.This image should be a parent or base image maintained and published by a trusted source, usually your company.

The FROM instruction initializes a new build stage and sets the base image for subsequent sequential commands. Thus the FROM instruction is usually the first instruction in a Dockerfile and can only be preceded by an optional ARG instruction to support variables.

Syntax: FROM <image>[:<tag> | @<digest>] [AS <name>]

The format for an image is <image>:<tag> or <image>@<digest>. If a tag or digest is not specified it defaults to the :latest tag. The format of <image> varies based on the registry used to store the image. For Artifact Registry the <image> format is <region>-docker.pkg.dev/<project ID>/<repository name>/<image name>:<image tag>.

For this lab we use the public openjdk:11.0-jdk image, add the following line to your Dockerfile

FROM openjdk:11.0-jdk

Set the working directory

The WORKDIR instruction sets the working directory for any sequential instructions that follow in the Dockerfile. For more information see the WORKDIR section of the Dockerfile reference documentation

Syntax: WORKDIR <path>

For this lab we use the /app directory as our WORKDIR, add the following line to the bottom of your Dockerfile

WORKDIR /app

Copy the application files

The COPY instruction copies directories or files from the <source> location to the <destination> path of the image filesystem. Multiple <source> resources can be specified and they are all relative to the build context. The build context will be discussed further in the Build section. For more information see the COPY section of the Dockerfile reference documentation

Syntax: COPY <source>... <destination>

For this lab we will copy all the files in the repository to the image filesystem, add the following line to the bottom of your Dockerfile

COPY . /app

Compile the application

The RUN instruction executes commands in a new image layer on top of the current image and commits the results. The resulting committed image will be used for sequential steps in the Dockerfile. For more information see the RUN section of the Dockerfile reference documentation

Syntax: RUN <command>

For this lab we will use Maven to compile the application to a JAR file, add the following line to the bottom of your Dockerfile

RUN ./mvnw compile assembly:single

Start the application

The CMD instruction provides the default command for a running container. There can only be one CMD instruction in a Dockerfile, if more than one CMD is specified then only the last CMD will take effect. There is more advanced functionality available using both the CMD and ENTRYPOINT instructions, but that is not covered in this lab. For more information see the CMD` section of the Dockerfile reference documentation

Syntax: CMD ["executable","param1","param2"]

For this lab we run the JAR file we compiled, add the following line to the bottom of your Dockerfile

CMD ["java","-jar","/app/target/sample-app-1.0.0-jar-with-dependencies.jar"]

Final Dockerfile

The final Dockerfile will be

FROM openjdk:11.0-jdk
WORKDIR /app
COPY . /app
RUN ./mvnw compile assembly:single
CMD ["java","-jar","/app/target/sample-app-1.0.0-jar-with-dependencies.jar"]

Commit the Dockerfile locally

cd ${HOME}/sample-app
git add Dockerfile
git commit -m "Added Dockerfile"

4. Build

Now we will build the image from the Dockerfile by using the docker build command. This command instructs the docker daemon to build the image using the instructions from our Dockerfile. See the docker build reference documentation for more information.

Build the image

cd ${HOME}/sample-app
export IMAGE_TAG=$(git rev-parse --short HEAD)
docker build --tag sample-app:${IMAGE_TAG} .

Output

Sending build context to Docker daemon  221.2kB
Step 1/4 : FROM openjdk:11.0-jdk
11.0-jdk: Pulling from library/openjdk
0c6b8ff8c37e: Pull complete
412caad352a3: Pull complete
e6d3e61f7a50: Pull complete
461bb1d8c517: Pull complete
e442ee9d8dd9: Pull complete
542c9fe4a7ba: Pull complete
41de18d1833d: Pull complete
Digest: sha256:d72b1b9e94e07278649d91c635e34737ae8f181c191b771bde6816f9bb4bd08a
Status: Downloaded newer image for openjdk:11.0-jdk
---> 2924126f1829
Step 2/4 : WORKDIR /app
---> Running in ea037abb273d
Removing intermediate container ea037abb273d
---> bd9b6d078082
Step 3/4 : COPY . /app
---> b9aec2b5de51
Step 4/4 : RUN ./mvnw compile jar:jar
---> Running in 3f5ff737b7fd
[INFO] Scanning for projects...
...
[INFO] Building jar: /app/target/sample-app-1.0.0.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  22.952 s
[INFO] Finished at: 2022-02-23T18:09:08Z
[INFO] ------------------------------------------------------------------------
Removing intermediate container 331443caebd3
---> 152f65cc441e
Step 5/5 : CMD ["java", "-jar", "/app/target/sample-app-1.0.0.jar"]
---> Running in 3d595a72231c
Removing intermediate container 3d595a72231c
---> 0e40d7548cab
Successfully built 0e40d7548cab
Successfully tagged sample-app:aaa8895

5. Run

Upon successful build of our container image, we are now able to run our application and make sure that it behaves as expected using the docker run command. This command will launch our container in the foreground of our command prompt for testing or debugging. See the docker run reference documentation for more information.

Run a container using the image

cd ${HOME}/sample-app
export IMAGE_TAG=$(git rev-parse --short HEAD)
docker run \
  --rm \
  -p 8080:8080 \
  sample-app:${IMAGE_TAG}

Output

Listening at http://localhost:8080

Preview the application running in a container

  • Click the Cloud Shell Web Preview button
  • Click Preview on port 8080
  • Press CTRL + c in Cloud Shell to stop the containers

Changing container behavior

Executing Docker Run uses the default configuration in the Dockerfile. Additional instructions and parameters can be added to modify this behavior.

Enable TRACE logging

cd ${HOME}/sample-app
export IMAGE_TAG=$(git rev-parse --short HEAD)
docker run \
  --rm \
  -p 8080:8080 \
  sample-app:${IMAGE_TAG} \
  java -Dorg.slf4j.simpleLogger.defaultLogLevel=trace -jar /app/target/sample-app-1.0.0-jar-with-dependencies.jar

Preview the application running in a container

  • Click the Cloud Shell Web Preview button
  • Click Preview on port 8080
  • Switch to the Cloud Shell tab and see that additional logging
  • Press CTRL + c in Cloud Shell to stop the container

Change port

cd ${HOME}/sample-app
export IMAGE_TAG=$(git rev-parse --short HEAD)
docker run \
--rm \
-e PORT=8081 \
-p 8081:8081 \
sample-app:${IMAGE_TAG}

Preview the application running in a container

  • Click the Cloud Shell Web Preview button
  • Click Change port
  • Enter 8081
  • Click Change and Preview
  • Press CTRL + c in Cloud Shell to stop the container

6. Push

Once confident that the container image is running properly and we want to make this container available to run in other environments and/or by other users, we need to push the image to a shared repository. This should happen as part of an automated build pipeline but in our test environment we already have a repository configured and we can manually push our image.

Push the Dockerfile commit to the sample-app repository

cd ${HOME}/sample-app
export IMAGE_TAG=$(git rev-parse --short HEAD)
git push

Tag the image for Artifact Registry

docker tag sample-app:${IMAGE_TAG} \
    us-central1-docker.pkg.dev/${GOOGLE_CLOUD_PROJECT}/apps/sample-app:${IMAGE_TAG}

Configure your credentials for Artifact Registry

gcloud auth configure-docker us-central1-docker.pkg.dev

When prompted Do you want to continue (Y/n)? answer y and press Enter

Push the image to Artifact Registry

docker push us-central1-docker.pkg.dev/${GOOGLE_CLOUD_PROJECT}/apps/sample-app:${IMAGE_TAG}

Output

 The push refers to repository [us-central1-docker.pkg.dev/qwiklabs-gcp-04-b47ced695a3c/apps/sample-app]
  453b97f86449: Pushed
  e86791aa0382: Pushed
  d404c7ee0850: Pushed
  fe4f44af763d: Pushed
  7c072cee6a29: Pushed
  1e5fdc3d671c: Pushed
  613ab28cf833: Pushed
  bed676ceab7a: Pushed
  6398d5cccd2c: Pushed
  0b0f2f2f5279: Pushed
  aaa8895: digest: sha256:459de00f86f159cc63f98687f7c9563fd65a2eb9bcc71c23dda3351baf13607a size: 2424

7. Congratulations!

Congratulations, you finished the codelab!

What you've covered

  • Created a Dockerfile for a sample application
  • Built an image
  • Ran the image as a container locally
  • Changed container behavior
  • Pushed the image to Artifact Registry