Non-Root Docker Update Summary

I haven’t got time to learn Docker systematically so far, but I still gain a lot from daily tasks. This post is a brief summary for what I have done to upgrade the container from owned by root to noon-root for security reason required by our customers.

I will talk the development workflow instead of the detail about how to set and modify the configurations inside the container:

I have a base image at beginning, let’s say root_is_engine.tar.gz, load it:

1
docker load -i root_is_engine.tar.gz

you can check the loaded image by running:

1
docker images

Now I am going to create the container from this image, but wait, I don’t want the container to run any script or program when it starts, what I need is it just hangs without doing anything.

That means I need to override the default entrypoint (entrypoint sets the command and parameters that will be executed first when spin up a container) in docker run command:

1
2
3
4
5
6
7
8
9
docker run --detach \
--cap-add=SYS_ADMIN \
--privileged=false --name=${ENGINE_HOST} --hostname=${ENGINE_HOST} \
--restart=always --add-host="${SERVICES_HOST} ${DB2_XMETA_HOST} ${ENGINE_HOST}":${ENGINE_HOST_IP} \
-p 8449:8449 \
-v ${DEDICATED_ENGINE_VOLPATH}/${ENGINE_HOST}/EngineClients/db2_client/dsadm:/home/dsadm \
--entrypoint=/bin/sh \
${DOCKER_IMAGE_TAG_ENGINE}:${DOCKER_IMAGE_VERSION} \
-c 'tail -f /dev/null'

Note that place the arguments to your entrypoint at the end of your docker command -c 'tail -f /dev/null'

Run docker ps command, you can see under COMMAND column the entrypoint is what I specified:

1
2
CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS                     NAMES
b462f6123684 is-engine-image:1 "/bin/sh -c 'tail -f..." 2 days ago Up 2 days 0.0.0.0:8449->8449/tcp is-en-conductor-0.en-cond

Then get into the container by running:

1
docker exec -it <container id or container name> [bash|sh]

Then if you check the process status, you can see the init process with PID #1 is running tail command to track /dev/null device file:

1
2
3
4
5
6
ps aux

USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 5968 616 ? Ss 16:47 0:00 tail -f /dev/null
root 27 1.5 0.0 13420 1992 pts/0 Ss 16:50 0:00 bash
root 45 0.0 0.0 53340 1864 pts/0 R+ 16:50 0:00 ps aux

OK, now I can make changes, for example, create and switch to ordinary user with specified user id and group id, grant them privileges, modify the owner and permission of some files, run and update startup script line by line to see if the applications setup correctly with non-root.

Note if you have mount path in host machine, you may need to chown correct uid and gid, otherwise ordinary user in container may get permission denied issue.

After running some tests and they succeed, I need to commit the changes into a new image.

  • First understand how does docker commit work? what will be committed into new image?

    The container has a writable layer that stacks on top of the image layers. This writable layer allows you to make changes to the container since the lower layers in the image are read-only.

    From the docker documentation, it said: It can be useful to commit a container’s file changes or settings into a new image.

    The commit operation will not include any data contained in volumes mounted inside the container.

    By default, the container being committed and its processes will be paused while the image is committed. This reduces the likelihood of encountering data corruption during the process of creating the commit. If this behavior is undesired, set the --pause option to false.

    The --change option will apply Dockerfile instructions to the image that is created. Supported Dockerfile instructions: CMD|ENTRYPOINT|ENV|EXPOSE|LABEL|ONBUILD|USER|VOLUME|WORKDIR

  • How about processes in the running container?

    When you start container from image then process will start here - processes exists only in executing container, when container stops there are no processes anymore - only files from container’s filesystem.

Note that before committing, you need to quiesce the services and remove the mount path content to unlink all broken symbolic links.

Also remember to put the old entrypoint back:

1
2
3
docker commit \
--change 'ENTRYPOINT ["/bin/bash", "-c", "/opt/xx/initScripts/startcontainer.sh"]' \
<container ID> is-engine-image:1

Note that podman commit format is different.

Note that you may use bin/sh instead of /bin/bash.

OK, now start to run the new image with non-root user:

1
2
3
4
5
6
7
8
docker run --detach \
--user 1000 \
--cap-add=SYS_ADMIN \
--privileged=false --name=${ENGINE_HOST} --hostname=${ENGINE_HOST} \
--restart=always --add-host="${SERVICES_HOST} ${DB2_XMETA_HOST} ${ENGINE_HOST}":${ENGINE_HOST_IP} \
-p 8449:8449 \
-v ${DEDICATED_ENGINE_VOLPATH}/${ENGINE_HOST}/EngineClients/db2_client/dsadm:/home/dsadm \
${DOCKER_IMAGE_TAG_ENGINE}:${DOCKER_IMAGE_VERSION}

Let’s see the processes in the non-root container:

1
2
3
4
5
6
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
dsadm 1 0.0 0.0 13288 1604 ? Ss 18:29 0:00 /bin/bash /opt/xx/initScripts/startcontainer.sh
dsadm 540 0.0 0.0 5968 620 ? S 18:29 0:00 tail -f /dev/null
dsadm 568 0.1 0.3 2309632 24792 ? Sl 18:29 0:00 /opt/xx/../../jdk
dsadm 589 2.5 0.0 13420 2012 pts/0 Ss 18:36 0:00 bash
dsadm 610 0.0 0.0 53340 1868 pts/0 R+ 18:36 0:00 ps aux

If all things are in good shape, save the image into tar.gz format, of course you can use a new tag before saving:

1
docker save is-engine-image:1 | gzip > ~/nonroot_is_engine.tar.gz

Note that there is a gzip to compress the image.

0%