浏览代码

Adds support to run processes as a user/group, defined
with PUID and PGID environment variables

- Detects if image is run with a user in docker command and fails if so
- Adds s6 prepare scripts for adding a 'npmuser'
- Split up and refactor the s6 prepare scripts
- Runs nginx and backend node as 'npmuser'
- Changes ownership of files required at startup

Jamie Curnow 2 年之前
父节点
当前提交
dad3e1da7c

+ 2 - 0
README.md

@@ -70,6 +70,8 @@ services:
       - ./letsencrypt:/etc/letsencrypt
 ```
 
+This is the bare minimum configuration required. See the [documentation](https://nginxproxymanager.com/setup/) for more.
+
 3. Bring up your stack by running
 
 ```bash

+ 6 - 0
backend/internal/certificate.js

@@ -46,6 +46,8 @@ const internalCertificate = {
 
 			const cmd = certbotCommand + ' renew --non-interactive --quiet ' +
 				'--config "' + letsencryptConfig + '" ' +
+				'--work-dir "/tmp/letsencrypt-lib" ' +
+				'--logs-dir "/tmp/letsencrypt-log" ' +
 				'--preferred-challenges "dns,http" ' +
 				'--disable-hook-validation ' +
 				(letsencryptStaging ? '--staging' : '');
@@ -833,6 +835,8 @@ const internalCertificate = {
 
 		const cmd = certbotCommand + ' certonly ' +
 			'--config "' + letsencryptConfig + '" ' +
+			'--work-dir "/tmp/letsencrypt-lib" ' +
+			'--logs-dir "/tmp/letsencrypt-log" ' +
 			'--cert-name "npm-' + certificate.id + '" ' +
 			'--agree-tos ' +
 			'--authenticator webroot ' +
@@ -878,6 +882,8 @@ const internalCertificate = {
 
 		let mainCmd = certbotCommand + ' certonly ' +
 			'--config "' + letsencryptConfig + '" ' +
+			'--work-dir "/tmp/letsencrypt-lib" ' +
+			'--logs-dir "/tmp/letsencrypt-log" ' +
 			'--cert-name "npm-' + certificate.id + '" ' +
 			'--agree-tos ' +
 			'--email "' + certificate.meta.letsencrypt_email + '" ' +

+ 2 - 0
docker/docker-compose.dev.yml

@@ -14,6 +14,8 @@ services:
     networks:
       - nginx_proxy_manager
     environment:
+      PUID: 1000
+      PGID: 1000
       NODE_ENV: "development"
       FORCE_COLOR: 1
       DEVELOPMENT: "true"

+ 29 - 0
docker/rootfs/bin/common.sh

@@ -0,0 +1,29 @@
+#!/bin/bash
+
+set -e
+
+CYAN='\E[1;36m'
+BLUE='\E[1;34m'
+YELLOW='\E[1;33m'
+RED='\E[1;31m'
+RESET='\E[0m'
+export CYAN BLUE YELLOW RED RESET
+
+log_info () {
+	echo -e "${BLUE}❯ ${CYAN}$1${RESET}"
+}
+
+log_error () {
+	echo -e "${RED}❯ $1${RESET}"
+}
+
+# The `run` file will only execute 1 line so this helps keep things
+# logically separated
+
+log_fatal () {
+	echo -e "${RED}--------------------------------------${RESET}"
+	echo -e "${RED}ERROR: $1${RESET}"
+	echo -e "${RED}--------------------------------------${RESET}"
+	/run/s6/basedir/bin/halt
+	exit 1
+}

+ 0 - 46
docker/rootfs/bin/handle-ipv6-setting

@@ -1,46 +0,0 @@
-#!/bin/bash
-
-# This command reads the `DISABLE_IPV6` env var and will either enable
-# or disable ipv6 in all nginx configs based on this setting.
-
-# Lowercase
-DISABLE_IPV6=$(echo "${DISABLE_IPV6:-}" | tr '[:upper:]' '[:lower:]')
-
-CYAN='\E[1;36m'
-BLUE='\E[1;34m'
-YELLOW='\E[1;33m'
-RED='\E[1;31m'
-RESET='\E[0m'
-
-FOLDER=$1
-if [ "$FOLDER" == "" ]; then
-	echo -e "${RED}❯ $0 requires a absolute folder path as the first argument!${RESET}"
-	echo -e "${YELLOW}  ie: $0 /data/nginx${RESET}"
-	exit 1
-fi
-
-FILES=$(find "$FOLDER" -type f -name "*.conf")
-if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ]; then
-	# IPV6 is disabled
-	echo "Disabling IPV6 in hosts"
-	echo -e "${BLUE}❯ ${CYAN}Disabling IPV6 in hosts: ${YELLOW}${FOLDER}${RESET}"
-
-	# Iterate over configs and run the regex
-	for FILE in $FILES
-	do
-		echo -e "  ${BLUE}❯ ${YELLOW}${FILE}${RESET}"
-		sed -E -i 's/^([^#]*)listen \[::\]/\1#listen [::]/g' "$FILE"
-	done
-
-else
-	# IPV6 is enabled
-	echo -e "${BLUE}❯ ${CYAN}Enabling IPV6 in hosts: ${YELLOW}${FOLDER}${RESET}"
-
-	# Iterate over configs and run the regex
-	for FILE in $FILES
-	do
-		echo -e "  ${BLUE}❯ ${YELLOW}${FILE}${RESET}"
-		sed -E -i 's/^(\s*)#listen \[::\]/\1listen [::]/g' "$FILE"
-	done
-
-fi

+ 2 - 3
docker/rootfs/etc/nginx/nginx.conf

@@ -1,7 +1,6 @@
 # run nginx in foreground
 daemon off;
-
-user root;
+pid /run/nginx/nginx.pid;
 
 # Set number of worker processes automatically based on number of CPU cores.
 worker_processes auto;
@@ -57,7 +56,7 @@ http {
 	}
 
 	# Real IP Determination
-	
+
 	# Local subnets:
 	set_real_ip_from 10.0.0.0/8;
 	set_real_ip_from 172.16.0.0/12; # Includes Docker subnet

+ 7 - 4
docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run

@@ -3,17 +3,20 @@
 
 set -e
 
-echo "❯ Starting backend ..."
+. /bin/common.sh
+
+log_info 'Starting backend ...'
+
 if [ "$DEVELOPMENT" == "true" ]; then
 	cd /app || exit 1
 	# If yarn install fails: add --verbose --network-concurrency 1
-	yarn install
-	node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js
+	s6-setuidgid npmuser yarn install
+	exec s6-setuidgid npmuser node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js
 else
 	cd /app || exit 1
 	while :
 	do
-		node --abort_on_uncaught_exception --max_old_space_size=250 index.js
+		s6-setuidgid npmuser node --abort_on_uncaught_exception --max_old_space_size=250 index.js
 		sleep 1
 	done
 fi

+ 8 - 2
docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run

@@ -6,10 +6,16 @@ set -e
 # This service is DEVELOPMENT only.
 
 if [ "$DEVELOPMENT" == "true" ]; then
+	. /bin/common.sh
 	cd /app/frontend || exit 1
+	log_info 'Starting frontend ...'
+	HOME=/tmp/npmuserhome
+	export HOME
+	mkdir -p /app/frontend/dist
+	chown -R npmuser:npmuser /app/frontend/dist
 	# If yarn install fails: add --verbose --network-concurrency 1
-	yarn install
-	yarn watch
+	s6-setuidgid npmuser yarn install
+	exec s6-setuidgid npmuser yarn watch
 else
 	exit 0
 fi

+ 5 - 2
docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/run

@@ -3,5 +3,8 @@
 
 set -e
 
-echo "❯ Starting nginx ..."
-exec nginx
+. /bin/common.sh
+
+log_info 'Starting nginx ...'
+
+exec s6-setuidgid npmuser nginx

+ 18 - 0
docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/00-all.sh

@@ -0,0 +1,18 @@
+#!/command/with-contenv bash
+# shellcheck shell=bash
+
+set -e
+
+. /bin/common.sh
+
+if [ "$(id -u)" != "0" ]; then
+	log_fatal "This docker container must be run as root, do not specify a user.\nYou can specify PUID and PGID env vars to run processes as that user and group after initialization."
+fi
+
+. /etc/s6-overlay/s6-rc.d/prepare/10-npmuser.sh
+. /etc/s6-overlay/s6-rc.d/prepare/20-paths.sh
+. /etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh
+. /etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh
+. /etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh
+. /etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh
+. /etc/s6-overlay/s6-rc.d/prepare/90-banner.sh

+ 18 - 0
docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/10-npmuser.sh

@@ -0,0 +1,18 @@
+#!/command/with-contenv bash
+# shellcheck shell=bash
+
+set -e
+
+PUID=${PUID:-911}
+PGID=${PGID:-911}
+
+# Add npmuser user
+log_info 'Creating npmuser ...'
+
+groupmod -g 1000 users || exit 1
+useradd -u "${PUID}" -U -d /data -s /bin/false npmuser || exit 1
+usermod -G users npmuser || exit 1
+groupmod -o -g "$PGID" npmuser || exit 1
+# Home for npmuser
+mkdir -p /tmp/npmuserhome
+chown -R npmuser:npmuser /tmp/npmuserhome

+ 41 - 0
docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh

@@ -0,0 +1,41 @@
+#!/command/with-contenv bash
+# shellcheck shell=bash
+
+set -e
+
+log_info 'Checking paths ...'
+
+# Ensure /data is mounted
+if [ ! -d '/data' ]; then
+	log_fatal '/data is not mounted! Check your docker configuration.'
+fi
+# Ensure /etc/letsencrypt is mounted
+if [ ! -d '/etc/letsencrypt' ]; then
+	log_fatal '/etc/letsencrypt is not mounted! Check your docker configuration.'
+fi
+
+# Create required folders
+mkdir -p \
+	/data/nginx \
+	/data/custom_ssl \
+	/data/logs \
+	/data/access \
+	/data/nginx/default_host \
+	/data/nginx/default_www \
+	/data/nginx/proxy_host \
+	/data/nginx/redirection_host \
+	/data/nginx/stream \
+	/data/nginx/dead_host \
+	/data/nginx/temp \
+	/data/letsencrypt-acme-challenge \
+	/run/nginx \
+	/tmp/nginx/body \
+	/var/log/nginx \
+	/var/lib/nginx/cache/public \
+	/var/lib/nginx/cache/private \
+	/var/cache/nginx/proxy_temp
+
+touch /var/log/nginx/error.log || true
+chmod 777 /var/log/nginx/error.log || true
+chmod -R 777 /var/cache/nginx || true
+chmod 644 /etc/logrotate.d/nginx-proxy-manager

+ 21 - 0
docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh

@@ -0,0 +1,21 @@
+#!/command/with-contenv bash
+# shellcheck shell=bash
+
+set -e
+
+log_info 'Setting ownership ...'
+
+# root
+chown root /tmp/nginx
+
+# npmuser
+chown -R npmuser:npmuser \
+	/data \
+	/etc/letsencrypt \
+	/etc/nginx \
+	/run/nginx \
+	/tmp/nginx \
+	/var/cache/nginx \
+	/var/lib/logrotate \
+	/var/lib/nginx \
+	/var/log/nginx

+ 17 - 0
docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh

@@ -0,0 +1,17 @@
+#!/command/with-contenv bash
+# shellcheck shell=bash
+
+set -e
+
+log_info 'Dynamic resolvers ...'
+
+DISABLE_IPV6=$(echo "${DISABLE_IPV6:-}" | tr '[:upper:]' '[:lower:]')
+
+# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]`
+# thanks @tfmm
+if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ];
+then
+	echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) ipv6=off valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
+else
+	echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
+fi

