Baeldung Pro – Ops – NPI EA (cat = Baeldung on Ops)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

Partner – Orkes – NPI EA (cat=Kubernetes)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

1. Overview

In this tutorial, we’ll discuss adding or modifying the content of a file within a Docker image. There are three different ways of doing so: Generating an image from a modified container, modifying the file in place from the Dockerfile, and copying the file directly while building the image.

To illustrate the different cases, we’ll use the /etc/sysctl.conf file as the destination for the configuration option: kernel.core_uses_pid = 1. This is a useful setting for debugging multi-threaded applications; here it’s used as an example.

2. Modify the Container Content and Generate a New Image

The first approach uses docker container commit. With the docker container commit command, we’ll generate an image based on an existing container.

Let’s start with the following Dockerfile:

$ cat Dockerfile
FROM alpine:latest

CMD ["sh"]

Next, we generate a base image from the Dockerfile with the docker build command:

$ docker build --tag image/base .

Docker is now aware of our image, as seen with docker image ls:

$ docker image ls 
REPOSITORY   TAG      IMAGE ID        CREATED        SIZE 
image/base   latest   62220e58e6c2    2 weeks ago    8.31MB

Then, it’s docker run to create a container based on image/base:

$ docker run -td image/base
1c7771ce9e2a38c9c80c420689833738c6601a04ce5536d73c090868c015f327

We’ve merged the -t option (equivalent to –tty) to allocate a pseudo-TTY, and the -d option (equivalent to –detach) to run the container in the background. The hexadecimal output is the container ID.

It’s important not to add the common –rm option since this will automatically remove the container and volumes when exiting. We need to keep the container to be able to generate an image from it. In fact, let’s check that our container exists with docker container ps:

$ docker container ps -a 
CONTAINER ID   IMAGE      COMMAND    CREATED        STATUS           PORTS   NAMES 
1c7771ce9e2a   image/base "sh"       10 seconds ago Up 10 seconds            sleepy_vaughan

Now we need to add the content we want to the file:

$ docker exec -it 1c7771ce9e2a /bin/sh -c "cat /etc/sysctl.conf"
$ docker exec -it 1c7771ce9e2a /bin/sh -c "echo 'kernel.core_uses_pid = 1' >> /etc/sysctl.conf"
$ docker exec -it 1c7771ce9e2a /bin/sh -c "cat /etc/sysctl.conf"
kernel.core_uses_pid = 1

We have used the -i option to keep the STDIN attached and the -t option as before. We provide the ID of the container and use /bin/sh with the command we want to run after the -c option, surrounded by quotes.

Regarding the commands, the first one checks with cat that the /etc/sysctl.conf file is empty. This file may contain some commented lines based on the distribution used. Then, the echo command appends the text “kernel.core_uses_pid = 1” into the /etc/sysctl.conf file using pipe redirection. Lastly, cat ensures that the content of the file includes the new line we’ve added.

All these operations can be run interactively as well by accessing the shell in the container:

$ docker exec -it 1c7771ce9e2a /bin/sh
/# cat /etc/sysctl.conf
/# echo 'kernel.core_uses_pid = 1' > /etc/sysctl.conf
/# cat /etc/sysctl.conf
kernel.core_uses_pid = 1

Once we have a container with our modified content, we generate a new image with its ID and the image name we want:

$ docker commit 1c7771ce9e2a image/mod
sha256:9c57163be3f6402fe95b781468f1b18b5862deefacf9b54ce3ae7c20b56c8706

If we list the images with docker image ls, we’ll have the original image/base and the newly created image/mod:

$ docker image ls 
REPOSITORY   TAG      IMAGE ID        CREATED        SIZE 
image/mod    latest   9c57163be3f6    5 seconds ago  8.31MB 
image/base   latest   62220e58e6c2    2 weeks ago    8.31MB

Following our same steps as before reveals that our new image has the file already altered:

$ docker run -td image/mod
b7273b7cf3ca24eba1d44c2324179bc15053115ec6bde3d8edf2bd63720bdaa5
$ docker exec -it b7273b7cf3ca /bin/sh -c "cat /etc/sysctl.conf"
kernel.core_uses_pid = 1

3. Modify the File via the Dockerfile

Once we’re happy with our previous procedure, let’s automate it for repeatability. The Dockerfile can also contain some text-editing actions inside it.

3.1. Basic Procedure to Append a New Line

Starting with a template from our previous Dockerfile, we use the RUN instruction to execute shell commands:

$ cat Dockerfile
FROM alpine:latest 

RUN echo "kernel.core_uses_pid = 1" > /etc/sysctl.conf 

CMD ["sh"]

Afterwards, the docker build command generates an image based on this modified Dockerfile:

