Latest Elastic APM Agent in Docker



How to automatically download and use the latest version of the Elastic APM Agent in your Java application Docker image.

Conclusion First

# https://olof.tech/latest-elastic-apm-agent-in-docker/
FROM alpine:latest AS builder-elastic-apm-agent-downloader
RUN apk --no-cache add curl jq
RUN ELASTIC_APM_AGENT_VERSION=`curl --silent --fail --show-error "https://search.maven.org/solrsearch/select?q=g:co.elastic.apm+AND+a:elastic-apm-agent&start=0&rows=20" | jq --raw-output --exit-status ".response | .docs[0] | .latestVersion"` \
    && echo "Elastic APM Agent Version: ${ELASTIC_APM_AGENT_VERSION}" \
    && curl --silent --fail --show-error "https://repo1.maven.org/maven2/co/elastic/apm/elastic-apm-agent/${ELASTIC_APM_AGENT_VERSION}/elastic-apm-agent-${ELASTIC_APM_AGENT_VERSION}.jar" -o /elastic-apm-agent.jar

FROM eclipse-temurin:11
RUN mkdir /opt/app
COPY app.jar /opt/app/app.jar
COPY --from=builder-elastic-apm-agent-downloader /elastic-apm-agent.jar /
ENTRYPOINT java -javaagent:/elastic-apm-agent.jar -jar /opt/app/app.jar

Hardcoding is Simple

Adding the Elastic APM Agent to a Java application Dockerfile is this simple if you just hardcode the version:

FROM eclipse-temurin:11

RUN mkdir /opt/app
COPY app.jar /opt/app/app.jar

ARG ELASTIC_APM_AGENT_VERSION="1.34.0"
RUN curl --silent --fail --show-error "https://repo1.maven.org/maven2/co/elastic/apm/elastic-apm-agent/${ELASTIC_APM_AGENT_VERSION}/elastic-apm-agent-${ELASTIC_APM_AGENT_VERSION}.jar" -o /elastic-apm-agent.jar

ENTRYPOINT java -javaagent:/elastic-apm-agent.jar -jar /opt/app/app.jar

The Latest Version?

What if you always want to download the very latest version?

According to the Elastic documentation there is no correlation between the version of the APM agent and the Elastic stack. That means you can update the elastic-apm-agent.jar to latest version without updating your Elastic stack.

I suggest always using the latest version to benefit from instrumentation improvements and security patches. In fact the Elastic APM Agent at one point contained the Log4Shell vulnerability 🐛. Staying up to date can some times be important for real 😱.

Sure, the benefit of just hardcoding is you know exactly which version you are running. But will you really remember to keep it updated? New versions of the elastic-apm-agent.jar are frequently released. It was released 16 times during 2021 alone.

Quick and Dirty

Here's a quick and dirty solution you may like:

FROM eclipse-temurin:11

RUN mkdir /opt/app
COPY app.jar /opt/app/app.jar

RUN ELASTIC_APM_AGENT_VERSION=`curl --silent --fail --show-error "https://search.maven.org/solrsearch/select?q=g:co.elastic.apm+AND+a:elastic-apm-agent&start=0&rows=20" | grep -Po 'latestVersion.:.\K[^"]*'` \
    && curl --silent --fail --show-error "https://repo1.maven.org/maven2/co/elastic/apm/elastic-apm-agent/${ELASTIC_APM_AGENT_VERSION}/elastic-apm-agent-${ELASTIC_APM_AGENT_VERSION}.jar" -o /elastic-apm-agent.jar

ENTRYPOINT java -javaagent:/elastic-apm-agent.jar -jar /opt/app/app.jar

A downside with this solution is that curl must be installed on the base image.

The grep -Po 'latestVersion.:.\K[^"]*' is a neat trick used to parse the version from the JSON returned. Credit goes to https://kthoms.wordpress.com/2019/03/06/get-the-latest-artifact-version-from-maven-central-via-shell-command/. It does not work with macOS favored grep however, making it hard to debug. Perhaps using jq would be better?

Docker Multi-Stage Build

I propose using a Docker multi-stage build 🐳🐳. This way we can ensure access to useful commands such as curl and jq without bloating the final image size. It also decouples the download logic really well. Once we get it right it will work for any base image.