+ 36 - 0
docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh

@@ -0,0 +1,36 @@
+#!/bin/bash
+
+# This command reads the `DISABLE_IPV6` env var and will either enable
+# or disable ipv6 in all nginx configs based on this setting.
+
+log_info 'IPv6 ...'
+
+# Lowercase
+DISABLE_IPV6=$(echo "${DISABLE_IPV6:-}" | tr '[:upper:]' '[:lower:]')
+
+process_folder () {
+	FILES=$(find "$1" -type f -name "*.conf")
+	SED_REGEX=
+
+	if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ]; then
+		# IPV6 is disabled
+		echo "Disabling IPV6 in hosts in: $1"
+		SED_REGEX='s/^([^#]*)listen \[::\]/\1#listen [::]/g'
+	else
+		# IPV6 is enabled
+		echo "Enabling IPV6 in hosts in: $1"
+		SED_REGEX='s/^(\s*)#listen \[::\]/\1listen [::]/g'
+	fi
+
+	for FILE in $FILES
+	do
+		echo "- ${FILE}"
+		sed -E -i "$SED_REGEX" "$FILE"
+	done
+
+	# ensure the files are still owned by the npmuser
+	chown -R npmuser:npmuser "$1"
+}
+
+process_folder /etc/nginx/conf.d
+process_folder /data/nginx

