Skip to content

How to extend CMD and ENTRYPOINT from a base Docker image

Published:

In this blog post we will find out how to “extend” or “inherit” the CMD and ENTRYPOINT commands of an existing docker image.

You might have come across a situation where you need to extend a base docker image (parent) with additional commands that need to execute at startup, or potentially run a background process. For example, let’s assume you have been provided with a custom image from a third party, and in addition you need to have a daemon process running in parallel. You don’t have access to the original Dockerfile or the build pipeline, and you certainly don’t want to rewrite it from scratch.

Table of Contents

Possible Solutions

It is important to remember that only the last CMD and ENTRYPOINT command is effective in a Dockerfile. All previous declarations are ignored.

Option 1: Writing new CMD/ENTRYPOINT commands

One option is to overwrite these in your Dockerfile altogether. We first need to find out what the parent commands do, and then write a new script to do what we need along with the original purpose.

Option 2: Modifying CMD/ENTRYPOINT scripts

If the parent commands are already using some script, such as CMD ["./startup.sh"] or similar, then we could edit startup.sh in-place to do what we need, without overwriting the base image commands.

Inspecting the Base Image

In any case, the first step is to find out how CMD or ENTRYPOINT are configured in the base image.

Let’s assume we want to spin up a Tomcat server and add some logging instrumentation to it. We will use Filebeat to send Tomcat logs to Logstash. Filebeat is a log shipper utility, which we will run in the background alongside Tomcat. We will pull and use tomcat:latest as our base image for this example.

docker pull tomcat:latest

The inspect command will print low-level information on the target image. In the truncated output below, we can see the configuration we are looking for.

docker inspect tomcat:latest
...
"Cmd": ["catalina.sh", "run"],
"Entrypoint": null,
...

Alternatively, the history command will print out an output closer to the original Dockerfile. If the output is truncated, use the flag --no-trunc. The output is sorted in reverse definition order.

docker history tomcat:latest
IMAGE          CREATED       CREATED BY                  SIZE  COMMENT
228690642041   3 weeks ago   CMD ["catalina.sh" "run"]   0B    buildkit.dockerfile.v0
<missing>      3 weeks ago   ENTRYPOINT []               0B    buildkit.dockerfile.v0
...

Writing a New Startup Script

By inspecting the base image we found out that we need to run catalina.sh run in order to start up Tomcat. One approach would be to modify catalina.sh directly, by editing it in inside the Dockerfile. However, in this case it is easier and more maintainable to create our own startup script, called bootstrap.sh as shown below.

In this script we first start up filebeat in the background and finally execute exec catalina.sh run as originally intended. Note that exec was added so that the Tomcat process takes over and is able to receive signals (such as Ctrl+C).

#!/bin/sh
set -e

echo "Checking filebeat config..."
./filebeat/filebeat test config

echo "Starting filebeat in the background..."
nohup ./filebeat/filebeat &

echo "Starting Tomcat..."
exec catalina.sh run

This script is then used in CMD ["./bootstrap.sh"] below, the last command of our Dockerfile, which uses tomcat:latest as base. The rest of the commands take care of installing filebeat and copying necessary files.

FROM tomcat:latest

# Install filebeat
ARG FILEBEAT_VERSION=8.15.3
ARG FILEBEAT=filebeat-${FILEBEAT_VERSION}-linux-x86_64
RUN curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/${FILEBEAT}.tar.gz && \
   tar xzf ${FILEBEAT}.tar.gz && \
   rm ${FILEBEAT}.tar.gz && \
   mv ${FILEBEAT} filebeat

# Copy filebeat configuration
COPY filebeat.yml .

# Copy bootstrap script and overwrite CMD from base image
COPY --chmod=0755 bootstrap.sh .
CMD ["./bootstrap.sh"]

For completeness, filebeat.yml is a simple configuration file for Filebeat that reads logs from /usr/local/tomcat/*.log and sends them over to our defined Logstash instance.

filebeat.inputs:
- type: filestream
  id: TomcatLogs
  paths:
   - /usr/local/tomcat/*.log

output.logstash:
 hosts: ["127.0.0.1:5044"]

Building & Running

We can finally build our image. The Filebeat version can be dynamically controlled by passing a --build-arg to the command and override the hardcoded version.

docker build -t tomcat-filebeat:latest .
docker build --build-arg FILEBEAT_VERSION=8.15.0 -t tomcat-filebeat:latest .

When we run the image, the logs indicate that our startup sequence worked successfully.

docker run tomcat-filebeat:latest
Checking filebeat config...
Config OK
Starting filebeat in the background...
Starting Tomcat...
02-Nov-2024 15:13:38.386 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name:   Apache Tomcat/11.0.0
...

To verify that filebeat is actually running, we can connect to the live container and check the running processes. The following command will list running containers.

docker ps
CONTAINER ID   IMAGE                    COMMAND            CREATED          STATUS          PORTS      NAMES
5409b3b1df83   tomcat-filebeat:latest   "./bootstrap.sh"   25 minutes ago   Up 25 minutes   8080/tcp   mystifying_einstein

To start a shell, we use the container id as follows. Then, we can use a command like top to check the running processes (Or run directly top instead of sh).

docker exec -it 5409b3b1df83 sh

When you are finished with this example, you can remove unused docker images and volumes to release space using the following prune command. The flag --all will remove all unused images and --volumes will prune anonymous volumes.

docker system prune --all --volumes