refactor(entrypoint) improve script clarity

Improved entrypoint script clarity:

- General code reformatting
- Refactors to keep most lines <=90 characters 
- Updates existing comments for clarity/consistency/usefulness
- Adds new, clarifying comments
- Adds/enhances descriptions for all utility functions
- Adds reminder language re: ash/dash script compatibility
- Minor adjustments to some error/general output
- Noted some future "TODO" (possible) items
- Fix non-POSIX variable compliance

No logic changes.

Signed-off-by: Josh <josh.t.richards@gmail.com>
jtr/refactor-entrypoint-2025q3-pass
Josh 4 months ago committed by GitHub
parent c8211b8672
commit d15179409a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,16 +1,64 @@
#!/bin/sh
set -eu
# version_greater A B returns whether A > B
###############################################################################
# Entrypoint script for Nextcloud Docker container
###############################################################################
# Handles container-specific operations such as initialization, automatic configuration,
# user/group ID management, and setup checks. Also runs Nextcloud Servers built-in
# installation and upgrade routines in a way that fits the container environment.
#
# Supports environment-based configuration injection at install time for all key
# parameters (see README for details). After installation, allows reconfiguration
# of select parameters via environment variables - except NEXTCLOUD_TRUSTED_DOMAINS
# and those set by the Nextcloud installer.
#
# REMINDER: This script must work with non-interactive, POSIX-compliant shells used in our
# images. Do not use Bash-specific syntax ("bashisms"): /bin/sh is always either 'ash'
# (BusyBox) or 'dash' (Debian), not Bash. Stick to standard POSIX shell features.
# Resources for writing portable shell scripts:
# - checkbashisms (Alpine: checkbashisms; Debian: devscripts)
# - https://mywiki.wooledge.org/Bashism
# - https://www.shellcheck.net/
# Same also applies to any commands called too (e.g., GNU find versus Busybox find).
###############################################################################
# Utility Functions
###############################################################################
# Command for running `occ`
OCC="php /var/www/html/occ"
# version_greater
# Compare two version strings (A and B).
# Arguments:
# $1: Version string A
# $2: Version string B
# Returns: 0 (true) if version A is greater than B; 1 (false) otherwise.
version_greater() {
[ "$(printf '%s\n' "$@" | sort -t '.' -n -k1,1 -k2,2 -k3,3 -k4,4 | head -n 1)" != "$1" ]
}
# return true if specified directory is empty
# directory_empty
# Check if a directory is empty.
# Arguments:
# $1: Directory path.
# Returns: 0 (true) if directory is empty; 1 (false) otherwise.
directory_empty() {
[ -z "$(ls -A "$1/")" ]
}
# run_as
# Run a command as the specified user if running as root, otherwise as current user.
# Arguments:
# $1: Command string to execute.
# Globals:
# user - Username to switch to (when running as root).
# Returns: the exit code of the executed command.
# TODO:
# Consider printing error message then returning (or exiting the script) if a command fails.
# If some callers want to handle errors, hide behind optional flag ("--exit-on-error").
run_as() {
if [ "$(id -u)" = 0 ]; then
su -p "$user" -s /bin/sh -c "$1"
@ -19,13 +67,17 @@ run_as() {
fi
}
# Execute all executable files in a given directory in alphanumeric order
# run_path
# Execute all executable .sh files in the specified hook folder, in alphanumeric order.
# Arguments:
# $1: Name of the hook folder inside /docker-entrypoint-hooks.d/
# Returns: 0 on success; exits the script on any hook failure.
run_path() {
local hook_folder_path="/docker-entrypoint-hooks.d/$1"
local return_code=0
local found=0
hook_folder_path="/docker-entrypoint-hooks.d/$1"
return_code=0
found=0
echo "=> Searching for hook scripts (*.sh) to run, located in the folder \"${hook_folder_path}\""
echo "=> Searching for hook scripts (*.sh) to run in \"${hook_folder_path}\""
if ! [ -d "${hook_folder_path}" ] || directory_empty "${hook_folder_path}"; then
echo "==> Skipped: the \"$1\" folder is empty (or does not exist)"
@ -35,40 +87,43 @@ run_path() {
find "${hook_folder_path}" -maxdepth 1 -iname '*.sh' '(' -type f -o -type l ')' -print | sort | (
while read -r script_file_path; do
if ! [ -x "${script_file_path}" ]; then
echo "==> The script \"${script_file_path}\" was skipped, because it lacks the executable flag"
echo "==> The script \"${script_file_path}\" was skipped: lacks exec flag"
found=$((found-1))
continue
fi
echo "==> Running the script (cwd: $(pwd)): \"${script_file_path}\""
echo "==> Running script (cwd: $(pwd)): \"${script_file_path}\""
found=$((found+1))
run_as "${script_file_path}" || return_code="$?"
if [ "${return_code}" -ne "0" ]; then
echo "==> Failed at executing script \"${script_file_path}\". Exit code: ${return_code}"
echo "==> Failed executing \"${script_file_path}\". Exit code: ${return_code}"
exit 1
fi
echo "==> Finished executing the script: \"${script_file_path}\""
echo "==> Finished executing: \"${script_file_path}\""
done
if [ "$found" -lt "1" ]; then
echo "==> Skipped: the \"$1\" folder does not contain any valid scripts"
echo "==> Skipped: the \"$1\" folder contains no valid scripts"
else
echo "=> Completed executing scripts in the \"$1\" folder"
echo "=> Completed executing scripts in \"$1\""
fi
)
}
# usage: file_env VAR [DEFAULT]
# ie: file_env 'XYZ_DB_PASSWORD' 'example'
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
# file_env
# Load an environment variable from a file if available (supporting Docker secrets).
# Arguments:
# $1: Name of the environment variable.
# $2: (Optional) Default value if not set.
# Returns: Sets the environment variable named by $1.
file_env() {
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
local varValue=$(env | grep -E "^${var}=" | sed -E -e "s/^${var}=//")
local fileVarValue=$(env | grep -E "^${fileVar}=" | sed -E -e "s/^${fileVar}=//")
var="$1"
fileVar="${var}_FILE"
def="${2:-}"
varValue=$(env | grep -E "^${var}=" | sed -E -e "s/^${var}=//")
fileVarValue=$(env | grep -E "^${fileVar}=" | sed -E -e "s/^${fileVar}=//")
if [ -n "${varValue}" ] && [ -n "${fileVarValue}" ]; then
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
exit 1
@ -83,22 +138,32 @@ file_env() {
unset "$fileVar"
}
###############################################################################
# Main Entrypoint Logic
###############################################################################
# Disable the Apache remoteip configuration if requested via environment variable.
# TODO: This probably be moved inside the main initialization/upgrade block below.
if expr "$1" : "apache" 1>/dev/null; then
if [ -n "${APACHE_DISABLE_REWRITE_IP+x}" ]; then
a2disconf remoteip
fi
fi
# Only run this block if entrypoint command is Apache|PHP-FPM, or if explicitly requested.
# TODO: This huge block should probably be broken into several discrete functions for maintainability.
if expr "$1" : "apache" 1>/dev/null || [ "$1" = "php-fpm" ] || [ "${NEXTCLOUD_UPDATE:-0}" -eq 1 ]; then
uid="$(id -u)"
gid="$(id -g)"
# Determine effective user and group for Nextcloud operations.
if [ "$uid" = '0' ]; then
case "$1" in
apache2*)
user="${APACHE_RUN_USER:-www-data}"
group="${APACHE_RUN_GROUP:-www-data}"
# strip off any '#' symbol ('#1000' is valid syntax for Apache)
# Strip off any '#' symbol ('#1000' is valid syntax for Apache)
user="${user#'#'}"
group="${group#'#'}"
;;
@ -112,104 +177,138 @@ if expr "$1" : "apache" 1>/dev/null || [ "$1" = "php-fpm" ] || [ "${NEXTCLOUD_UP
group="$gid"
fi
# If REDIS_HOST is set, configure PHP sessions to use Redis.
if [ -n "${REDIS_HOST+x}" ]; then
echo "Configuring Redis as session handler"
{
file_env REDIS_HOST_PASSWORD
echo 'session.save_handler = redis'
# check if redis host is an unix socket path
if [ "$(echo "$REDIS_HOST" | cut -c1-1)" = "/" ]; then
if [ -n "${REDIS_HOST_PASSWORD+x}" ]; then
if [ -n "${REDIS_HOST_USER+x}" ]; then
echo "session.save_path = \"unix://${REDIS_HOST}?auth[]=${REDIS_HOST_USER}&auth[]=${REDIS_HOST_PASSWORD}\""
else
echo "session.save_path = \"unix://${REDIS_HOST}?auth=${REDIS_HOST_PASSWORD}\""
fi
else
echo "session.save_path = \"unix://${REDIS_HOST}\""
fi
# check if redis password has been set
elif [ -n "${REDIS_HOST_PASSWORD+x}" ]; then
file_env REDIS_HOST_PASSWORD
# Determine session.save_path depending on socket or TCP and credentials.
redis_save_path=""
first_char=$(printf '%s' "$REDIS_HOST" | cut -c1-1)
if [ "$first_char" = "/" ]; then
# Unix socket
if [ -n "${REDIS_HOST_PASSWORD+x}" ]; then
if [ -n "${REDIS_HOST_USER+x}" ]; then
echo "session.save_path = \"tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}?auth[]=${REDIS_HOST_USER}&auth[]=${REDIS_HOST_PASSWORD}\""
redis_save_path="unix://${REDIS_HOST}?auth[]=${REDIS_HOST_USER}&auth[]=${REDIS_HOST_PASSWORD}"
else
echo "session.save_path = \"tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}?auth=${REDIS_HOST_PASSWORD}\""
redis_save_path="unix://${REDIS_HOST}?auth=${REDIS_HOST_PASSWORD}"
fi
else
echo "session.save_path = \"tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}\""
redis_save_path="unix://${REDIS_HOST}"
fi
echo "redis.session.locking_enabled = 1"
echo "redis.session.lock_retries = -1"
# redis.session.lock_wait_time is specified in microseconds.
# Wait 10ms before retrying the lock rather than the default 2ms.
echo "redis.session.lock_wait_time = 10000"
} > /usr/local/etc/php/conf.d/redis-session.ini
elif [ -n "${REDIS_HOST_PASSWORD+x}" ]; then
# TCP with password
if [ -n "${REDIS_HOST_USER+x}" ]; then
redis_save_path="tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}?auth[]=${REDIS_HOST_USER}&auth[]=${REDIS_HOST_PASSWORD}"
else
redis_save_path="tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}?auth=${REDIS_HOST_PASSWORD}"
fi
else
# TCP without password
redis_save_path="tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}"
fi
# Write the configuration file using a heredoc.
cat > /usr/local/etc/php/conf.d/redis-session.ini <<EOF
session.save_handler = redis
session.save_path = "${redis_save_path}"
redis.session.locking_enabled = 1
redis.session.lock_retries = -1
# redis.session.lock_wait_time is specified in microseconds.
# Wait 10ms before retrying the lock rather than the default 2ms.
redis.session.lock_wait_time = 10000
EOF
fi
# If another process is syncing the html folder, wait for
# it to be done, then escape initalization.
# Use file locking to ensure only one initialization or upgrade runs at a time.
(
if ! flock -n 9; then
# If we couldn't get it immediately, show a message, then wait for real
echo "Another process is initializing Nextcloud. Waiting..."
flock 9
fi
installed_version="0.0.0.0"
if [ -f /var/www/html/version.php ]; then
# TODO: The error handling should be improved here in case of syntax/etc errors
# shellcheck disable=SC2016
installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')"
fi
# TODO: The error handling should be improved here in case of syntax/etc errors
# shellcheck disable=SC2016
image_version="$(php -r 'require "/usr/src/nextcloud/version.php"; echo implode(".", $OC_Version);')"
if version_greater "$installed_version" "$image_version"; then
echo "Can't start Nextcloud because the version of the data ($installed_version) is higher than the docker image version ($image_version) and downgrading is not supported. Are you sure you have pulled the newest image version?"
echo "Can't start Nextcloud: data version ($installed_version) is higher than"
echo "docker image version ($image_version); downgrading is not supported."
echo "Are you sure you have pulled a newer image version?"
exit 1
fi
# Image version needs to be > installed version to proceed farther.
# NOTE: Also true if there is no installed version.
if version_greater "$image_version" "$installed_version"; then
echo "Initializing nextcloud $image_version ..."
# Check for an already installed version that isn't in allowable upgrade jump range.
if [ "$installed_version" != "0.0.0.0" ]; then
if [ "${image_version%%.*}" -gt "$((${installed_version%%.*} + 1))" ]; then
echo "Can't start Nextcloud because upgrading from $installed_version to $image_version is not supported."
echo "It is only possible to upgrade one major version at a time. For example, if you want to upgrade from version 14 to 16, you will have to upgrade from version 14 to 15, then from 15 to 16."
echo "Can't start Nextcloud: upgrading from $installed_version to"
echo "$image_version is not supported."
echo "You can upgrade only one major version at a time."
echo "E.g., to upgrade from 14 to 16, first upgrade 14 to 15, then 15 to 16."
exit 1
fi
# Installed version has been deemed within allow upgrade jump range...
echo "Upgrading nextcloud from $installed_version ..."
run_as 'php /var/www/html/occ app:list' | sed -n "/Enabled:/,/Disabled:/p" > /tmp/list_before
run_as "$OCC app:list" | sed -n "/Enabled:/,/Disabled:/p" > /tmp/list_before
fi
# Handle rsync configuration
if [ "$(id -u)" = 0 ]; then
rsync_options="-rlDog --chown $user:$group"
else
rsync_options="-rlD"
fi
rsync $rsync_options --delete --exclude-from=/upgrade.exclude /usr/src/nextcloud/ /var/www/html/
# Replace installed code with newer image code except for exclusions
rsync "$rsync_options" --delete --exclude-from=/upgrade.exclude \
/usr/src/nextcloud/ /var/www/html/
# Utilize newer image code versions if no existing { config, data, custom_apps, themes }
for dir in config data custom_apps themes; do
if [ ! -d "/var/www/html/$dir" ] || directory_empty "/var/www/html/$dir"; then
rsync $rsync_options --include "/$dir/" --exclude '/*' /usr/src/nextcloud/ /var/www/html/
rsync "$rsync_options" --include "/$dir/" --exclude '/*' \
/usr/src/nextcloud/ /var/www/html/
fi
done
rsync $rsync_options --include '/version.php' --exclude '/*' /usr/src/nextcloud/ /var/www/html/
# Install
# Replace installed code's version.php with newer image code version
rsync "$rsync_options" --include '/version.php' --exclude '/*' \
/usr/src/nextcloud/ /var/www/html/
# Install block for fresh instances.
if [ "$installed_version" = "0.0.0.0" ]; then
echo "New nextcloud instance"
# Handle initial admin credentials (if provided)
file_env NEXTCLOUD_ADMIN_PASSWORD
file_env NEXTCLOUD_ADMIN_USER
install=false
if [ -n "${NEXTCLOUD_ADMIN_USER+x}" ] && [ -n "${NEXTCLOUD_ADMIN_PASSWORD+x}" ]; then
# shellcheck disable=SC2016
install_options='-n --admin-user "$NEXTCLOUD_ADMIN_USER" --admin-pass "$NEXTCLOUD_ADMIN_PASSWORD"'
if [ -n "${NEXTCLOUD_ADMIN_USER+x}" ] \
&& [ -n "${NEXTCLOUD_ADMIN_PASSWORD+x}" ]; then
install_options="-n \
--admin-user \"$NEXTCLOUD_ADMIN_USER\" \
--admin-pass \"$NEXTCLOUD_ADMIN_PASSWORD\""
if [ -n "${NEXTCLOUD_DATA_DIR+x}" ]; then
# shellcheck disable=SC2016
install_options=$install_options' --data-dir "$NEXTCLOUD_DATA_DIR"'
install_options="$install_options \
--data-dir \"$NEXTCLOUD_DATA_DIR\""
fi
# Handle database configuration (if specified)
file_env MYSQL_DATABASE
file_env MYSQL_PASSWORD
file_env MYSQL_USER
@ -219,68 +318,98 @@ if expr "$1" : "apache" 1>/dev/null || [ "$1" = "php-fpm" ] || [ "${NEXTCLOUD_UP
if [ -n "${SQLITE_DATABASE+x}" ]; then
echo "Installing with SQLite database"
# shellcheck disable=SC2016
install_options=$install_options' --database-name "$SQLITE_DATABASE"'
install_options="$install_options \
--database-name \"$SQLITE_DATABASE\""
install=true
elif [ -n "${MYSQL_DATABASE+x}" ] && [ -n "${MYSQL_USER+x}" ] && [ -n "${MYSQL_PASSWORD+x}" ] && [ -n "${MYSQL_HOST+x}" ]; then
elif [ -n "${MYSQL_DATABASE+x}" ] \
&& [ -n "${MYSQL_USER+x}" ] \
&& [ -n "${MYSQL_PASSWORD+x}" ] \
&& [ -n "${MYSQL_HOST+x}" ]; then
echo "Installing with MySQL database"
# shellcheck disable=SC2016
install_options=$install_options' --database mysql --database-name "$MYSQL_DATABASE" --database-user "$MYSQL_USER" --database-pass "$MYSQL_PASSWORD" --database-host "$MYSQL_HOST"'
install_options="$install_options \
--database mysql \
--database-name \"$MYSQL_DATABASE\" \
--database-user \"$MYSQL_USER\" \
--database-pass \"$MYSQL_PASSWORD\" \
--database-host \"$MYSQL_HOST\""
install=true
elif [ -n "${POSTGRES_DB+x}" ] && [ -n "${POSTGRES_USER+x}" ] && [ -n "${POSTGRES_PASSWORD+x}" ] && [ -n "${POSTGRES_HOST+x}" ]; then
elif [ -n "${POSTGRES_DB+x}" ] \
&& [ -n "${POSTGRES_USER+x}" ] \
&& [ -n "${POSTGRES_PASSWORD+x}" ] \
&& [ -n "${POSTGRES_HOST+x}" ]; then
echo "Installing with PostgreSQL database"
# shellcheck disable=SC2016
install_options=$install_options' --database pgsql --database-name "$POSTGRES_DB" --database-user "$POSTGRES_USER" --database-pass "$POSTGRES_PASSWORD" --database-host "$POSTGRES_HOST"'
install_options="$install_options \
--database pgsql \
--database-name \"$POSTGRES_DB\" \
--database-user \"$POSTGRES_USER\" \
--database-pass \"$POSTGRES_PASSWORD\" \
--database-host \"$POSTGRES_HOST\""
install=true
fi
# Run Nextcloud installer if we were provided enough auto-config values.
# (if not, we don't trigger the actual Nextcloud installer; the config values
# will need to be provided via the Nextcloud Installer's Web UI / wizard).
if [ "$install" = true ]; then
# Trigger pre-installation hook scripts (if any)
run_path pre-installation
echo "Starting nextcloud installation"
max_retries=10
try=0
until [ "$try" -gt "$max_retries" ] || run_as "php /var/www/html/occ maintenance:install $install_options"
until [ "$try" -gt "$max_retries" ] || run_as \
"$OCC maintenance:install $install_options"
do
echo "Retrying install..."
try=$((try+1))
sleep 10s
done
if [ "$try" -gt "$max_retries" ]; then
echo "Installing of nextcloud failed!"
echo "Installation of nextcloud failed!"
exit 1
fi
# Configure trusted domains if provided.
# TODO: This could probably be moved elsewhere to permit reconfiguration within existing installs.
if [ -n "${NEXTCLOUD_TRUSTED_DOMAINS+x}" ]; then
echo "Setting trusted domains…"
set -f # turn off glob
echo "Setting trusted_domains…"
set -f # turn off glob
NC_TRUSTED_DOMAIN_IDX=1
for DOMAIN in ${NEXTCLOUD_TRUSTED_DOMAINS}; do
DOMAIN=$(echo "${DOMAIN}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
run_as "php /var/www/html/occ config:system:set trusted_domains $NC_TRUSTED_DOMAIN_IDX --value=\"${DOMAIN}\""
run_as \
"$OCC config:system:set trusted_domains $NC_TRUSTED_DOMAIN_IDX --value=\"${DOMAIN}\""
NC_TRUSTED_DOMAIN_IDX=$((NC_TRUSTED_DOMAIN_IDX+1))
done
set +f # turn glob back on
set +f # turn glob back on
fi
# Trigger post-installation hook scripts (if any)
run_path post-installation
fi
fi
fi
# not enough specified to do a fully automated installation
# Not enough parameters specified to do a fully automated installation.
if [ "$install" = false ]; then
echo "Next step: Access your instance to finish the web-based installation!"
echo "Hint: You can specify NEXTCLOUD_ADMIN_USER and NEXTCLOUD_ADMIN_PASSWORD and the database variables _prior to first launch_ to fully automate initial installation."
echo "Hint: Set NEXTCLOUD_ADMIN_USER, NEXTCLOUD_ADMIN_PASSWORD, and DB vars"
echo "before first launch to fully automate initial installation."
fi
# Upgrade
# Upgrade path for existing instances.
else
# Trigger pre-upgrade hook scripts (if any)
run_path pre-upgrade
run_as 'php /var/www/html/occ upgrade'
run_as "$OCC upgrade"
run_as 'php /var/www/html/occ app:list' | sed -n "/Enabled:/,/Disabled:/p" > /tmp/list_after
run_as "$OCC app:list" | sed -n "/Enabled:/,/Disabled:/p" > /tmp/list_after
echo "The following apps have been disabled:"
diff /tmp/list_before /tmp/list_after | grep '<' | cut -d- -f2 | cut -d: -f1
diff /tmp/list_before /tmp/list_after \
| grep '<' | cut -d- -f2 | cut -d: -f1
rm -f /tmp/list_before /tmp/list_after
# Trigger post-upgrade hook scripts (if any)
run_path post-upgrade
fi
@ -288,23 +417,32 @@ if expr "$1" : "apache" 1>/dev/null || [ "$1" = "php-fpm" ] || [ "${NEXTCLOUD_UP
fi
# Update htaccess after init if requested
if [ -n "${NEXTCLOUD_INIT_HTACCESS+x}" ] && [ "$installed_version" != "0.0.0.0" ]; then
run_as 'php /var/www/html/occ maintenance:update:htaccess'
if [ -n "${NEXTCLOUD_INIT_HTACCESS+x}" ] \
&& [ "$installed_version" != "0.0.0.0" ]; then
run_as "$OCC maintenance:update:htaccess"
fi
) 9> /var/www/html/nextcloud-init-sync.lock
# warn if config files on persistent storage differ from the latest version of this image
# Warn the user if any config files in persistent storage differ from the image defaults.
for cfgPath in /usr/src/nextcloud/config/*.php; do
cfgFile=$(basename "$cfgPath")
if [ "$cfgFile" != "config.sample.php" ] && [ "$cfgFile" != "autoconfig.php" ]; then
if [ "$cfgFile" != "config.sample.php" ] \
&& [ "$cfgFile" != "autoconfig.php" ]; then
if ! cmp -s "/usr/src/nextcloud/config/$cfgFile" "/var/www/html/config/$cfgFile"; then
echo "Warning: /var/www/html/config/$cfgFile differs from the latest version of this image at /usr/src/nextcloud/config/$cfgFile"
echo "Warning: /var/www/html/config/$cfgFile differs from the image default at"
echo " /usr/src/nextcloud/config/$cfgFile"
fi
fi
done
# Trigger before-starting hook scripts (if any)
run_path before-starting
fi
###############################################################################
# Handoff to Main Container Process
###############################################################################
# Hand off to the main container process (e.g., Apache, php-fpm, etc.)
exec "$@"

Loading…
Cancel
Save