+ 30 - 0
docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh

@@ -0,0 +1,30 @@
+#!/command/with-contenv bash
+# shellcheck shell=bash
+
+set -e
+
+# in s6, environmental variables are written as text files for s6 to monitor
+# search through full-path filenames for files ending in "__FILE"
+log_info 'Docker secrets ...'
+
+for FILENAME in $(find /var/run/s6/container_environment/ | grep "__FILE$"); do
+	echo "[secret-init] Evaluating ${FILENAME##*/} ..."
+
+	# set SECRETFILE to the contents of the full-path textfile
+	SECRETFILE=$(cat "${FILENAME}")
+	# if SECRETFILE exists / is not null
+	if [[ -f "${SECRETFILE}" ]]; then
+		# strip the appended "__FILE" from environmental variable name ...
+		STRIPFILE=$(echo "${FILENAME}" | sed "s/__FILE//g")
+		# echo "[secret-init] Set STRIPFILE to ${STRIPFILE}"  # DEBUG - rm for prod!
+
+		# ... and set value to contents of secretfile
+		# since s6 uses text files, this is effectively "export ..."
+		printf $(cat "${SECRETFILE}") > "${STRIPFILE}"
+		# echo "[secret-init] Set ${STRIPFILE##*/} to $(cat ${STRIPFILE})"  # DEBUG - rm for prod!"
+		echo "Success: ${STRIPFILE##*/} set from ${FILENAME##*/}"
+
+	else
+		echo "Cannot find secret in ${FILENAME}"
+	fi
+done

