mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-06-06 20:37:32 +00:00
292 lines
10 KiB
Bash
292 lines
10 KiB
Bash
#!/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 <<EOF
|
|
[entrypoint] WARNING: PUID/PGID is set but the container started as UID $(id -u) (not root).
|
|
[entrypoint] WARNING: PUID/PGID only apply when the entrypoint runs as root and drops privileges.
|
|
[entrypoint] WARNING:
|
|
[entrypoint] WARNING: To use PUID/PGID: remove any 'user:' directive from your compose file.
|
|
[entrypoint] WARNING: To run as a fixed UID: remove PUID/PGID from your env.
|
|
[entrypoint] WARNING:
|
|
[entrypoint] WARNING: Continuing as UID $(id -u). See:
|
|
[entrypoint] WARNING: https://docs.solidtime.io/self-hosting/guides/permissions
|
|
|
|
EOF
|
|
fi
|
|
|
|
bootstrap_storage_tree() {
|
|
mkdir -p "${WRITABLE_PATHS[@]}" 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 <<EOF
|
|
[entrypoint] NOTE: ${STORAGE_PATH} is owned by UID ${owner_uid}:${owner_gid},
|
|
[entrypoint] but the container is starting as UID ${TARGET_UID}:${TARGET_GID}.
|
|
[entrypoint] Files will be chowned to ${TARGET_UID}:${TARGET_GID} and may
|
|
[entrypoint] appear with an unfamiliar owner on the host.
|
|
[entrypoint]
|
|
[entrypoint] If you want the container to write as UID ${owner_uid} (common
|
|
[entrypoint] on Synology / TrueNAS / Unraid where host users aren't UID
|
|
[entrypoint] 1000), set in your env and restart:
|
|
[entrypoint]
|
|
[entrypoint] PUID=${owner_uid}
|
|
[entrypoint] PGID=${owner_gid}
|
|
[entrypoint]
|
|
[entrypoint] More: https://docs.solidtime.io/self-hosting/guides/permissions
|
|
|
|
EOF
|
|
}
|
|
|
|
print_write_test_failure() {
|
|
local owner
|
|
owner="$(stat -c '%u:%g' "${STORAGE_PATH}" 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 <<EOF
|
|
|
|
============================================================
|
|
ERROR: Solidtime writable directories are not writable.
|
|
|
|
Diagnosis:
|
|
Container will run as: UID ${runtime_uid}, GID ${runtime_gid}
|
|
Storage directory owner: ${owner}
|
|
|
|
Likely cause: a bind-mounted host directory is owned by a different
|
|
user than the container's application user.
|
|
|
|
Fix on the host:
|
|
|
|
sudo chown -R ${runtime_uid}:${runtime_gid} <your-bind-mount-path>
|
|
|
|
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
|