Running Business Containers as Non-Root: Practical Guide and Real-World Scripts
This article explains why running business containers without root privileges is essential for security, outlines the necessary background and risks, and provides detailed step‑by‑step methods, Dockerfile snippets, entrypoint scripts, and real‑world examples for MySQL, Redis, CoreDNS, Consul, and cAdvisor to achieve safe non‑root container deployments.
Background
Customer security requirements demand that business containers run without root privileges. Many containers need to manipulate ipset, iptables, and other system resources, which cannot be solved by plain rootless Docker; therefore existing images must be modified so that all processes inside run as non‑root.
Prerequisites
Why root is unsafe
Although Linux provides user namespaces, Docker does not support per‑container UID mappings like Podman, and containers can still modify mounted filesystems. A careless rm -rf /* in an Alpine container would delete the entire host filesystem.
docker run --rm -v /mnt/sda1:/mnt/sda1 -it alpine
cp /mnt/sda1/somefile.tar.gz .
tar xzvf somefile.tar.gz
cd somefile-v1.0
# inspect contents, then
cd ..
rm -rf *Choosing USER vs entrypoint script
For simple processes (exporters, HTTP APIs) you can set USER in the Dockerfile or use -u user:group at run time. Examples include:
danielqsj/kafka_exporter
ClickHouse/clickhouse_exporter
kubernetes addonresizer
For containers that persist data (MySQL, Redis) you must adjust directory ownership before the container starts, because the UID/GID inside the container differs from the host.
Mount with -v or Docker volume
Use hostPath in Kubernetes
Fixed PV
PVC in a storage class
Deploy to another Kubernetes cluster
Practical Cases
MySQL
The official MySQL image creates a dedicated mysql user and starts with ENTRYPOINT CMD. Example entrypoint: docker-entrypoint.sh mysqld You can also change the listening port via command line, e.g. docker run ... mysql:5.7 --port 4306.
Redis
Example startup script that switches to a non‑root user with gosu (or su‑exec on Alpine) and fixes permissions.
#!/bin/sh
set -e
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
set -- redis-server "$@"
fi
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
find . \! -user redis -exec chown redis '{}' +
exec gosu redis "$0" "$@"
fi
# set appropriate umask
um=$(umask)
if [ "$um" = '0022' ]; then
umask 0077
fi
exec "$@"CoreDNS
CoreDNS 1.11.0 supports non‑root, but the project uses 1.10.1. A custom Dockerfile rebuilds the binary, sets the capability cap_net_bind_service=+ep, and runs as nonroot:
ARG DEBIAN_IMAGE=debian:stable-slim
ARG BASE=gcr.io/distroless/static-debian12:nonroot
FROM coredns/coredns:1.10.1 as bin
FROM ${DEBIAN_IMAGE} AS build
SHELL ["/bin/sh", "-ec"]
RUN export DEBCONF_NONINTERACTIVE_SEEN=true \
DEBIAN_FRONTEND=noninteractive \
DEBIAN_PRIORITY=critical TERM=linux ; \
apt-get -qq update ; \
apt-get -yyqq upgrade ; \
apt-get -yyqq install ca-certificates libcap2-bin ; \
apt-get clean
COPY --from=bin /coredns /coredns
RUN setcap cap_net_bind_service=+ep /coredns
FROM ${BASE}
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /coredns /coredns
USER nonroot:nonroot
EXPOSE 53 53/udp
ENTRYPOINT ["/coredns"]Building must use BuildKit (e.g.
DOCKER_BUILDKIT=1 docker build --platform=amd64 . -t coredns/coredns:1.10.1 --load) otherwise the capability is lost.
Consul
Modify the official entrypoint to use recursive chown and drop dumb‑init so that PID 1 runs as non‑root:
FROM consul:${VER}
RUN sed -ri -e 's/(chown)(\s+consul:)/\1 -R\2/' \
-e '1s@/usr/bin/dumb-init\s+@@' /usr/local/bin/docker-entrypoint.shcAdvisor
To run cAdvisor without root, add a custom entrypoint that changes ownership of the Docker socket, adds the non‑root user to the socket’s group, and then executes cAdvisor with su‑exec or gosu:
#!/bin/sh
set -e
[ -z "$D_SOCK" ] && D_SOCK=/var/run/docker.sock
if [ "${1:0:1}" = '-' ]; then
set -- cadvisor "$@"
fi
if [ "$1" = 'cadvisor' ]; then
if [ "$(id -u)" = '0' -a -n "$RUN_USER" ]; then
if [ -S "$D_SOCK" ]; then
group_id=$(stat -c "%g" "$D_SOCK")
if ! getent group | cut -d: -f3 | grep -wq $group_id; then
addgroup -g $group_id docker
fi
group_name=$(stat -c "%G" "$D_SOCK")
if ! id -nG $RUN_USER | grep -w $group_name; then
adduser $RUN_USER $group_name
fi
fi
exec su-exec $RUN_USER "$@"
fi
fi
exec "$@"Additional notes
Place pid and socket files under /tmp.
Grant write permission to /dev/std* if the business process needs it (e.g., chmod a+w /dev/std*).
Keep consistent uid:gid across custom images and official images to avoid permission problems on mounted data directories.
Avoid chmod -R 777 on data directories.
Non‑root processes cannot bind ports below 1024 without capabilities; use setcap for services like CoreDNS.
When mounting /var/run/docker.sock, ensure the non‑root user belongs to the socket’s group (often GID 660).
Cron cannot run as non‑root; use go-crond as an alternative.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Liangxu Linux
Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