+ 17 - 0
docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/90-banner.sh

@@ -0,0 +1,17 @@
+#!/command/with-contenv bash
+# shellcheck shell=bash
+
+set -e
+
+echo
+echo "-------------------------------------
+ _   _ ____  __  __
+| \ | |  _ \|  \/  |
+|  \| | |_) | |\/| |
+| |\  |  __/| |  | |
+|_| \_|_|   |_|  |_|
+-------------------------------------
+User UID: $(id -u npmuser)
+User GID: $(id -g npmuser)
+-------------------------------------
+"

+ 0 - 93
docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/script.sh

@@ -1,93 +0,0 @@
-#!/command/with-contenv bash
-# shellcheck shell=bash
-
-set -e
-
-DATA_PATH=/data
-
-# Ensure /data is mounted
-if [ ! -d "$DATA_PATH" ]; then
-	echo '--------------------------------------'
-	echo "ERROR: $DATA_PATH is not mounted! Check your docker configuration."
-	echo '--------------------------------------'
-	/run/s6/basedir/bin/halt
-	exit 1
-fi
-
-echo "❯ Checking folder structure ..."
-
-# Create required folders
-mkdir -p /tmp/nginx/body \
-	/run/nginx \
-	/var/log/nginx \
-	/data/nginx \
-	/data/custom_ssl \
-	/data/logs \
-	/data/access \
-	/data/nginx/default_host \
-	/data/nginx/default_www \
-	/data/nginx/proxy_host \
-	/data/nginx/redirection_host \
-	/data/nginx/stream \
-	/data/nginx/dead_host \
-	/data/nginx/temp \
-	/var/lib/nginx/cache/public \
-	/var/lib/nginx/cache/private \
-	/var/cache/nginx/proxy_temp \
-	/data/letsencrypt-acme-challenge
-
-touch /var/log/nginx/error.log && chmod 777 /var/log/nginx/error.log && chmod -R 777 /var/cache/nginx
-chown root /tmp/nginx
-
-# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]`
-# thanks @tfmm
-if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ];
-then
-	echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) ipv6=off valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
-else
-	echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
-fi
-
-echo "Changing ownership of /data/logs to $(id -u):$(id -g)"
-chown -R "$(id -u):$(id -g)" /data/logs
-
-# Handle IPV6 settings
-/bin/handle-ipv6-setting /etc/nginx/conf.d
-/bin/handle-ipv6-setting /data/nginx
-
-# ref: https://github.com/linuxserver/docker-baseimage-alpine/blob/master/root/etc/cont-init.d/01-envfile
-
-# in s6, environmental variables are written as text files for s6 to monitor
-# search through full-path filenames for files ending in "__FILE"
-echo "❯ Secrets-init ..."
-for FILENAME in $(find /var/run/s6/container_environment/ | grep "__FILE$"); do
-	echo "[secret-init] Evaluating ${FILENAME##*/} ..."
-
-	# set SECRETFILE to the contents of the full-path textfile
-	SECRETFILE=$(cat "${FILENAME}")
-	# if SECRETFILE exists / is not null
-	if [[ -f "${SECRETFILE}" ]]; then
-		# strip the appended "__FILE" from environmental variable name ...
-		STRIPFILE=$(echo "${FILENAME}" | sed "s/__FILE//g")
-		# echo "[secret-init] Set STRIPFILE to ${STRIPFILE}"  # DEBUG - rm for prod!
-
-		# ... and set value to contents of secretfile
-		# since s6 uses text files, this is effectively "export ..."
-		printf $(cat "${SECRETFILE}") > "${STRIPFILE}"
-		# echo "[secret-init] Set ${STRIPFILE##*/} to $(cat ${STRIPFILE})"  # DEBUG - rm for prod!"
-		echo "[secret-init] Success! ${STRIPFILE##*/} set from ${FILENAME##*/}"
-
-	else
-		echo "[secret-init] cannot find secret in ${FILENAME}"
-	fi
-done
-
-echo
-echo "-------------------------------------
- _   _ ____  __  __
-| \ | |  _ \|  \/  |
-|  \| | |_) | |\/| |
-| |\  |  __/| |  | |
-|_| \_|_|   |_|  |_|
--------------------------------------
-"