$ docker build --tag image/mod .
[+] Building 0.9s (6/6) FINISHED                                                  docker:default
=> [internal] load build definition from Dockerfile                               0.0s
=> => transferring dockerfile: 127B                                               0.0s
=> [internal] load metadata for docker.io/library/alpine:latest                   0.4s
=> [internal] load .dockerignore                                                  0.0s
=> => transferring context: 2B                                                    0.0s
=> CACHED [1/2] FROM docker.io/library/alpine:latest@sha256:8a1f59ffb675680d47d   0.0s
=> [2/2] RUN echo "kernel.core_uses_pid = 1" >> /etc/sysctl.conf                  0.3s
=> exporting to image                                                             0.0s
=> => exporting layers                                                            0.0s
=> => writing image sha256:83ddfd5f059a7a92d250dfaa11df14dfb42375a9753be1f271f9   0.0s
=> => naming to docker.io/image/mod                                               0.0s

docker image will list the images available in our system:

$ docker image ls 
REPOSITORY   TAG      IMAGE ID        CREATED        SIZE 
image/mod    latest   83ddfd5f059a    4 seconds ago  8.31MB

We now create a container with the image from the modified Dockerfile and inspect the /etc/sysctl.conf file with cat as done before:

$ docker run -td image/mod
d610219984dc30fa79fb0ba3a7724b13a1e2d643e459451c2c48b4713d1c9362
$ docker exec -it d610219984dc /bin/sh -c "cat /etc/sysctl.conf"
kernel.core_uses_pid = 1

The file contains the new kernel option, which was not part of the original file.

3.2. Other Operations

In the previous example, we added a single line to a file. However, many modifications will entail adding several lines to a file. Simply concatenating several RUN instructions will achieve this:

RUN echo "line1" >> /etc/sysctl.conf 
RUN echo "line2" >> /etc/sysctl.conf 
RUN echo "line3" >> /etc/sysctl.conf 
RUN echo "line4" >> /etc/sysctl.conf 

More complex operations are also possible. Let’s assume we want to replace some original_text in the /etc/systctl.conf file with a new_text. sed performs more complex operations from within the Dockerfile:

RUN sed -i 's/original_text/new_text/g' /etc/sysctl.conf

In this case, we’re using -i to perform the edition inline and avoid having temporary files. The leading s requests substitution, and the trailing g is to replace occurrences in the whole file.

4. Copying the File Directly

Imagine we need to make multiple changes to a file or iterate on them. However, we want to take advantage of the perks that modern IDEs have.

We’ll make the changes in the files within our system and then transfer the file using the COPY command in the Dockerfile.

Let’s start with a custom sysctl.conf in our system already modified in our IDE:

$ cat custom_sysctl.conf 
kernel.core_uses_pid = 1

Next, we add the COPY instruction in the Dockerfile to transfer the file to the image:

$ cat Dockerfile 
FROM alpine:latest 

COPY custom_sysctl.conf /etc/sysctl.conf 

CMD ["sh"]

As before, the next step is to build the image based on the Dockerfile:

$ docker build --tag image/mod .
.[+] Building 0.5s (7/7) FINISHED                                                      docker:default
=> [internal] load build definition from Dockerfile                                    0.0s
=> => transferring dockerfile: 110B                                                    0.0s
=> [internal] load metadata for docker.io/library/alpine:latest                        0.4s
=> [internal] load .dockerignore                                                       0.0s
=> => transferring context: 2B                                                         0.0s
=> [internal] load build context                                                       0.0s
=> => transferring context: 39B                                                        0.0s
=> [1/2] FROM docker.io/library/alpine:latest@sha256:8a1f59ffb675680d47db6337b4        0.0s
=> CACHED [2/2] COPY custom_sysctl.conf /etc/sysctl.conf 0.0s=> exporting to image     0.0s
=> => exporting layers                                                                 0.0s
=> => writing image sha256:6982185cb145573f62b1656fbe3c90cb9d110fa820596adc245b        0.0s
=> => naming to docker.io/image/mod                                                    0.0s
$ docker image ls 
REPOSITORY   TAG      IMAGE ID        CREATED         SIZE 
image/mod    latest   6982185cb145    15 seconds ago  8.31MB 

Finally, we run a container and inspect the file that we copied to ensure it’s right:

$ docker run -td image/mod
a2aa891cd796b48eefd15176b1d2e165995d646aa50f035c29ca9a5422ae8408
$ docker exec -it a2aa891cd796 /bin/sh -c "cat /etc/sysctl.conf"
kernel.core_uses_pid = 1

This is a convenient solution that allows us to interactively edit a file and then copy it to the destination path.

5. Conclusion

In this article, we discussed three solutions for editing specific files and incorporating them into our Docker image.

In the first solution, we modified the files inside a Docker container and generated an image from it using the docker container commit command.

For the second solution, we used RUN inside the Dockerfile to execute shell commands that manipulated the file for us.

As the last solution, we edited the file in our system in our favorite IDE and copied it via the COPY instruction in a Dockerfile.