diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile index d3cbca764..471ac5820 100644 --- a/contrib/docker/Dockerfile +++ b/contrib/docker/Dockerfile @@ -5,14 +5,29 @@ # Configuration ####################################################### +# See: https://github.com/composer/composer/releases ARG COMPOSER_VERSION="2.6" + +# See: https://nginx.org/ ARG NGINX_VERSION=1.25.3 + +# See: https://github.com/ddollar/forego ARG FOREGO_VERSION=0.17.2 +# See: https://github.com/hairyhenderson/gomplate +ARG GOMPLATE_VERSION=v3.11.6 + +### # PHP base configuration +### + +# See: https://hub.docker.com/_/php/tags ARG PHP_VERSION="8.1" + +# See: https://github.com/docker-library/docs/blob/master/php/README.md#image-variants ARG PHP_BASE_TYPE="apache" ARG PHP_DEBIAN_RELEASE="bullseye" + ARG RUNTIME_UID=33 # often called 'www-data' ARG RUNTIME_GID=33 # often called 'www-data' @@ -57,17 +72,31 @@ FROM nginx:${NGINX_VERSION} AS nginx-image # See: https://github.com/nginx-proxy/forego FROM nginxproxy/forego:${FOREGO_VERSION}-debian AS forego-image +# gomplate-image grabs the gomplate binary from GitHub releases +# +# It's in its own layer so it can be fetched in parallel with other build steps +FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS gomplate-image + +ARG BUILDARCH +ARG BUILDOS +ARG GOMPLATE_VERSION + +RUN set -ex \ + && curl --silent --show-error --location --output /usr/local/bin/gomplate https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${BUILDOS}-${BUILDARCH} \ + && chmod +x /usr/local/bin/gomplate \ + && /usr/local/bin/gomplate --version + ####################################################### # Base image ####################################################### FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base -ARG PHP_VERSION -ARG PHP_DEBIAN_RELEASE ARG APT_PACKAGES_EXTRA -ARG RUNTIME_UID +ARG PHP_DEBIAN_RELEASE +ARG PHP_VERSION ARG RUNTIME_GID +ARG RUNTIME_UID ARG TARGETPLATFORM ARG BUILDKIT_SBOM_SCAN_STAGE=true @@ -173,8 +202,11 @@ USER root:root FROM base AS shared-runtime -ARG RUNTIME_UID +ARG BUILDARCH +ARG BUILDOS +ARG GOMPLATE_VERSION ARG RUNTIME_GID +ARG RUNTIME_UID ENV RUNTIME_UID=${RUNTIME_UID} ENV RUNTIME_GID=${RUNTIME_GID} @@ -183,6 +215,7 @@ COPY --link --from=php-extensions /usr/local/lib/php/extensions /usr/local/lib/p COPY --link --from=php-extensions /usr/local/etc/php /usr/local/etc/php COPY --link --from=composer-and-src --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www /var/www COPY --link --from=forego-image /usr/local/bin/forego /usr/local/bin/forego +COPY --link --from=gomplate-image /usr/local/bin/gomplate /usr/local/bin/gomplate # for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862 RUN set -ex \ diff --git a/contrib/docker/README.md b/contrib/docker/README.md index 9dd46d06e..a0b07f5fa 100644 --- a/contrib/docker/README.md +++ b/contrib/docker/README.md @@ -93,6 +93,72 @@ services: PHP_BASE_TYPE: fpm ``` +## Customizing your `Dockerfile` + +### Running commands on container start + +#### Description + +When a Pixelfed container starts up, the [`ENTRYPOINT`](https://docs.docker.com/engine/reference/builder/#entrypoint) script will + +1. Search the `/docker/entrypoint.d/` directory for files and for each file (in lexical order). +1. Check if the file is executable. + 1. If the file is not executable, print an error and exit the container. +1. If the file has the extension `.envsh` the file will be [sourced](https://superuser.com/a/46146). +1. If the file has the extension `.sh` the file will be run like a normal script. +1. Any other file extension will log a warning and will be ignored. + +#### Included scripts + +* `/docker/entrypoint.d/04-defaults.envsh` calculates Docker container environment variables needed for [templating](#templating) configuration files. +* `/docker/entrypoint.d/05-templating.sh` renders [template](#templating) configuration files. +* `/docker/entrypoint.d/10-storage.sh` ensures Pixelfed storage related permissions and commands are run. +* `/docker/entrypoint.d/20-horizon.sh` ensures [Laravel Horizon](https://laravel.com/docs/master/horizon) used by Pixelfed is configured +* `/docker/entrypoint.d/30-cache.sh` ensures all Pixelfed caches (router, view, config) is warmed + +#### Disabling entrypoint or individual scripts + +To disable the entire entrypoint you can set the variable `ENTRYPOINT_SKIP=1`. + +To disable individual entrypoint scripts you can add the filename to the space (`" "`) separated variable `ENTRYPOINT_SKIP_SCRIPTS`. (example: `ENTRYPOINT_SKIP_SCRIPTS="10-storage.sh 30-cache.sh"`) + +### Templating + +The Docker container can do some basic templating (more like variable replacement) as part of the entrypoint scripts via [gomplate](https://docs.gomplate.ca/). + +Any file put in the `/docker/templates/` directory will be templated and written to the right directory. + +#### File path examples + +1. To template `/usr/local/etc/php/php.ini` in the container put the source file in `/docker/templates/usr/local/etc/php/php.ini`. +1. To template `/a/fantastic/example.txt` in the container put the source file in `/docker/templates/a/fantastic/example.txt`. +1. To template `/some/path/anywhere` in the container put the source file in `/docker/templates/a/fantastic/example.txt`. + +#### Available variables + +Variables available for templating are sourced (in order, so *last* source takes precedence) like this: + +1. `env:` in your `docker-compose.yml` or `-e` in your `docker run` / `docker compose run` +1. Any exported variables in `.envsh` files loaded *before* `05-templating.sh` (e.g. any file with `04-`, `03-`, `02-`, `01-` or `00-` prefix) +1. All key/value pairs in `/var/www/.env.docker` +1. All key/value pairs in `/var/www/.env` + +#### Template guide 101 + +Please see the [gomplate documentation](https://docs.gomplate.ca/) for a more comprehensive overview. + +The most frequent use-case you have is likely to print a environment variable (or a default value if it's missing), so this is how to do that: + +* `{{ getenv "VAR_NAME" }}` print an environment variable and **fail** if the variable is not set. ([docs](https://docs.gomplate.ca/functions/env/#envgetenv)) +* `{{ getenv "VAR_NAME" "default" }}` print an environment variable and print `default` if the variable is not set. ([docs](https://docs.gomplate.ca/functions/env/#envgetenv)) + +The script will *fail* if you reference a variable that does not exist (and don't have a default value) in a template. + +Please see the + +* [gomplate syntax documentation](https://docs.gomplate.ca/syntax/) +* [gomplate functions documentation](https://docs.gomplate.ca/functions/) + ## Build settings (arguments) The Pixelfed Dockerfile utilizes [Docker Multi-stage builds](https://docs.docker.com/build/building/multi-stage/) and [Build arguments](https://docs.docker.com/build/guide/build-args/). diff --git a/contrib/docker/nginx/root/etc/nginx/conf.d/default.conf b/contrib/docker/nginx/root/docker/templates/conf.d/default.conf similarity index 90% rename from contrib/docker/nginx/root/etc/nginx/conf.d/default.conf rename to contrib/docker/nginx/root/docker/templates/conf.d/default.conf index af5a66b77..671332e78 100644 --- a/contrib/docker/nginx/root/etc/nginx/conf.d/default.conf +++ b/contrib/docker/nginx/root/docker/templates/conf.d/default.conf @@ -1,7 +1,7 @@ server { listen 80 default_server; - server_name ${APP_DOMAIN}; + server_name {{ getenv "APP_DOMAIN" }}; root /var/www/public; add_header X-Frame-Options "SAMEORIGIN"; @@ -14,7 +14,7 @@ server { index index.html index.htm index.php; charset utf-8; - client_max_body_size ${POST_MAX_SIZE}; + client_max_body_size {{ getenv "POST_MAX_SIZE" }}; location / { try_files $uri $uri/ /index.php?$query_string; diff --git a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh index 4b9f9014a..468b10617 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -3,38 +3,37 @@ source /docker/helpers.sh set_identity "$0" -auto_envsubst() { - local template_dir="${ENVSUBST_TEMPLATE_DIR:-/docker/templates}" - local output_dir="${ENVSUBST_OUTPUT_DIR:-}" - local filter="${ENVSUBST_FILTER:-}" - local template defined_envs relative_path output_path output_dir subdir +declare template_dir="${ENVSUBST_TEMPLATE_DIR:-/docker/templates}" +declare output_dir="${ENVSUBST_OUTPUT_DIR:-}" +declare filter="${ENVSUBST_FILTER:-}" +declare template defined_envs relative_path output_path output_dir subdir - # load all dot-env files - load-config-files +# load all dot-env files +load-config-files - # export all dot-env variables so they are available in templating - export ${seen_dot_env_variables[@]} +: ${ENTRYPOINT_SHOW_TEMPLATE_DIFF:=1} - defined_envs=$(printf '${%s} ' $(awk "END { for (name in ENVIRON) { print ( name ~ /${filter}/ ) ? name : \"\" } }" "$output_path" - done -} + log "Running [gomplate] on [$template] --> [$output_path]" + cat "$template" | gomplate >"$output_path" -auto_envsubst - -exit 0 + # Show the diff from the envsubst command + if [[ ${ENTRYPOINT_SHOW_TEMPLATE_DIFF} = 1 ]]; then + git --no-pager diff "$template" "${output_path}" || : + fi +done diff --git a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh index c814a3df4..a35eeb5d3 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -3,10 +3,7 @@ source /docker/helpers.sh set_identity "$0" -log "Create the storage tree if needed" as_runtime_user cp --recursive storage.skel/* storage/ - -log "Ensure storage is linked" as_runtime_user php artisan storage:link log "Ensure permissions are correct" diff --git a/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh b/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh index e561daef9..11965fcea 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh @@ -3,11 +3,6 @@ source /docker/helpers.sh set_identity "$0" -log "==> route:cache" as_runtime_user php artisan route:cache - -log "==> view:cache" as_runtime_user php artisan view:cache - -log "==> config:cache" as_runtime_user php artisan config:cache diff --git a/contrib/docker/shared/root/docker/entrypoint.sh b/contrib/docker/shared/root/docker/entrypoint.sh index 0964980bc..dbc8aa1b5 100755 --- a/contrib/docker/shared/root/docker/entrypoint.sh +++ b/contrib/docker/shared/root/docker/entrypoint.sh @@ -1,50 +1,71 @@ #!/bin/bash set -e -o errexit -o nounset -o pipefail -[[ -n ${ENTRYPOINT_DEBUG:-} ]] && set -x +: ${ENTRYPOINT_SKIP:=0} +: ${ENTRYPOINT_SKIP_SCRIPTS:=""} +: ${ENTRYPOINT_DEBUG:=0} +: ${ENTRYPOINT_ROOT:="/docker/entrypoint.d/"} -declare -g ME="$0" -declare -gr ENTRYPOINT_ROOT=/docker/entrypoint.d/ +export ENTRYPOINT_ROOT -source /docker/helpers.sh +if [[ ${ENTRYPOINT_SKIP} == 0 ]]; then + [[ ${ENTRYPOINT_DEBUG} == 1 ]] && set -x -# ensure the entrypoint folder exists -mkdir -p "${ENTRYPOINT_ROOT}" + source /docker/helpers.sh -if /usr/bin/find "${ENTRYPOINT_ROOT}" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then - log "looking for shell scripts in /docker/entrypoint.d/" - find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r f; do - case "$f" in - *.envsh) - if [ -x "$f" ]; then - log "Sourcing $f" - source "$f" - resetore_identity - else - # warn on shell scripts without exec bit - log_warning "Ignoring $f, not executable" + declare -a skip_scripts=() + IFS=' ' read -a skip_scripts <<<"$ENTRYPOINT_SKIP_SCRIPTS" + + declare script_name + + # ensure the entrypoint folder exists + mkdir -p "${ENTRYPOINT_ROOT}" + + if /usr/bin/find "${ENTRYPOINT_ROOT}" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then + log "looking for shell scripts in /docker/entrypoint.d/" + + find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r f; do + script_name="$(get_script_name $f)" + if array_value_exists skip_scripts "${script_name}"; then + log_warning "Skipping script [${script_name}] since it's in the skip list (\$ENTRYPOINT_SKIP_SCRIPTS)" + + continue fi - ;; - *.sh) - if [ -x "$f" ]; then - log "Launching $f" - "$f" - else - # warn on shell scripts without exec bit - log_warning "Ignoring $f, not executable" - fi - ;; + case "$f" in + *.envsh) + if [ -x "$f" ]; then + log "Sourcing $f" - *) - log_warning "Ignoring $f" - ;; - esac - done + source "$f" - log "Configuration complete; ready for start up" -else - log_warning "No files found in ${ENTRYPOINT_ROOT}, skipping configuration" + resetore_identity + else + # warn on shell scripts without exec bit + log_error_and_exit "File [$f] is not executable (please 'chmod +x' it)" + fi + ;; + + *.sh) + if [ -x "$f" ]; then + log "Launching $f" + "$f" + else + # warn on shell scripts without exec bit + log_error_and_exit "File [$f] is not executable (please 'chmod +x' it)" + fi + ;; + + *) + log_warning "Ignoring $f" + ;; + esac + done + + log "Configuration complete; ready for start up" + else + log_warning "No files found in ${ENTRYPOINT_ROOT}, skipping configuration" + fi fi exec "$@" diff --git a/contrib/docker/shared/root/docker/helpers.sh b/contrib/docker/shared/root/docker/helpers.sh index fc8691324..f1ca33d09 100644 --- a/contrib/docker/shared/root/docker/helpers.sh +++ b/contrib/docker/shared/root/docker/helpers.sh @@ -10,11 +10,11 @@ declare -ra dot_env_files=( /var/www/.env.docker /var/www/.env ) -declare -a seen_dot_env_variables=() +declare -ga seen_dot_env_variables=() function set_identity() { old_log_prefix="${log_prefix}" - log_prefix="ENTRYPOINT - [${1}] - " + log_prefix="ENTRYPOINT - [$(get_script_name $1)] - " } function resetore_identity() { @@ -22,7 +22,23 @@ function resetore_identity() { } function as_runtime_user() { - su --preserve-environment $(id -un ${RUNTIME_UID}) --shell /bin/bash --command "${*}" + local -i exit_code + local target_user + + target_user=$(id -un ${RUNTIME_UID}) + + log "👷 Running [${*}] as [${target_user}]" + + su --preserve-environment "${target_user}" --shell /bin/bash --command "${*}" + exit_code=$? + + if [[ $exit_code != 0 ]]; then + log_error "❌ Error!" + return $exit_code + fi + + log "✅ OK!" + return $exit_code } # @description Display the given error message with its line number on stderr and exit with error. @@ -53,7 +69,7 @@ function log() { } function load-config-files() { - # Associative array (aka map/disctrionary) holding the unique keys found in dot-env files + # Associative array (aka map/dictionary) holding the unique keys found in dot-env files local -A _tmp_dot_env_keys for f in "${dot_env_files[@]}"; do @@ -73,3 +89,14 @@ function load-config-files() { seen_dot_env_variables=(${!_tmp_dot_env_keys[@]}) } + +function array_value_exists() { + local -nr validOptions=$1 + local -r providedValue="\<${2}\>" + + [[ ${validOptions[*]} =~ $providedValue ]] +} + +function get_script_name() { + echo "${1#"$ENTRYPOINT_ROOT"}" +} diff --git a/contrib/docker/shared/root/docker/install/base.sh b/contrib/docker/shared/root/docker/install/base.sh index b0e3d7b6d..b9b37b031 100755 --- a/contrib/docker/shared/root/docker/install/base.sh +++ b/contrib/docker/shared/root/docker/install/base.sh @@ -15,7 +15,7 @@ echo 'APT::Install-Suggests "false";' >>/etc/apt/apt.conf declare -ra standardPackages=( apt-utils ca-certificates - gettext-base + curl git gnupg1 gosu @@ -25,9 +25,10 @@ declare -ra standardPackages=( locales-all nano procps - unzip - zip software-properties-common + unzip + wget + zip ) # Image Optimization diff --git a/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini b/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini index 1a9f6b598..81ba3d207 100644 --- a/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini +++ b/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini @@ -679,7 +679,7 @@ auto_globals_jit = On ; Its value may be 0 to disable the limit. It is ignored if POST data reading ; is disabled through enable_post_data_reading. ; http://php.net/post-max-size -post_max_size = ${POST_MAX_SIZE} +post_max_size = {{ getenv "POST_MAX_SIZE" }} ; Automatically add files before PHP document. ; http://php.net/auto-prepend-file @@ -831,10 +831,10 @@ file_uploads = On ; Maximum allowed size for uploaded files. ; http://php.net/upload-max-filesize -upload_max_filesize = ${POST_MAX_SIZE} +upload_max_filesize = {{ getenv "POST_MAX_SIZE" }} ; Maximum number of files that can be uploaded via a single request -max_file_uploads = ${MAX_ALBUM_LENGTH} +max_file_uploads = {{ getenv "MAX_ALBUM_LENGTH" }} ;;;;;;;;;;;;;;;;;; ; Fopen wrappers ;