+ 1 - 1
docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/up

@@ -1,2 +1,2 @@
 # shellcheck shell=bash
-/etc/s6-overlay/s6-rc.d/prepare/script.sh
+/etc/s6-overlay/s6-rc.d/prepare/00-all.sh

+ 5 - 1
docs/setup/README.md

@@ -20,7 +20,7 @@ services:
 
     # Uncomment the next line if you uncomment anything in the section
     # environment:
-      # Uncomment this if you want to change the location of 
+      # Uncomment this if you want to change the location of
       # the SQLite DB file within the container
       # DB_SQLITE_FILE: "/data/database.sqlite"
 
@@ -64,6 +64,10 @@ services:
       # Add any other Stream port you want to expose
       # - '21:21' # FTP
     environment:
+      # Unix user and group IDs, optional
+      PUID: 1000
+      PGID: 1000
+      # Mysql/Maria connection parameters:
       DB_MYSQL_HOST: "db"
       DB_MYSQL_PORT: 3306
       DB_MYSQL_USER: "npm"

+ 1 - 0
docs/upgrading/README.md

@@ -9,3 +9,4 @@ This project will automatically update any databases or other requirements so yo
 any crazy instructions. These steps above will pull the latest updates and recreate the docker
 containers.
 
+See the [list of releases](https://github.com/NginxProxyManager/nginx-proxy-manager/releases) for any upgrade steps specific to each release.