How-To Create Production Docker Images

Overview

When instructions are encountered to build a Docker image or use a Docker image in the ForgeRock DevOps documentation, you are met with the following warning (this warning also appears in the Dockerfile):

Support Note

ForgeRock provides no commercial support for production deployments that use ForgeRock’s evaluation-only Docker images. When deploying the ForgeRock Identity Platform using Docker images, you must build and use your own images for production deployments.

The main reason behind not being able to provide commercial support is that ForgeRock is not a Linux distribution software vendor. We cannot provide Debian updates to a base docker image, or JDK updates. It is up to you to ensure that the production Docker image used has the correct versions of software needed to run ForgeRock products, and that they don’t contain vulnerabilities. ForgeRock provides the Docker images for evaluation purposes only and will update the images as upstream updates are provided.

This article will cover an example of how to build your own Docker image, both using a different JDK/JRE and a different version of the Debian Linux distribution. Specifically, we will cover the Dockerfile provided with the Identity Gateway (IG) software distribution. The same concepts could easily be applied to the other evaluation docker images that ForgeRock supplies, for example: Access Management (AM).

There are several layers to building your own production image. In reverse order:

  1. IG Dockerfile (uses forgerock-io/java-11
  2. forgerock-io/java-11
  3. openjdk:11-jdk-slim / debian:buster (default references used by java-11 Dockerfile)

In a more abstract form:

  1. IG Production Image
  2. ForgeRock Base Image (customized)
  3. Source Images for constructing Base Image.

This article will cover how to build the first and second items. The 3rd items are provided as part of the Docker ecosystem (or you could build your own of course).

Examine the base forgerock Java-11 image

First, pull the docker image to your local workstation so that we can launch the container and take a look at its Dockerfile.

$ docker pull gcr.io/forgerock-io/java-11

Then we can check that it is indeed local:

$ docker image ls | grep forgerock-io
gcr.io/forgerock-io/java-11                                 latest     6918c55ea9f7   6 days ago      144MB
gcr.io/forgerock-io/java-17                                 latest     ee9f5259f3fb   5 weeks ago     144MB

Now we can start a bash shell for that image and inspect the Dockerfile:

$ docker run -it gcr.io/forgerock-io/java-11 /bin/bash
root@2c46f435a1bc:/# ls
Dockerfile.java-11  bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root	run  sbin  srv	sys  tmp  usr  var
root@2c46f435a1bc:/# cat Dockerfile.java-11
# Base Java Image for ForgeRock Identity Platform. Used for DS, amster, IGx and IDM (Anything that does not need a web
# container)
#
# Copyright 2019-2021 ForgeRock AS. All Rights Reserved
#
# Use of this code requires a commercial software license with ForgeRock AS.
# or with one of its affiliates. All use shall be exclusively subject
# to such license between the licensee and ForgeRock AS.
#
FROM openjdk:11-jdk-slim
RUN jlink --compress=2 \
          --no-header-files \
          --no-man-pages \
          --strip-debug \
          --add-modules java.base,java.compiler,java.desktop,java.instrument,java.management.rmi,java.prefs,java.security.jgss,\
java.security.sasl,java.sql,jdk.security.auth,jdk.unsupported,java.datatransfer,java.logging,java.naming,java.rmi,java.scripting,\
java.sql.rowset,java.transaction.xa,java.xml,java.xml.crypto,jdk.xml.dom,\
jdk.crypto.ec,jdk.crypto.cryptoki,jdk.jfr,jdk.jcmd,jdk.jdwp.agent,jdk.naming.dns,jdk.zipfs,jdk.management.agent \
          --output /opt/jdk \
   && cp /usr/local/openjdk-11/bin/jstack /opt/jdk/bin \
   && cp /usr/local/openjdk-11/bin/jps /opt/jdk/bin \
   && apt-get update && apt-get install -y binutils wget \
   && strip -p --strip-unneeded /opt/jdk/lib/server/libjvm.so

# This installs the GCP stack driver profiler. Adds approx 8MB
RUN mkdir -p /opt/cprof && \
     wget -q -O- https://storage.googleapis.com/cloud-profiler/java/latest/profiler_java_agent.tar.gz \
     | tar xzv -C /opt/cprof && \
     chmod 0555 /opt/cprof/*.so

# Installs the async-profiler - adds 280K
RUN mkdir -p /opt/async-profiler && \
    wget -q -O-  https://github.com/jvm-profiling-tools/async-profiler/releases/download/v1.5/async-profiler-1.5-linux-x64.tar.gz \
    | tar xzv -C /opt/async-profiler


FROM debian:buster-slim
# TODO: Upgrade to bullseye whehn pipeline is stable
# FROM debian:bullseye-slim


COPY --from=0 /opt /opt
COPY Dockerfile /Dockerfile.java-11

# Add in ca-certificates needed for uploads to google cloud storage.
RUN adduser --home "/home/forgerock" -uid 11111 --gid 0 forgerock --disabled-password --gecos "forgerock user" && \
   apt-get update && \
   apt-get -y upgrade && \
   apt-get install -y ca-certificates procps && \
   rm -rf /var/lib/apt/lists/*

ENV JAVA_HOME /opt/jdk

ENV PATH $PATH:/opt/jdk/bin

ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8

What does this Dockerfile do?

This Dockerfile uses the Docker builder pattern to build a base image for use with IG. The first FROM statement:

FROM openjdk:11-jdk-slim

Tells docker to use that image for the next three RUN commands. All of the output of those commands are stored in /opt

Next, we see:

FROM debian:buster-slim

The remaining commands will use this image. The first COPY command shows that we are copying over /opt from the first build image (the JRE, and profiling tools), The remaining commands perform administrative tasks such as adding a user and installing packages.

Steps to create your own production image

The first step is to determine what the source Docker images for the foundation of the new Base image will be.

For the purposes of this document we’ll want the following:

  1. JDK is Eclipse Temurin. From ForgeRocks IG release notes, it is one of the supported Java versions.
  2. The OS base image will be Debian Bullseye(11), which is a newer distribution than Buster(10). This image should have more up-to-date security fixes.

In order to build a production IG Docker image, we need to create a base image. Then we’ll use the newly created base image with the Dockerfile provided in the IG distribution to build the final production IG image:

  1. Build a base Java 11 image (requirements from 1,2 above)
  2. Build a new IG image using the new base Java 11 image.

Keep in mind that the decisions for the JDK and OS were made purely on my preferences and supportability, and to show an example of making your own production image. You could certainly pick your own JDK, and a different Linux distribution if desired.

To keep things organized, the base image will be tagged with the following: forgerock-temurin:11 The production IG image will be tagged with: ig-temurin:11

Step 1: Building the Base Image

In order to create the new base image, we need to modify the FROM statements to use different docker sources, and we need to adjust the paths slightly as the termurin image uses /opt/java/openjdk/bin instead of /usr/local/openjdk-bin.

Create the Dockerfile

Create a working directory:

$ mkdir ~/dev/ig/ig-baseimage
$ cd ~/dev/ig/ig-baseimage

Copy over the Dockerfile (from the Java-11 image) and make the following modifications:

FROM eclipse-temeruin:11-jammy
...
&& cp /opt/java/openjdk/bin/jstack /opt/jdk/bin \
   && cp /opt/java/openjdk/bin/jps /opt/jdk/bin \
...
FROM debian:bullseye-slim

After everything is done, the Dockerfile will look like the following:

# Base Java Image for ForgeRock Identity Platform. Used for DS, amster, IGx and IDM (Anything that does not need a web
# container)
#
# Copyright 2019-2021 ForgeRock AS. All Rights Reserved
#
# Use of this code requires a commercial software license with ForgeRock AS.
# or with one of its affiliates. All use shall be exclusively subject
# to such license between the licensee and ForgeRock AS.
#
FROM eclipse-temurin:11-jammy
RUN jlink --compress=2 \
          --no-header-files \
          --no-man-pages \
          --strip-debug \
          --add-modules java.base,java.compiler,java.desktop,java.instrument,java.management.rmi,java.prefs,java.security.jgss,\
java.security.sasl,java.sql,jdk.security.auth,jdk.unsupported,java.datatransfer,java.logging,java.naming,java.rmi,java.scripting,\
java.sql.rowset,java.transaction.xa,java.xml,java.xml.crypto,jdk.xml.dom,\
jdk.crypto.ec,jdk.crypto.cryptoki,jdk.jfr,jdk.jcmd,jdk.jdwp.agent,jdk.naming.dns,jdk.zipfs,jdk.management.agent \
          --output /opt/jdk \
   && cp /opt/java/openjdk/bin/jstack /opt/jdk/bin \
   && cp /opt/java/openjdk/bin/jps /opt/jdk/bin \
   && apt-get update && apt-get install -y binutils wget \
   && strip -p --strip-unneeded /opt/jdk/lib/server/libjvm.so

# This installs the GCP stack driver profiler. Adds approx 8MB
RUN mkdir -p /opt/cprof && \
     wget -q -O- https://storage.googleapis.com/cloud-profiler/java/latest/profiler_java_agent.tar.gz \
     | tar xzv -C /opt/cprof && \
     chmod 0555 /opt/cprof/*.so

# Installs the async-profiler - adds 280K
RUN mkdir -p /opt/async-profiler && \
    wget -q -O-  https://github.com/jvm-profiling-tools/async-profiler/releases/download/v1.5/async-profiler-1.5-linux-x64.tar.gz \
    | tar xzv -C /opt/async-profiler


#FROM debian:buster-slim
# TODO: Upgrade to bullseye whehn pipeline is stable
FROM debian:bullseye-slim


COPY --from=0 /opt /opt
COPY Dockerfile /Dockerfile.java-11

# Add in ca-certificates needed for uploads to google cloud storage.
RUN adduser --home "/home/forgerock" -uid 11111 --gid 0 forgerock --disabled-password --gecos "forgerock user" && \
   apt-get update && \
   apt-get -y upgrade && \
   apt-get install -y ca-certificates procps && \
   rm -rf /var/lib/apt/lists/*

ENV JAVA_HOME /opt/jdk

ENV PATH $PATH:/opt/jdk/bin

ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8

Build the new base

Now we use docker to build the new base image and tag it accordingly:

$ docker build -t forgerock-temurin:11 .

That command will build an image locally on your workstation.

Step 2: Build the IG Production Image

Now that we have a base image built, we can modify the IG Dockerfile to use this new base image.

$ cd ~/ig/
$ unzip IG-7.2.0.zip
$ cd identity-gateway

The complete Dockerfile (in the docker folder) will look like the following:

Modify the FROM directive in the provided Dockerfile to be: FROM forgerock-temurin:11

The complete Dockerfile (in the docker folder) will look like the following:

#
#  Copyright 2020-2022 ForgeRock AS. All Rights Reserved
#
#  Use of this code requires a commercial software license with ForgeRock AS.
#  or with one of its affiliates. All use shall be exclusively subject
#  to such license between the licensee and ForgeRock AS.
#
# =============================================================================================================
#  ForgeRock provides no commercial support for production deployments that use ForgeRock’s evaluation-only
#  Docker images.
#  When deploying the ForgeRock Identity Platform using Docker images, you must build and use your own images
#  for production deployments.
# =============================================================================================================
#
#FROM gcr.io/forgerock-io/java-11:latest
FROM forgerock-temurin:11
#
# Default install directory for IG. Override this to set a different location.
#
ENV INSTALL_DIR /opt/ig

COPY --chown=forgerock:root . "${INSTALL_DIR}"
#
# Default home for IG config. Override this to set a different location.
#
ENV IG_INSTANCE_DIR /var/ig

RUN mkdir -p "${IG_INSTANCE_DIR}" \
    && chown -R forgerock:root "${IG_INSTANCE_DIR}" "${INSTALL_DIR}" \
    && chmod -R g+rwx "${IG_INSTANCE_DIR}" "${INSTALL_DIR}"

USER 11111

ENTRYPOINT ${INSTALL_DIR}/bin/start.sh ${IG_INSTANCE_DIR}

And now, building from the identity-gateway folder (not the docker folder):

$ docker build -t ig-temurin:11 -f docker/Dockerfile .

Verification

Now that we’ve built the production IG image, we can do a quick “hello world” configuration to test that basic functionality works with IG.

Hello World IG Configuration

For the IG config, we’ll use a simple static response handler to return “Hello world” when the user navigates to /hello on the site

In your home directory, or another path of your choosing, create the IG configuration directories:

$ mkdir .openig
$ mkdir .openig/config
$ mkdir .openig/config/routes

# Create hello.json

hello.json will consist of the following:

{
  "name": "hello",
  "handler": {
    "type": "StaticResponseHandler",
    "config": {
      "status": 200,
      "headers": {
        "Content-Type": [ "text/plain; charset=UTF-8" ]
      },
      "entity": "Hello world!"
    }
  },
  "condition": "${find(request.uri.path, '^/hello')}"
}

Start IG

Now we can map this configuration to /var/ig in the docker image and test out the functionality.

The following docker command uses a volume mount to map the local configuration directories into /var/ig on the docker instance:

$ docker run -p 8080:8080 -v $HOME/.openig:/var/ig ig-temurin:11
[main] INFO  o.f.openig.launcher.Launcher @system - /var/ig/config/admin.json not readable, using default-admin.json
[main] INFO  o.f.openig.launcher.Launcher @system - Identity Gateway 7.2.0 (branch sustaining/7.2.x, revision ebc0d185224338da5e4c54ee247634b58d3a529a)
[main] INFO  o.f.openig.launcher.Launcher @system - Environment:
[main] INFO  o.f.openig.launcher.Launcher @system - - instance directory: /var/ig
[main] INFO  o.f.openig.launcher.Launcher @system - - temporary directory: /var/ig/tmp
[main] INFO  o.f.openig.launcher.Launcher @system - - PID file: /var/ig/tmp/ig.pid
[main] INFO  org.forgerock.api.models.Items @system - Asked for Items for annotated type, but type does not have required RequestHandler annotation. No api descriptor will be available for class org.forgerock.monitoring.DropwizardMetricsCollectionResourceProvider
[main] INFO  org.forgerock.api.models.Resource @system - Asked for Resource for annotated type, but type does not have required RequestHandler annotation. No api descriptor will be available for class org.forgerock.monitoring.DropwizardMetricsCollectionResourceProvider
[main] INFO  o.f.openig.launcher.Launcher @system - /var/ig/config/config.json not readable, using default-config.json
[main] INFO  o.f.o.handler.router.RouterHandler @system - Loaded the route with id 'hello' registered with the name 'hello'
[main] INFO  o.f.openig.launcher.Launcher @system - All 8 verticles started in 3808ms on ports : [8080]

If you do not see the output above, then something went wrong with building the IG image.

Make an HTTP request against IG

Using the curl command, make a HTTP GET request against IG:

$ curl -v http://localhost:8080/hello
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 12
< Content-Type: text/plain; charset=UTF-8
< X-Content-Type-Options: nosniff
<
* Connection #0 to host localhost left intact
Hello world!%

As you can see, “Hello world!” is returned.

Next Steps

If you are familiar with Docker, you’ll recognize that the builds are all local to the workstation. If you were doing production level development you’ll push these images and tag them according to your best practices. In practice, you might want to tag these images with datestamps to better track when they were built.

Quick Links

3 Likes