Cloud native in-depth analysis of docker utility tools gosu and su-exec practice

1. Volume permission issues

  • In Docker, when the host directory needs to be mounted into a container to be used as a volume, file permission issues often occur. A common phenomenon is that the container does not have write permissions on the path, causing all kinds of strange problems in the services there.
  • The reason for this type of problem is that the UIDs inside and outside the container are different. For example, the UID of the user currently using docker on the host is 1000 (this is the UID of the default first user). If the UID in the container is 2000, then the directory created by the host is not the owner of the container and cannot be written by default. .
  • In addition, there is another situation, that is, the mounted directory does not exist on the host before mounting. Docker will first create the directory with root permissions and then mount it. As a result, even if the UIDs of host and container are both 1000, there will be no write permission. This phenomenon only occurs during initialization, but it is enough to confuse novices and bore veterans.
  • Why can't the volume permissions be configured properly in the Dockerfile? Because Dockerfile is a description of image, and volume is the content of container. The permission configuration made in the Dockerfile can take effect for non-volumes, but not for volumes. Essentially, the directory mounted by the host to the volume belongs to the host. Dockerfile is executed during docker build, and volume is generated when docker run.
  • In fact, when Docker automatically creates the volume path, it should automatically modify it to the user:group of the foreground process in the container. However, Docker currently does not have such a mechanism, so users like us can only find another way out.
  • The general temporary solution is to manually modify permissions. Either use chown to change the owner to the UID of the user in the container; or use chmod 777 to make it common to all users. These are certainly not good long-term solutions, and they violate the original intention of Docker to facilitate deployment.
  • At present, it seems that the best solution is to customize the ENTRYPOINT of Dockerfile.

2. ENTRYPOINT

  • ENTRYPOINT has the following key points:
    • ENTRYPOINT specifies the default entry command of the image. This entry command will be executed as the root command when starting the container, and all other incoming values ​​are used as parameters of the command.
    • The value of ENTRYPOINT can be overwritten by docker run --entrypoint.
    • Only the last ENTRYPOINT directive in the Dockerfile will work.
  • When ENTRYPOINT is specified, the meaning of CMD changes. Instead of running its command directly, the content of CMD is passed as a parameter to the ENTRYPOINT instruction. In other words, when actually executed, it will become "".
  • Therefore, write an entry script entrypoint.sh or docker-entrypoint.sh in ENTRYPOINT in the dockerfile. Use ENTRYPOINT to perform some operations when the container is running, such as changing the permissions of the directory where the volume is mounted correctly, and then switching to a normal user to run a normal program process.

3. gosu sum su-exec

  • gosu's github warehouse address:
https://github.com/tianon/gosu
  • usage:
$ gosu
Usage: ./gosu user-spec command [args]
   eg: ./gosu tianon bash
       ./gosu nobody:root bash -c 'whoami && id'
       ./gosu 1000:1 id

./gosu version: 1.1 (go1.3.1 on linux/amd64; gc)
  • Simple example:
$ docker run -it --rm ubuntu:trusty su -c 'exec ps aux'
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  46636  2688 ?        Ss+  02:22   0:00 su -c exec ps a
root         6  0.0  0.0  15576  2220 ?        Rs   02:22   0:00 ps aux
$ docker run -it --rm ubuntu:trusty sudo ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  3.0  0.0  46020  3144 ?        Ss+  02:22   0:00 sudo ps aux
root         7  0.0  0.0  15576  2172 ?        R+   02:22   0:00 ps aux
$ docker run -it --rm -v $PWD/gosu-amd64:/usr/local/bin/gosu:ro ubuntu:trusty gosu root ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   7140   768 ?        Rs+  02:22   0:00 ps aux
  • Regardless of whether it is su or sudo, the PID number when they execute the ps aux command is not 1. Although it is possible in a container, this is not a good solution. The process with PID=1 in the container is the application itself. Therefore, you can use the gosu command to switch users to execute commands.
  • For debian installation method is as follows:
    • Debian 9 ("Debian Stretch") or newer:
