#!/bin/bash set -e # ============================================================================ # Solidtime container entrypoint. # # Layout: # 1. Storage tree bootstrap (idempotent, runs as any user) # 2. UID/GID remap + chown (root only, controlled by PUID/PGID env vars) # 3. Pre-flight write test (fails fast with a diagnosis message) # 4. Privilege drop via gosu, then re-exec self as APP_USER # 5. Original CONTAINER_MODE routing (runs as APP_USER) # # Env vars: # PUID, PGID UID/GID for the application user. Defaults 1000:1000. # Only takes effect when the container starts as root # (which is the image's default — if you set a # `user:` directive in compose, PUID/PGID are ignored # and a startup warning is printed). # SOLIDTIME_DROP_PRIVILEGES auto (default) | never # auto: if started as root, drop privileges to APP_USER; otherwise just exec. # never: never drop privileges. Run as whatever UID/GID was started. # ============================================================================ APP_USER="octane" APP_PATH="${ROOT:-/var/www/html}" STORAGE_PATH="${APP_PATH}/storage" CACHE_PATH="${APP_PATH}/bootstrap/cache" DEFAULT_UID=1000 DEFAULT_GID=1000 TARGET_UID="${PUID:-${DEFAULT_UID}}" TARGET_GID="${PGID:-${DEFAULT_GID}}" DROP_PRIVS="${SOLIDTIME_DROP_PRIVILEGES:-auto}" WRITABLE_PATHS=( "${STORAGE_PATH}/framework/cache/data" "${STORAGE_PATH}/framework/sessions" "${STORAGE_PATH}/framework/views" "${STORAGE_PATH}/framework/testing" "${STORAGE_PATH}/logs" "${STORAGE_PATH}/app/public" "${STORAGE_PATH}/app/private" "${CACHE_PATH}" ) case "${DROP_PRIVS}" in never) SHOULD_DROP=0 ;; auto) if [ "$(id -u)" = "0" ]; then SHOULD_DROP=1 else SHOULD_DROP=0 fi ;; *) echo "[entrypoint] ERROR: invalid SOLIDTIME_DROP_PRIVILEGES='${DROP_PRIVS}'" >&2 echo "[entrypoint] Valid values: auto (default), never" >&2 exit 1 ;; esac # Warn if PUID/PGID are set but the container started non-root. PUID/PGID only # take effect during the drop-privileges flow, which requires starting as root. # A common cause is leaving `user:` in the compose file alongside PUID env vars. if { [ -n "${PUID}" ] || [ -n "${PGID}" ]; } \ && [ "$(id -u)" != "0" ] \ && [ "${SOLIDTIME_PRIVILEGES_DROPPED:-0}" != "1" ]; then cat >&2 </dev/null || return 1 } # Proactive warning when the existing storage directory is owned by a non-default # UID (typical on NAS systems where host users aren't UID 1000) and PUID/PGID # aren't set. Without this nudge, the chown step silently re-owns the files to # 1000:1000 and the user only discovers the mismatch later when host-side tools # (backup, file browser, rsync) show unfamiliar ownership. maybe_warn_ownership_mismatch() { [ "${SHOULD_DROP}" = "1" ] || return 0 [ -n "${PUID}" ] && return 0 [ -n "${PGID}" ] && return 0 [ -d "${STORAGE_PATH}" ] || return 0 local owner_uid owner_gid owner_uid="$(stat -c '%u' "${STORAGE_PATH}" 2>/dev/null)" || return 0 owner_gid="$(stat -c '%g' "${STORAGE_PATH}" 2>/dev/null)" || return 0 # Root-owned: probably freshly created by the entrypoint, will be chowned shortly. [ "${owner_uid}" = "0" ] && return 0 # Already the target: nothing to warn about. [ "${owner_uid}" = "${TARGET_UID}" ] && return 0 cat >&2 </dev/null || echo unknown)" local runtime_uid local runtime_gid if [ "$(id -u)" = "0" ] && [ "${SHOULD_DROP}" = "1" ]; then runtime_uid="${TARGET_UID}" runtime_gid="${TARGET_GID}" else runtime_uid="$(id -u)" runtime_gid="$(id -g)" fi local owner_uid owner_uid="$(stat -c '%u' "${STORAGE_PATH}" 2>/dev/null || echo 1000)" local owner_gid owner_gid="$(stat -c '%g' "${STORAGE_PATH}" 2>/dev/null || echo 1000)" cat >&2 < Or set PUID/PGID to match the host directory owner: PUID=${owner_uid} PGID=${owner_gid} To run intentionally as root, set: SOLIDTIME_DROP_PRIVILEGES=never For more help: https://docs.solidtime.io/self-hosting/guides/permissions ============================================================ EOF } write_test_as_user() { local user="$1" local script=' set -e for dir in "$@"; do test_file="${dir}/.solidtime-write-test" touch "${test_file}" rm -f "${test_file}" done ' if [ -n "${user}" ]; then gosu "${user}" sh -c "${script}" sh "${WRITABLE_PATHS[@]}" 2>/dev/null else sh -c "${script}" sh "${WRITABLE_PATHS[@]}" 2>/dev/null fi } # ---------------------------------------------------------------------------- # Root preamble: bootstrap, remap, chown, write-test, then drop and re-exec. # ---------------------------------------------------------------------------- if [ "$(id -u)" = "0" ]; then if ! bootstrap_storage_tree; then echo "[entrypoint] ERROR: failed to create storage subdirectories at ${STORAGE_PATH}" >&2 exit 1 fi if [ "${SHOULD_DROP}" = "1" ]; then maybe_warn_ownership_mismatch if [ "${TARGET_UID}" != "${DEFAULT_UID}" ] || [ "${TARGET_GID}" != "${DEFAULT_GID}" ]; then echo "[entrypoint] Remapping ${APP_USER} to ${TARGET_UID}:${TARGET_GID}" groupmod -o -g "${TARGET_GID}" "${APP_USER}" usermod -o -u "${TARGET_UID}" "${APP_USER}" fi # Idempotent chown: only fix entries whose owner or group is wrong. # On large storage volumes (lots of user uploads) this is dramatically # faster than a blanket `chown -R` every restart. Pattern borrowed from # docker-library/postgres and linuxserver.io's baseimage. find "${STORAGE_PATH}" "${CACHE_PATH}" \ \( ! -user "${TARGET_UID}" -o ! -group "${TARGET_GID}" \) \ -exec chown "${TARGET_UID}:${TARGET_GID}" {} + 2>/dev/null || true if ! write_test_as_user "${APP_USER}"; then print_write_test_failure exit 1 fi exec gosu "${APP_USER}" env SOLIDTIME_PRIVILEGES_DROPPED=1 "$0" "$@" fi if ! write_test_as_user ""; then print_write_test_failure exit 1 fi else if ! bootstrap_storage_tree; then echo "[entrypoint] WARNING: could not create some storage subdirectories at ${STORAGE_PATH} (will continue if existing tree is writable)" >&2 fi if ! write_test_as_user ""; then print_write_test_failure exit 1 fi fi # ---------------------------------------------------------------------------- # Application: runs as APP_USER (or whatever non-root UID was started). # ---------------------------------------------------------------------------- unset SOLIDTIME_PRIVILEGES_DROPPED container_mode=${CONTAINER_MODE:-"http"} octane_server=${OCTANE_SERVER} auto_db_migrate=${AUTO_DB_MIGRATE:-false} initialStuff() { echo "Container mode: $container_mode" if [ "${auto_db_migrate}" = "true" ]; then echo "Auto database migration enabled." php artisan migrate --isolated --force fi if [ ! -L "${APP_PATH}/public/storage" ]; then php artisan storage:link fi php artisan optimize:clear php artisan optimize } if [ "$1" != "" ]; then exec "$@" elif [ "${container_mode}" = "http" ]; then initialStuff echo "Octane Server: $octane_server" if [ "${octane_server}" = "frankenphp" ]; then exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.frankenphp.conf elif [ "${octane_server}" = "swoole" ]; then exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.swoole.conf elif [ "${octane_server}" = "roadrunner" ]; then exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.roadrunner.conf else echo "Invalid Octane server supplied." exit 1 fi elif [ "${container_mode}" = "horizon" ]; then initialStuff exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.horizon.conf elif [ "${container_mode}" = "reverb" ]; then initialStuff exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.reverb.conf elif [ "${container_mode}" = "scheduler" ]; then initialStuff exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.scheduler.conf elif [ "${container_mode}" = "worker" ]; then if [ -z "${WORKER_COMMAND}" ]; then echo "WORKER_COMMAND is undefined." exit 1 fi initialStuff exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.worker.conf else echo "Container mode mismatched." exit 1 fi