Skip to content

Commit

Permalink
Adds support to run processes as a user/group, defined
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jc21 committed Mar 20, 2023
1 parent 82d9452 commit dad3e1d
Show file tree
Hide file tree
Showing 21 changed files with 266 additions and 152 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions backend/internal/certificate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' : '');
Expand Down Expand Up @@ -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 ' +
Expand Down Expand Up @@ -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 + '" ' +
Expand Down
2 changes: 2 additions & 0 deletions docker/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ services:
networks:
- nginx_proxy_manager
environment:
PUID: 1000
PGID: 1000
NODE_ENV: "development"
FORCE_COLOR: 1
DEVELOPMENT: "true"
Expand Down
29 changes: 29 additions & 0 deletions docker/rootfs/bin/common.sh
Original file line number Diff line number Diff line change
@@ -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
}
46 changes: 0 additions & 46 deletions docker/rootfs/bin/handle-ipv6-setting

This file was deleted.

5 changes: 2 additions & 3 deletions docker/rootfs/etc/nginx/nginx.conf
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 8 additions & 2 deletions docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 5 additions & 2 deletions docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/run
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@

set -e

echo "❯ Starting nginx ..."
exec nginx
. /bin/common.sh

log_info 'Starting nginx ...'

exec s6-setuidgid npmuser nginx
18 changes: 18 additions & 0 deletions docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/00-all.sh
Original file line number Diff line number Diff line change
@@ -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 changes: 18 additions & 0 deletions docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/10-npmuser.sh
Original file line number Diff line number Diff line change
@@ -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 changes: 41 additions & 0 deletions docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh
Original file line number Diff line number Diff line change
@@ -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 changes: 21 additions & 0 deletions docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh
Original file line number Diff line number Diff line change
@@ -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 changes: 17 additions & 0 deletions docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh
Original file line number Diff line number Diff line change
@@ -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 changes: 36 additions & 0 deletions docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh
Original file line number Diff line number Diff line change
@@ -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 changes: 30 additions & 0 deletions docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit dad3e1d

Please sign in to comment.