We can leverage the jq flag --exit-status to make the Dockerfile build fail if the jq-query does not yield a result.

We can also add an echo with the version number into the mix:

# https://olof.tech/latest-elastic-apm-agent-in-docker/
FROM alpine:latest AS builder-elastic-apm-agent-downloader
RUN apk --no-cache add curl jq
RUN ELASTIC_APM_AGENT_VERSION=`curl --silent --fail --show-error "https://search.maven.org/solrsearch/select?q=g:co.elastic.apm+AND+a:elastic-apm-agent&start=0&rows=20" | jq --raw-output --exit-status ".response | .docs[0] | .latestVersion"` \
    && echo "Elastic APM Agent Version: ${ELASTIC_APM_AGENT_VERSION}" \
    && curl --silent --fail --show-error "https://repo1.maven.org/maven2/co/elastic/apm/elastic-apm-agent/${ELASTIC_APM_AGENT_VERSION}/elastic-apm-agent-${ELASTIC_APM_AGENT_VERSION}.jar" -o /elastic-apm-agent.jar

FROM eclipse-temurin:11
RUN mkdir /opt/app
COPY app.jar /opt/app/app.jar
COPY --from=builder-elastic-apm-agent-downloader /elastic-apm-agent.jar /
ENTRYPOINT java -javaagent:/elastic-apm-agent.jar -jar /opt/app/app.jar

Try it Yourself

If you have Docker installed on your system you can try this solution out yourself right now.

You don't even need a Dockerfile. Just execute these commands:

# Start and enter a temporary Alpine container
docker run --rm -it --entrypoint /bin/sh alpine:latest

# Install curl and jq
apk --no-cache add curl jq

# Download the latest elastic-apm-agent.jar
ELASTIC_APM_AGENT_VERSION=`curl --silent --fail --show-error "https://search.maven.org/solrsearch/select?q=g:co.elastic.apm+AND+a:elastic-apm-agent&start=0&rows=20" | jq --raw-output --exit-status ".response | .docs[0] | .latestVersion"` \
    && echo "Elastic APM Agent Version: ${ELASTIC_APM_AGENT_VERSION}" \
    && curl --silent --fail --show-error "https://repo1.maven.org/maven2/co/elastic/apm/elastic-apm-agent/${ELASTIC_APM_AGENT_VERSION}/elastic-apm-agent-${ELASTIC_APM_AGENT_VERSION}.jar" -o /elastic-apm-agent.jar

# Check that it's there
cd /
ls -ahl

# Exit the temporary Alpine container
exit

Or alternatively you can create this Dockerfile:

# https://olof.tech/latest-elastic-apm-agent-in-docker/
FROM alpine:latest AS builder-elastic-apm-agent-downloader
RUN apk --no-cache add curl jq
RUN ELASTIC_APM_AGENT_VERSION=`curl --silent --fail --show-error "https://search.maven.org/solrsearch/select?q=g:co.elastic.apm+AND+a:elastic-apm-agent&start=0&rows=20" | jq --raw-output --exit-status ".response | .docs[0] | .latestVersion"` \
    && echo "Elastic APM Agent Version: ${ELASTIC_APM_AGENT_VERSION}" \
    && curl --silent --fail --show-error "https://repo1.maven.org/maven2/co/elastic/apm/elastic-apm-agent/${ELASTIC_APM_AGENT_VERSION}/elastic-apm-agent-${ELASTIC_APM_AGENT_VERSION}.jar" -o /elastic-apm-agent.jar

FROM eclipse-temurin:11
COPY --from=builder-elastic-apm-agent-downloader /elastic-apm-agent.jar /
ENTRYPOINT echo "Hello World!"

Then test it by executing these commands:

# Build the Dockerfile with the tag "test"
docker build . -t test

# Start and enter a temporary container
docker run --rm -it --entrypoint /bin/sh test

# Check that it's there
cd /
ls -ahl

# Exit the temporary Alpine container
exit
Screenshot of Terminal Window when testing

Also Keep in Mind

Running apps in Docker as root is bad practice. It's common to create another user such as appuser instead. The /elastic-apm-agent.jar must be owned by the same user as the app.jar for attaching it as an agent to work. You may have to add --chown=appuser:appuser like this:

COPY --from=builder-elastic-apm-agent-downloader --chown=appuser:appuser /elastic-apm-agent.jar /