mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-14 11:12:16 +01:00
improve self-hosting permission handling
This commit is contained in:
@@ -68,6 +68,7 @@ RUN apt-get update; \
|
||||
wget \
|
||||
vim \
|
||||
git \
|
||||
gosu \
|
||||
ncdu \
|
||||
procps \
|
||||
unzip \
|
||||
@@ -193,9 +194,20 @@ COPY --link --chown=${WWWUSER}:${WWWUSER} . .
|
||||
#COPY --link --chown=${WWWUSER}:${WWWUSER} --from=build ${ROOT}/public public
|
||||
|
||||
RUN mkdir -p \
|
||||
storage/framework/{sessions,views,cache,testing} \
|
||||
storage/framework/{sessions,views,cache/data,testing} \
|
||||
storage/logs \
|
||||
bootstrap/cache && chmod -R a+rw storage
|
||||
storage/app/public \
|
||||
storage/app/private \
|
||||
bootstrap/cache && \
|
||||
ln -s ../storage/app/public public/storage && \
|
||||
chmod -R a+rw storage bootstrap/cache
|
||||
|
||||
# OpenShift / arbitrary-UID compatibility: group 0 (root group) gets read+write+execute
|
||||
# on writable paths. Any UID can run the container if it joins the root group.
|
||||
# https://docs.openshift.com/container-platform/latest/openshift_images/create-images.html
|
||||
USER root
|
||||
RUN chgrp -R 0 storage bootstrap/cache && \
|
||||
chmod -R g+rwX storage bootstrap/cache
|
||||
|
||||
#RUN composer install \
|
||||
# --classmap-authoritative \
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
[program:octane]
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = php %(ENV_ROOT)s/artisan octane:frankenphp --host=0.0.0.0 --port=8000 --admin-port=2019 --caddyfile=%(ENV_ROOT)s/docker/prod/deployment/octane/FrankenPHP/Caddyfile
|
||||
user = %(ENV_USER)s
|
||||
priority = 1
|
||||
autostart = true
|
||||
autorestart = true
|
||||
@@ -14,7 +13,6 @@ stderr_logfile_maxbytes = 0
|
||||
[program:horizon]
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = php %(ENV_ROOT)s/artisan horizon
|
||||
user = %(ENV_USER)s
|
||||
priority = 3
|
||||
autostart = %(ENV_WITH_HORIZON)s
|
||||
autorestart = true
|
||||
@@ -27,7 +25,6 @@ stopwaitsecs = 3600
|
||||
[program:scheduler]
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = supercronic -overlapping /etc/supercronic/laravel
|
||||
user = %(ENV_USER)s
|
||||
autostart = %(ENV_WITH_SCHEDULER)s
|
||||
autorestart = true
|
||||
stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
@@ -38,7 +35,6 @@ stderr_logfile_maxbytes = 200MB
|
||||
[program:clear-scheduler-cache]
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = php %(ENV_ROOT)s/artisan schedule:clear-cache
|
||||
user = %(ENV_USER)s
|
||||
autostart = %(ENV_WITH_SCHEDULER)s
|
||||
autorestart = false
|
||||
startsecs = 0
|
||||
@@ -51,7 +47,6 @@ stderr_logfile_maxbytes = 200MB
|
||||
[program:reverb]
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = php %(ENV_ROOT)s/artisan reverb:start
|
||||
user = %(ENV_USER)s
|
||||
priority = 2
|
||||
autostart = %(ENV_WITH_REVERB)s
|
||||
autorestart = true
|
||||
|
||||
@@ -1,6 +1,224 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/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.
|
||||
# SOLIDTIME_DROP_PRIVILEGES auto (default) | always | never
|
||||
# auto: if started as root, drop privileges; otherwise just exec.
|
||||
# always: if started as root, drop privileges (errors if not root).
|
||||
# never: never drop. 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
|
||||
always) SHOULD_DROP=1 ;;
|
||||
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), always, never" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "${DROP_PRIVS}" = "always" ] && [ "$(id -u)" != "0" ] && [ "${SOLIDTIME_PRIVILEGES_DROPPED:-0}" != "1" ]; then
|
||||
echo "[entrypoint] ERROR: SOLIDTIME_DROP_PRIVILEGES=always requires the container to start as root" >&2
|
||||
exit 1
|
||||
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}
|
||||
@@ -8,14 +226,16 @@ auto_db_migrate=${AUTO_DB_MIGRATE:-false}
|
||||
initialStuff() {
|
||||
echo "Container mode: $container_mode"
|
||||
|
||||
if [ ${auto_db_migrate} = "true" ]; then
|
||||
if [ "${auto_db_migrate}" = "true" ]; then
|
||||
echo "Auto database migration enabled."
|
||||
php artisan migrate --isolated --force
|
||||
fi
|
||||
|
||||
php artisan storage:link; \
|
||||
php artisan optimize:clear; \
|
||||
php artisan optimize;
|
||||
if [ ! -L "${APP_PATH}/public/storage" ]; then
|
||||
php artisan storage:link
|
||||
fi
|
||||
php artisan optimize:clear
|
||||
php artisan optimize
|
||||
}
|
||||
|
||||
if [ "$1" != "" ]; then
|
||||
@@ -23,11 +243,11 @@ if [ "$1" != "" ]; then
|
||||
elif [ "${container_mode}" = "http" ]; then
|
||||
initialStuff
|
||||
echo "Octane Server: $octane_server"
|
||||
if [ "${octane_server}" = "frankenphp" ]; then
|
||||
if [ "${octane_server}" = "frankenphp" ]; then
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.frankenphp.conf
|
||||
elif [ "${octane_server}" = "swoole" ]; then
|
||||
elif [ "${octane_server}" = "swoole" ]; then
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.swoole.conf
|
||||
elif [ "${octane_server}" = "roadrunner" ]; then
|
||||
elif [ "${octane_server}" = "roadrunner" ]; then
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.roadrunner.conf
|
||||
else
|
||||
echo "Invalid Octane server supplied."
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[supervisord]
|
||||
nodaemon = true
|
||||
user = %(ENV_USER)s
|
||||
logfile = /var/log/supervisor/supervisord.log
|
||||
pidfile = /var/run/supervisord.pid
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
[program:horizon]
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = php %(ENV_ROOT)s/artisan horizon
|
||||
user = %(ENV_USER)s
|
||||
autostart = true
|
||||
autorestart = true
|
||||
stdout_logfile = /dev/stdout
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
[program:reverb]
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = php %(ENV_ROOT)s/artisan reverb:start
|
||||
user = %(ENV_USER)s
|
||||
autostart = true
|
||||
autorestart = true
|
||||
stdout_logfile = /dev/stdout
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
[program:scheduler]
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = supercronic -overlapping /etc/supercronic/laravel
|
||||
user = %(ENV_USER)s
|
||||
autostart = true
|
||||
autorestart = true
|
||||
stdout_logfile = /dev/stdout
|
||||
@@ -12,7 +11,6 @@ stderr_logfile_maxbytes = 0
|
||||
[program:clear-scheduler-cache]
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = php %(ENV_ROOT)s/artisan schedule:clear-cache
|
||||
user = %(ENV_USER)s
|
||||
autostart = true
|
||||
autorestart = false
|
||||
startsecs = 0
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
[program:worker]
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = %(ENV_WORKER_COMMAND)s
|
||||
user = %(ENV_USER)s
|
||||
autostart = true
|
||||
autorestart = true
|
||||
stdout_logfile = /dev/stdout
|
||||
|
||||
Reference in New Issue
Block a user