github.com/moby/docker@v26.1.3+incompatible/contrib/dockerd-rootless-setuptool.sh (about)

     1  #!/bin/sh
     2  # dockerd-rootless-setuptool.sh: setup tool for dockerd-rootless.sh
     3  # Needs to be executed as a non-root user.
     4  #
     5  # Typical usage: dockerd-rootless-setuptool.sh install --force
     6  #
     7  # Documentation: https://docs.docker.com/go/rootless/
     8  set -eu
     9  
    10  # utility functions
    11  INFO() {
    12  	/bin/echo -e "\e[104m\e[97m[INFO]\e[49m\e[39m $@"
    13  }
    14  
    15  WARNING() {
    16  	/bin/echo >&2 -e "\e[101m\e[97m[WARNING]\e[49m\e[39m $@"
    17  }
    18  
    19  ERROR() {
    20  	/bin/echo >&2 -e "\e[101m\e[97m[ERROR]\e[49m\e[39m $@"
    21  }
    22  
    23  # constants
    24  DOCKERD_ROOTLESS_SH="dockerd-rootless.sh"
    25  SYSTEMD_UNIT="docker.service"
    26  CLI_CONTEXT="rootless"
    27  
    28  # CLI opt: --force
    29  OPT_FORCE=""
    30  # CLI opt: --skip-iptables
    31  OPT_SKIP_IPTABLES=""
    32  
    33  # global vars
    34  ARG0="$0"
    35  DOCKERD_ROOTLESS_SH_FLAGS=""
    36  BIN=""
    37  SYSTEMD=""
    38  CFG_DIR=""
    39  XDG_RUNTIME_DIR_CREATED=""
    40  USERNAME=""
    41  USERNAME_ESCAPED=""
    42  
    43  # run checks and also initialize global vars
    44  init() {
    45  	# OS verification: Linux only
    46  	case "$(uname)" in
    47  		Linux) ;;
    48  
    49  		*)
    50  			ERROR "Rootless Docker cannot be installed on $(uname)"
    51  			exit 1
    52  			;;
    53  	esac
    54  
    55  	# User verification: deny running as root
    56  	if [ "$(id -u)" = "0" ]; then
    57  		ERROR "Refusing to install rootless Docker as the root user"
    58  		exit 1
    59  	fi
    60  
    61  	# set BIN
    62  	if ! BIN="$(command -v "$DOCKERD_ROOTLESS_SH" 2> /dev/null)"; then
    63  		ERROR "$DOCKERD_ROOTLESS_SH needs to be present under \$PATH"
    64  		exit 1
    65  	fi
    66  	BIN=$(dirname "$BIN")
    67  
    68  	# set SYSTEMD
    69  	if systemctl --user show-environment > /dev/null 2>&1; then
    70  		SYSTEMD=1
    71  	fi
    72  
    73  	# HOME verification
    74  	if [ -z "${HOME:-}" ] || [ ! -d "$HOME" ]; then
    75  		ERROR "HOME needs to be set"
    76  		exit 1
    77  	fi
    78  	if [ ! -w "$HOME" ]; then
    79  		ERROR "HOME needs to be writable"
    80  		exit 1
    81  	fi
    82  
    83  	# Set USERNAME from `id -un` and potentially protect backslash
    84  	# for windbind/samba domain users
    85  	USERNAME=$(id -un)
    86  	USERNAME_ESCAPED=$(echo $USERNAME | sed 's/\\/\\\\/g')
    87  
    88  	# set CFG_DIR
    89  	CFG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}"
    90  
    91  	# Existing rootful docker verification
    92  	if [ -w /var/run/docker.sock ] && [ -z "$OPT_FORCE" ]; then
    93  		ERROR "Aborting because rootful Docker (/var/run/docker.sock) is running and accessible. Set --force to ignore."
    94  		exit 1
    95  	fi
    96  
    97  	# Validate XDG_RUNTIME_DIR and set XDG_RUNTIME_DIR_CREATED
    98  	if [ -z "${XDG_RUNTIME_DIR:-}" ] || [ ! -w "$XDG_RUNTIME_DIR" ]; then
    99  		if [ -n "$SYSTEMD" ]; then
   100  			ERROR "Aborting because systemd was detected but XDG_RUNTIME_DIR (\"$XDG_RUNTIME_DIR\") is not set, does not exist, or is not writable"
   101  			ERROR "Hint: this could happen if you changed users with 'su' or 'sudo'. To work around this:"
   102  			ERROR "- try again by first running with root privileges 'loginctl enable-linger <user>' where <user> is the unprivileged user and export XDG_RUNTIME_DIR to the value of RuntimePath as shown by 'loginctl show-user <user>'"
   103  			ERROR "- or simply log back in as the desired unprivileged user (ssh works for remote machines, machinectl shell works for local machines)"
   104  			exit 1
   105  		fi
   106  		export XDG_RUNTIME_DIR="$HOME/.docker/run"
   107  		mkdir -p -m 700 "$XDG_RUNTIME_DIR"
   108  		XDG_RUNTIME_DIR_CREATED=1
   109  	fi
   110  
   111  	instructions=""
   112  	# instruction: uidmap dependency check
   113  	if ! command -v newuidmap > /dev/null 2>&1; then
   114  		if command -v apt-get > /dev/null 2>&1; then
   115  			instructions=$(
   116  				cat <<- EOI
   117  					${instructions}
   118  					# Install newuidmap & newgidmap binaries
   119  					apt-get install -y uidmap
   120  				EOI
   121  			)
   122  		elif command -v dnf > /dev/null 2>&1; then
   123  			instructions=$(
   124  				cat <<- EOI
   125  					${instructions}
   126  					# Install newuidmap & newgidmap binaries
   127  					dnf install -y shadow-utils
   128  				EOI
   129  			)
   130  		elif command -v yum > /dev/null 2>&1; then
   131  			instructions=$(
   132  				cat <<- EOI
   133  					${instructions}
   134  					# Install newuidmap & newgidmap binaries
   135  					yum install -y shadow-utils
   136  				EOI
   137  			)
   138  		else
   139  			ERROR "newuidmap binary not found. Please install with a package manager."
   140  			exit 1
   141  		fi
   142  	fi
   143  
   144  	# instruction: iptables dependency check
   145  	faced_iptables_error=""
   146  	if ! command -v iptables > /dev/null 2>&1 && [ ! -f /sbin/iptables ] && [ ! -f /usr/sbin/iptables ]; then
   147  		faced_iptables_error=1
   148  		if [ -z "$OPT_SKIP_IPTABLES" ]; then
   149  			if command -v apt-get > /dev/null 2>&1; then
   150  				instructions=$(
   151  					cat <<- EOI
   152  						${instructions}
   153  						# Install iptables
   154  						apt-get install -y iptables
   155  					EOI
   156  				)
   157  			elif command -v dnf > /dev/null 2>&1; then
   158  				instructions=$(
   159  					cat <<- EOI
   160  						${instructions}
   161  						# Install iptables
   162  						dnf install -y iptables
   163  					EOI
   164  				)
   165  			elif command -v yum > /dev/null 2>&1; then
   166  				instructions=$(
   167  					cat <<- EOI
   168  						${instructions}
   169  						# Install iptables
   170  						yum install -y iptables
   171  					EOI
   172  				)
   173  			else
   174  				ERROR "iptables binary not found. Please install with a package manager."
   175  				exit 1
   176  			fi
   177  		fi
   178  	fi
   179  
   180  	# instruction: ip_tables module dependency check
   181  	if ! grep -q ip_tables /proc/modules 2> /dev/null && ! grep -q ip_tables /lib/modules/$(uname -r)/modules.builtin 2> /dev/null; then
   182  		faced_iptables_error=1
   183  		if [ -z "$OPT_SKIP_IPTABLES" ]; then
   184  			instructions=$(
   185  				cat <<- EOI
   186  					${instructions}
   187  					# Load ip_tables module
   188  					modprobe ip_tables
   189  				EOI
   190  			)
   191  		fi
   192  	fi
   193  
   194  	# set DOCKERD_ROOTLESS_SH_FLAGS
   195  	if [ -n "$faced_iptables_error" ] && [ -n "$OPT_SKIP_IPTABLES" ]; then
   196  		DOCKERD_ROOTLESS_SH_FLAGS="${DOCKERD_ROOTLESS_SH_FLAGS} --iptables=false"
   197  	fi
   198  
   199  	# instruction: Debian and Arch require setting unprivileged_userns_clone
   200  	if [ -f /proc/sys/kernel/unprivileged_userns_clone ]; then
   201  		if [ "1" != "$(cat /proc/sys/kernel/unprivileged_userns_clone)" ]; then
   202  			instructions=$(
   203  				cat <<- EOI
   204  					${instructions}
   205  					# Set kernel.unprivileged_userns_clone
   206  					cat <<EOT > /etc/sysctl.d/50-rootless.conf
   207  					kernel.unprivileged_userns_clone = 1
   208  					EOT
   209  					sysctl --system
   210  				EOI
   211  			)
   212  		fi
   213  	fi
   214  
   215  	# instruction: RHEL/CentOS 7 requires setting max_user_namespaces
   216  	if [ -f /proc/sys/user/max_user_namespaces ]; then
   217  		if [ "0" = "$(cat /proc/sys/user/max_user_namespaces)" ]; then
   218  			instructions=$(
   219  				cat <<- EOI
   220  					${instructions}
   221  					# Set user.max_user_namespaces
   222  					cat <<EOT > /etc/sysctl.d/51-rootless.conf
   223  					user.max_user_namespaces = 28633
   224  					EOT
   225  					sysctl --system
   226  				EOI
   227  			)
   228  		fi
   229  	fi
   230  
   231  	# instructions: validate subuid/subgid files for current user
   232  	if ! grep -q "^$USERNAME_ESCAPED:\|^$(id -u):" /etc/subuid 2> /dev/null; then
   233  		instructions=$(
   234  			cat <<- EOI
   235  				${instructions}
   236  				# Add subuid entry for ${USERNAME}
   237  				echo "${USERNAME}:100000:65536" >> /etc/subuid
   238  			EOI
   239  		)
   240  	fi
   241  	if ! grep -q "^$USERNAME_ESCAPED:\|^$(id -u):" /etc/subgid 2> /dev/null; then
   242  		instructions=$(
   243  			cat <<- EOI
   244  				${instructions}
   245  				# Add subgid entry for ${USERNAME}
   246  				echo "${USERNAME}:100000:65536" >> /etc/subgid
   247  			EOI
   248  		)
   249  	fi
   250  
   251  	# fail with instructions if requirements are not satisfied.
   252  	if [ -n "$instructions" ]; then
   253  		ERROR "Missing system requirements. Run the following commands to"
   254  		ERROR "install the requirements and run this tool again."
   255  		if [ -n "$faced_iptables_error" ] && [ -z "$OPT_SKIP_IPTABLES" ]; then
   256  			ERROR "Alternatively iptables checks can be disabled with --skip-iptables ."
   257  		fi
   258  		echo
   259  		echo "########## BEGIN ##########"
   260  		echo "sudo sh -eux <<EOF"
   261  		echo "$instructions" | sed -e '/^$/d'
   262  		echo "EOF"
   263  		echo "########## END ##########"
   264  		echo
   265  		exit 1
   266  	fi
   267  	# TODO: support printing non-essential but recommended instructions:
   268  	# - sysctl: "net.ipv4.ping_group_range"
   269  	# - sysctl: "net.ipv4.ip_unprivileged_port_start"
   270  	# - external binary: slirp4netns
   271  	# - external binary: fuse-overlayfs
   272  
   273  	# check RootlessKit functionality. RootlessKit will print hints if something is still unsatisfied.
   274  	# (e.g., `kernel.apparmor_restrict_unprivileged_userns` constraint)
   275  	if ! rootlesskit true; then
   276  		ERROR "RootlessKit failed, see the error messages and https://rootlesscontaine.rs/getting-started/common/ ."
   277  		exit 1
   278  	fi
   279  }
   280  
   281  # CLI subcommand: "check"
   282  cmd_entrypoint_check() {
   283  	init
   284  	# requirements are already checked in init()
   285  	INFO "Requirements are satisfied"
   286  }
   287  
   288  # CLI subcommand: "nsenter"
   289  cmd_entrypoint_nsenter() {
   290  	# No need to call init()
   291  	pid=$(cat "$XDG_RUNTIME_DIR/dockerd-rootless/child_pid")
   292  	exec nsenter --no-fork --wd="$(pwd)" --preserve-credentials -m -n -U -t "$pid" -- "$@"
   293  }
   294  
   295  show_systemd_error() {
   296  	n="20"
   297  	ERROR "Failed to start ${SYSTEMD_UNIT}. Run \`journalctl -n ${n} --no-pager --user --unit ${SYSTEMD_UNIT}\` to show the error log."
   298  	ERROR "Before retrying installation, you might need to uninstall the current setup: \`$0 uninstall -f ; ${BIN}/rootlesskit rm -rf ${HOME}/.local/share/docker\`"
   299  	if journalctl -q -n ${n} --user --unit ${SYSTEMD_UNIT} | grep -qF "/run/xtables.lock: Permission denied"; then
   300  		ERROR "Failure likely related to https://github.com/moby/moby/issues/41230"
   301  		ERROR "This may work as a workaround: \`sudo dnf install -y policycoreutils-python-utils && sudo semanage permissive -a iptables_t\`"
   302  	fi
   303  }
   304  
   305  # install (systemd)
   306  install_systemd() {
   307  	mkdir -p "${CFG_DIR}/systemd/user"
   308  	unit_file="${CFG_DIR}/systemd/user/${SYSTEMD_UNIT}"
   309  	if [ -f "${unit_file}" ]; then
   310  		WARNING "File already exists, skipping: ${unit_file}"
   311  	else
   312  		INFO "Creating ${unit_file}"
   313  		cat <<- EOT > "${unit_file}"
   314  			[Unit]
   315  			Description=Docker Application Container Engine (Rootless)
   316  			Documentation=https://docs.docker.com/go/rootless/
   317  
   318  			[Service]
   319  			Environment=PATH=$BIN:/sbin:/usr/sbin:$PATH
   320  			ExecStart=$BIN/dockerd-rootless.sh $DOCKERD_ROOTLESS_SH_FLAGS
   321  			ExecReload=/bin/kill -s HUP \$MAINPID
   322  			TimeoutSec=0
   323  			RestartSec=2
   324  			Restart=always
   325  			StartLimitBurst=3
   326  			StartLimitInterval=60s
   327  			LimitNOFILE=infinity
   328  			LimitNPROC=infinity
   329  			LimitCORE=infinity
   330  			TasksMax=infinity
   331  			Delegate=yes
   332  			Type=notify
   333  			NotifyAccess=all
   334  			KillMode=mixed
   335  
   336  			[Install]
   337  			WantedBy=default.target
   338  		EOT
   339  		systemctl --user daemon-reload
   340  	fi
   341  	if ! systemctl --user --no-pager status "${SYSTEMD_UNIT}" > /dev/null 2>&1; then
   342  		INFO "starting systemd service ${SYSTEMD_UNIT}"
   343  		(
   344  			set -x
   345  			if ! systemctl --user start "${SYSTEMD_UNIT}"; then
   346  				set +x
   347  				show_systemd_error
   348  				exit 1
   349  			fi
   350  			sleep 3
   351  		)
   352  	fi
   353  	(
   354  		set -x
   355  		if ! systemctl --user --no-pager --full status "${SYSTEMD_UNIT}"; then
   356  			set +x
   357  			show_systemd_error
   358  			exit 1
   359  		fi
   360  		DOCKER_HOST="unix://$XDG_RUNTIME_DIR/docker.sock" $BIN/docker version
   361  		systemctl --user enable "${SYSTEMD_UNIT}"
   362  	)
   363  	INFO "Installed ${SYSTEMD_UNIT} successfully."
   364  	INFO "To control ${SYSTEMD_UNIT}, run: \`systemctl --user (start|stop|restart) ${SYSTEMD_UNIT}\`"
   365  	INFO "To run ${SYSTEMD_UNIT} on system startup, run: \`sudo loginctl enable-linger ${USERNAME}\`"
   366  	echo
   367  }
   368  
   369  # install (non-systemd)
   370  install_nonsystemd() {
   371  	INFO "systemd not detected, ${DOCKERD_ROOTLESS_SH} needs to be started manually:"
   372  	echo
   373  	echo "PATH=$BIN:/sbin:/usr/sbin:\$PATH ${DOCKERD_ROOTLESS_SH} ${DOCKERD_ROOTLESS_SH_FLAGS}"
   374  	echo
   375  }
   376  
   377  cli_ctx_exists() {
   378  	name="$1"
   379  	"${BIN}/docker" --context=default context inspect -f "{{.Name}}" "${name}" > /dev/null 2>&1
   380  }
   381  
   382  cli_ctx_create() {
   383  	name="$1"
   384  	host="$2"
   385  	description="$3"
   386  	"${BIN}/docker" --context=default context create "${name}" --docker "host=${host}" --description "${description}" > /dev/null
   387  }
   388  
   389  cli_ctx_use() {
   390  	name="$1"
   391  	"${BIN}/docker" --context=default context use "${name}" > /dev/null
   392  }
   393  
   394  cli_ctx_rm() {
   395  	name="$1"
   396  	"${BIN}/docker" --context=default context rm -f "${name}" > /dev/null
   397  }
   398  
   399  # CLI subcommand: "install"
   400  cmd_entrypoint_install() {
   401  	init
   402  	# requirements are already checked in init()
   403  	if [ -z "$SYSTEMD" ]; then
   404  		install_nonsystemd
   405  	else
   406  		install_systemd
   407  	fi
   408  
   409  	if cli_ctx_exists "${CLI_CONTEXT}"; then
   410  		INFO "CLI context \"${CLI_CONTEXT}\" already exists"
   411  	else
   412  		INFO "Creating CLI context \"${CLI_CONTEXT}\""
   413  		cli_ctx_create "${CLI_CONTEXT}" "unix://${XDG_RUNTIME_DIR}/docker.sock" "Rootless mode"
   414  	fi
   415  
   416  	INFO "Using CLI context \"${CLI_CONTEXT}\""
   417  	cli_ctx_use "${CLI_CONTEXT}"
   418  
   419  	echo
   420  	INFO "Make sure the following environment variable(s) are set (or add them to ~/.bashrc):"
   421  	if [ -n "$XDG_RUNTIME_DIR_CREATED" ]; then
   422  		echo "# WARNING: systemd not found. You have to remove XDG_RUNTIME_DIR manually on every logout."
   423  		echo "export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR}"
   424  	fi
   425  	echo "export PATH=${BIN}:\$PATH"
   426  	echo
   427  	INFO "Some applications may require the following environment variable too:"
   428  	echo "export DOCKER_HOST=unix://${XDG_RUNTIME_DIR}/docker.sock"
   429  	echo
   430  
   431  }
   432  
   433  # CLI subcommand: "uninstall"
   434  cmd_entrypoint_uninstall() {
   435  	init
   436  	# requirements are already checked in init()
   437  	if [ -z "$SYSTEMD" ]; then
   438  		INFO "systemd not detected, ${DOCKERD_ROOTLESS_SH} needs to be stopped manually:"
   439  	else
   440  		unit_file="${CFG_DIR}/systemd/user/${SYSTEMD_UNIT}"
   441  		(
   442  			set -x
   443  			systemctl --user stop "${SYSTEMD_UNIT}"
   444  		) || :
   445  		(
   446  			set -x
   447  			systemctl --user disable "${SYSTEMD_UNIT}"
   448  		) || :
   449  		rm -f "${unit_file}"
   450  		INFO "Uninstalled ${SYSTEMD_UNIT}"
   451  	fi
   452  
   453  	if cli_ctx_exists "${CLI_CONTEXT}"; then
   454  		cli_ctx_rm "${CLI_CONTEXT}"
   455  		INFO "Deleted CLI context \"${CLI_CONTEXT}\""
   456  	fi
   457  	unset DOCKER_HOST
   458  	unset DOCKER_CONTEXT
   459  	cli_ctx_use "default"
   460  	INFO 'Configured CLI to use the "default" context.'
   461  	INFO
   462  	INFO 'Make sure to unset or update the environment PATH, DOCKER_HOST, and DOCKER_CONTEXT environment variables if you have added them to `~/.bashrc`.'
   463  	INFO "This uninstallation tool does NOT remove Docker binaries and data."
   464  	INFO "To remove data, run: \`$BIN/rootlesskit rm -rf $HOME/.local/share/docker\`"
   465  }
   466  
   467  # text for --help
   468  usage() {
   469  	echo "Usage: ${ARG0} [OPTIONS] COMMAND"
   470  	echo
   471  	echo "A setup tool for Rootless Docker (${DOCKERD_ROOTLESS_SH})."
   472  	echo
   473  	echo "Documentation: https://docs.docker.com/go/rootless/"
   474  	echo
   475  	echo "Options:"
   476  	echo "  -f, --force                Ignore rootful Docker (/var/run/docker.sock)"
   477  	echo "      --skip-iptables        Ignore missing iptables"
   478  	echo
   479  	echo "Commands:"
   480  	echo "  check        Check prerequisites"
   481  	echo "  nsenter      Enter into RootlessKit namespaces (mostly for debugging)"
   482  	echo "  install      Install systemd unit (if systemd is available) and show how to manage the service"
   483  	echo "  uninstall    Uninstall systemd unit"
   484  }
   485  
   486  # parse CLI args
   487  if ! args="$(getopt -o hf --long help,force,skip-iptables -n "$ARG0" -- "$@")"; then
   488  	usage
   489  	exit 1
   490  fi
   491  eval set -- "$args"
   492  while [ "$#" -gt 0 ]; do
   493  	arg="$1"
   494  	shift
   495  	case "$arg" in
   496  		-h | --help)
   497  			usage
   498  			exit 0
   499  			;;
   500  		-f | --force)
   501  			OPT_FORCE=1
   502  			;;
   503  		--skip-iptables)
   504  			OPT_SKIP_IPTABLES=1
   505  			;;
   506  		--)
   507  			break
   508  			;;
   509  		*)
   510  			# XXX this means we missed something in our "getopt" arguments above!
   511  			ERROR "Scripting error, unknown argument '$arg' when parsing script arguments."
   512  			exit 1
   513  			;;
   514  	esac
   515  done
   516  
   517  command="${1:-}"
   518  if [ -z "$command" ]; then
   519  	ERROR "No command was specified. Run with --help to see the usage. Maybe you want to run \`$ARG0 install\`?"
   520  	exit 1
   521  fi
   522  
   523  if ! command -v "cmd_entrypoint_${command}" > /dev/null 2>&1; then
   524  	ERROR "Unknown command: ${command}. Run with --help to see the usage."
   525  	exit 1
   526  fi
   527  
   528  # main
   529  "cmd_entrypoint_${command}" "$@"