RUN set -eux; \
 apt-get update; \
 apt-get install -y gosu; \
 rm -rf /var/lib/apt/lists/*; \
# verify that the binary works
 gosu nobody true
    • Older Debian versions (or newer gosu versions):
ENV GOSU_VERSION 1.16
RUN set -eux; \
# save list of currently installed packages for later so we can clean up
 savedAptMark="$(apt-mark showmanual)"; \
 apt-get update; \
 apt-get install -y --no-install-recommends ca-certificates wget; \
 if ! command -v gpg; then \
  apt-get install -y --no-install-recommends gnupg2 dirmngr; \
 elif gpg --version | grep -q '^gpg (GnuPG) 1\.'; then \
# "This package provides support for HKPS keyservers." (GnuPG 1.x only)
  apt-get install -y --no-install-recommends gnupg-curl; \
 fi; \
 rm -rf /var/lib/apt/lists/*; \
 \
 dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
 wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
 wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
 \
# verify the signature
 export GNUPGHOME="$(mktemp -d)"; \
 gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
 gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
 command -v gpgconf && gpgconf --kill all || :; \
 rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
 \
# clean up fetch dependencies
 apt-mark auto '.*' > /dev/null; \
 [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; \
 apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
 \
 chmod +x /usr/local/bin/gosu; \
# verify that the binary works
 gosu --version; \
 gosu nobody true
    • For alpine (3.7+): When using Alpine, it might also be worth checking out su-exec (apk add --no-cache su-exec), which since version 0.2 is fully compatible with gosu at a fraction of the file size one:
ENV GOSU_VERSION 1.16
RUN set -eux; \
 \
 apk add --no-cache --virtual .gosu-deps \
  ca-certificates \
  dpkg \
  gnupg \
 ; \
 \
 dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
 wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
 wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
 \
# verify the signature
 export GNUPGHOME="$(mktemp -d)"; \
 gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
 gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
 command -v gpgconf && gpgconf --kill all || :; \
 rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
 \
# clean up fetch dependencies
 apk del --no-network .gosu-deps; \
 \
 chmod +x /usr/local/bin/gosu; \
# verify that the binary works
 gosu --version; \
 gosu nobody true

4. entrypoint script file

  • Script example 1:
#!/bin/sh
set -e
ls ${
    
    LOG_PATH} > /dev/null 2>&1 || mkdir -p ${
    
    LOG_PATH}
chown -R www-data ${
    
    LOG_PATH}
if [ $# -gt 0 ];then
    #su ${
    
    USERNAME} -c "exec $@"
    exec su-exec www-data $@
else
    #su ${
    
    USERNAME} -c "exec uwsgi --ini uwsgi.ini --http=0.0.0.0:${DJANGO_PORT}"
    exec su-exec www-data uwsgi --ini uwsgi.ini --http=0.0.0.0:${
    
    DJANGO_PORT}
fi
  • Script description:
    • set -e: If the command execution fails, you should exit the script and not continue to execute it to avoid the subsequent impact of the failure. This can avoid the problem of continuing to execute even if the operation fails.
    • exec: The system call exec replaces the original process with a new process, but the PID of the process remains unchanged, which ensures that the container's main program PID=1.
  • Script example 2:
#!/bin/sh
set -e
if [ "$1" = 'uwsgi' -a "$(id -u)" = '0' ]
then
    ls ${
    
    LOG_PATH} > /dev/null 2>&1 || mkdir -p ${
    
    LOG_PATH}
    chown -R www-data ${
    
    LOG_PATH}
    exec su-exec www-data "$0" "$@"
fi
exec "$@"
  • Script description:
    • If the current user is root, then create and modify the LOG_PATH directory permissions, switch to the identity of www-data, take the remaining parameters, and run the docker-entrypoint.sh file again ("< a i=1> 0 " means docker-entrypoint.sh itself, " 0"表示dockerentryp oint.s h itself, "@" represents the remaining parameters). If there is code elsewhere in this script that needs to be executed by the www-data user, it can be executed as well.
    • When the script is executed again, since it is no longer the root user, exec "$@" will be executed directly, so the parameters, that is, the script defined by CMD, will be executed directly.
  • Add the docker-entrypoint.sh script in the Dockerfile, and you need to pay attention to the x execution permission, otherwise it will not have permission to execute:
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
  • Through this docker-entrypoint.sh script, you can force the directory permissions to be modified to the required permissions when the container is running, even if docker initializes the volume mounting directory created by the root user. In this way, you can run the program as a normal user in the container and write files in the directory with normal permissions.

Guess you like

Origin blog.csdn.net/Forever_wj/article/details/135022541