mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 08:12:17 +01:00
Compare commits
4 Commits
feature/vo
...
feature/do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
595132ec2c | ||
|
|
0659cb3993 | ||
|
|
e8fc6fd77e | ||
|
|
a04185921d |
258
.github/workflows/image-smoke-test.yml
vendored
Normal file
258
.github/workflows/image-smoke-test.yml
vendored
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
name: Image Smoke Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'docker/prod/**'
|
||||||
|
- '.github/workflows/image-smoke-test.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
smoke:
|
||||||
|
name: Smoke (${{ matrix.mode }})
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 30
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
mode:
|
||||||
|
- default
|
||||||
|
- puid-pgid
|
||||||
|
- openshift
|
||||||
|
- drop-never
|
||||||
|
- diagnostic
|
||||||
|
- puid-mismatch-warning
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Copy .env template
|
||||||
|
run: |
|
||||||
|
cp .env.production .env
|
||||||
|
rm .env.production .env.ci .env.example
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: mbstring, dom, fileinfo, pgsql
|
||||||
|
|
||||||
|
- name: Composer install
|
||||||
|
run: composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
|
||||||
|
- name: NPM ci
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: NPM build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build smoke image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/prod/Dockerfile
|
||||||
|
build-args: |
|
||||||
|
DOCKER_FILES_BASE_PATH=docker/prod/
|
||||||
|
load: true
|
||||||
|
tags: solidtime-smoke:test
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: "Smoke: default (image config + fresh deploy with empty bind mounts)"
|
||||||
|
if: matrix.mode == 'default'
|
||||||
|
run: |
|
||||||
|
echo "[smoke] image's default USER is root (entrypoint needs root to drop privs)"
|
||||||
|
user=$(docker inspect --format '{{.Config.User}}' solidtime-smoke:test)
|
||||||
|
if [ "$user" != "root" ]; then
|
||||||
|
echo "Expected 'root', got '$user'. The Dockerfile must end with USER root so the entrypoint can chown/usermod and drop privileges."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[smoke] storage tree is group-0 owned (OpenShift / arbitrary-UID compat)"
|
||||||
|
group=$(docker run --rm --entrypoint stat solidtime-smoke:test -c '%g' /var/www/html/storage)
|
||||||
|
if [ "$group" != "0" ]; then
|
||||||
|
echo "Expected group 0, got '$group'. The Dockerfile must chgrp -R 0 storage bootstrap/cache so arbitrary-UID containers can write."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p test-storage test-cache
|
||||||
|
docker run --rm \
|
||||||
|
-v "$(pwd)/test-storage:/var/www/html/storage" \
|
||||||
|
-v "$(pwd)/test-cache:/var/www/html/bootstrap/cache" \
|
||||||
|
solidtime-smoke:test \
|
||||||
|
sh -c '
|
||||||
|
set -e
|
||||||
|
echo "[smoke] framework subdirs exist"
|
||||||
|
test -d /var/www/html/storage/framework/cache/data
|
||||||
|
test -d /var/www/html/storage/framework/sessions
|
||||||
|
test -d /var/www/html/storage/framework/views
|
||||||
|
test -d /var/www/html/storage/framework/testing
|
||||||
|
test -d /var/www/html/storage/logs
|
||||||
|
test -d /var/www/html/storage/app/public
|
||||||
|
test -d /var/www/html/storage/app/private
|
||||||
|
test -d /var/www/html/bootstrap/cache
|
||||||
|
echo "[smoke] storage is writable"
|
||||||
|
touch /var/www/html/storage/framework/cache/data/test-file
|
||||||
|
echo "[smoke] running as octane (UID 1000)"
|
||||||
|
[ "$(id -u)" = "1000" ]
|
||||||
|
echo "[smoke] PASS"
|
||||||
|
'
|
||||||
|
|
||||||
|
- name: "Smoke: PUID/PGID remap"
|
||||||
|
if: matrix.mode == 'puid-pgid'
|
||||||
|
run: |
|
||||||
|
mkdir -p test-storage test-cache
|
||||||
|
sudo chown -R 1501:1501 test-storage test-cache
|
||||||
|
docker run --rm \
|
||||||
|
-e PUID=1501 -e PGID=1501 \
|
||||||
|
-v "$(pwd)/test-storage:/var/www/html/storage" \
|
||||||
|
-v "$(pwd)/test-cache:/var/www/html/bootstrap/cache" \
|
||||||
|
solidtime-smoke:test \
|
||||||
|
sh -c '
|
||||||
|
set -e
|
||||||
|
echo "[smoke] running as remapped UID/GID 1501"
|
||||||
|
[ "$(id -u)" = "1501" ]
|
||||||
|
[ "$(id -g)" = "1501" ]
|
||||||
|
echo "[smoke] storage is writable as 1501"
|
||||||
|
touch /var/www/html/storage/framework/cache/data/test-file
|
||||||
|
echo "[smoke] PASS"
|
||||||
|
'
|
||||||
|
|
||||||
|
- name: "Smoke: OpenShift / arbitrary UID + group 0"
|
||||||
|
if: matrix.mode == 'openshift'
|
||||||
|
run: |
|
||||||
|
mkdir -p test-storage test-cache
|
||||||
|
sudo chown -R 2000:0 test-storage test-cache
|
||||||
|
sudo chmod -R g+rwX test-storage test-cache
|
||||||
|
docker run --rm --user 2000:0 \
|
||||||
|
-v "$(pwd)/test-storage:/var/www/html/storage" \
|
||||||
|
-v "$(pwd)/test-cache:/var/www/html/bootstrap/cache" \
|
||||||
|
solidtime-smoke:test \
|
||||||
|
sh -c '
|
||||||
|
set -e
|
||||||
|
echo "[smoke] running as arbitrary UID 2000, group 0"
|
||||||
|
[ "$(id -u)" = "2000" ]
|
||||||
|
[ "$(id -g)" = "0" ]
|
||||||
|
echo "[smoke] storage is writable via group 0"
|
||||||
|
touch /var/www/html/storage/framework/cache/data/test-file
|
||||||
|
echo "[smoke] PASS"
|
||||||
|
'
|
||||||
|
|
||||||
|
- name: "Smoke: SOLIDTIME_DROP_PRIVILEGES=never (run as root)"
|
||||||
|
if: matrix.mode == 'drop-never'
|
||||||
|
run: |
|
||||||
|
mkdir -p test-storage test-cache
|
||||||
|
docker run --rm \
|
||||||
|
-e SOLIDTIME_DROP_PRIVILEGES=never \
|
||||||
|
-v "$(pwd)/test-storage:/var/www/html/storage" \
|
||||||
|
-v "$(pwd)/test-cache:/var/www/html/bootstrap/cache" \
|
||||||
|
solidtime-smoke:test \
|
||||||
|
sh -c '
|
||||||
|
set -e
|
||||||
|
echo "[smoke] running as root (privilege drop disabled)"
|
||||||
|
[ "$(id -u)" = "0" ]
|
||||||
|
echo "[smoke] bootstrap still ran"
|
||||||
|
test -d /var/www/html/storage/framework/cache/data
|
||||||
|
echo "[smoke] storage writable as root"
|
||||||
|
touch /var/www/html/storage/framework/cache/data/test-file
|
||||||
|
echo "[smoke] PASS"
|
||||||
|
'
|
||||||
|
|
||||||
|
- name: "Smoke: PUID set + started non-root prints a warning but continues"
|
||||||
|
if: matrix.mode == 'puid-mismatch-warning'
|
||||||
|
run: |
|
||||||
|
mkdir -p test-storage test-cache
|
||||||
|
sudo chown -R 1500:1500 test-storage test-cache
|
||||||
|
|
||||||
|
set +e
|
||||||
|
docker run --rm \
|
||||||
|
--user 1500:1500 \
|
||||||
|
-e PUID=1500 -e PGID=1500 \
|
||||||
|
-v "$(pwd)/test-storage:/var/www/html/storage" \
|
||||||
|
-v "$(pwd)/test-cache:/var/www/html/bootstrap/cache" \
|
||||||
|
solidtime-smoke:test \
|
||||||
|
sh -c '
|
||||||
|
set -e
|
||||||
|
echo "[smoke] running as 1500 (user: directive wins)"
|
||||||
|
[ "$(id -u)" = "1500" ]
|
||||||
|
echo "[smoke] storage is writable as 1500"
|
||||||
|
touch /var/www/html/storage/framework/cache/data/test-file
|
||||||
|
echo "[smoke] container completed successfully"
|
||||||
|
' \
|
||||||
|
>stdout.log 2>stderr.log
|
||||||
|
exit_code=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "[smoke] exit code: $exit_code"
|
||||||
|
echo "--- stderr ---"
|
||||||
|
cat stderr.log
|
||||||
|
echo "--- end stderr ---"
|
||||||
|
|
||||||
|
if [ "$exit_code" -ne 0 ]; then
|
||||||
|
echo "Expected the entrypoint to continue (warning is non-fatal)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for needle in "PUID/PGID is set but the container started as UID" "remove any 'user:' directive" "Continuing as UID"; do
|
||||||
|
if ! grep -q "$needle" stderr.log; then
|
||||||
|
echo "Missing warning fragment: $needle"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "[smoke] PASS"
|
||||||
|
|
||||||
|
- name: "Smoke: diagnostic error path (read-only storage mount)"
|
||||||
|
if: matrix.mode == 'diagnostic'
|
||||||
|
run: |
|
||||||
|
# Pre-create the full storage tree on the host so the entrypoint's
|
||||||
|
# bootstrap_storage_tree() is a no-op (mkdir -p on existing dirs
|
||||||
|
# returns 0 even on a read-only mount). The write test then fires
|
||||||
|
# against the RO mount and triggers our diagnostic.
|
||||||
|
mkdir -p test-storage/framework/cache/data \
|
||||||
|
test-storage/framework/sessions \
|
||||||
|
test-storage/framework/views \
|
||||||
|
test-storage/framework/testing \
|
||||||
|
test-storage/logs \
|
||||||
|
test-storage/app/public \
|
||||||
|
test-storage/app/private \
|
||||||
|
test-cache
|
||||||
|
|
||||||
|
set +e
|
||||||
|
docker run --rm \
|
||||||
|
-v "$(pwd)/test-storage:/var/www/html/storage:ro" \
|
||||||
|
-v "$(pwd)/test-cache:/var/www/html/bootstrap/cache:ro" \
|
||||||
|
solidtime-smoke:test \
|
||||||
|
true \
|
||||||
|
>stdout.log 2>stderr.log
|
||||||
|
exit_code=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "[smoke] exit code: $exit_code"
|
||||||
|
echo "--- stderr ---"
|
||||||
|
cat stderr.log
|
||||||
|
echo "--- end stderr ---"
|
||||||
|
|
||||||
|
if [ "$exit_code" -eq 0 ]; then
|
||||||
|
echo "Expected the entrypoint to exit non-zero on an unwritable storage mount."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for needle in "not writable" "PUID=" "permissions"; do
|
||||||
|
if ! grep -q "$needle" stderr.log; then
|
||||||
|
echo "Missing diagnostic fragment: $needle"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "[smoke] PASS"
|
||||||
@@ -68,6 +68,7 @@ RUN apt-get update; \
|
|||||||
wget \
|
wget \
|
||||||
vim \
|
vim \
|
||||||
git \
|
git \
|
||||||
|
gosu \
|
||||||
ncdu \
|
ncdu \
|
||||||
procps \
|
procps \
|
||||||
unzip \
|
unzip \
|
||||||
@@ -193,9 +194,20 @@ COPY --link --chown=${WWWUSER}:${WWWUSER} . .
|
|||||||
#COPY --link --chown=${WWWUSER}:${WWWUSER} --from=build ${ROOT}/public public
|
#COPY --link --chown=${WWWUSER}:${WWWUSER} --from=build ${ROOT}/public public
|
||||||
|
|
||||||
RUN mkdir -p \
|
RUN mkdir -p \
|
||||||
storage/framework/{sessions,views,cache,testing} \
|
storage/framework/{sessions,views,cache/data,testing} \
|
||||||
storage/logs \
|
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 \
|
#RUN composer install \
|
||||||
# --classmap-authoritative \
|
# --classmap-authoritative \
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
[program:octane]
|
[program:octane]
|
||||||
process_name = %(program_name)s_%(process_num)s
|
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
|
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
|
priority = 1
|
||||||
autostart = true
|
autostart = true
|
||||||
autorestart = true
|
autorestart = true
|
||||||
@@ -14,7 +13,6 @@ stderr_logfile_maxbytes = 0
|
|||||||
[program:horizon]
|
[program:horizon]
|
||||||
process_name = %(program_name)s_%(process_num)s
|
process_name = %(program_name)s_%(process_num)s
|
||||||
command = php %(ENV_ROOT)s/artisan horizon
|
command = php %(ENV_ROOT)s/artisan horizon
|
||||||
user = %(ENV_USER)s
|
|
||||||
priority = 3
|
priority = 3
|
||||||
autostart = %(ENV_WITH_HORIZON)s
|
autostart = %(ENV_WITH_HORIZON)s
|
||||||
autorestart = true
|
autorestart = true
|
||||||
@@ -27,7 +25,6 @@ stopwaitsecs = 3600
|
|||||||
[program:scheduler]
|
[program:scheduler]
|
||||||
process_name = %(program_name)s_%(process_num)s
|
process_name = %(program_name)s_%(process_num)s
|
||||||
command = supercronic -overlapping /etc/supercronic/laravel
|
command = supercronic -overlapping /etc/supercronic/laravel
|
||||||
user = %(ENV_USER)s
|
|
||||||
autostart = %(ENV_WITH_SCHEDULER)s
|
autostart = %(ENV_WITH_SCHEDULER)s
|
||||||
autorestart = true
|
autorestart = true
|
||||||
stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
||||||
@@ -38,7 +35,6 @@ stderr_logfile_maxbytes = 200MB
|
|||||||
[program:clear-scheduler-cache]
|
[program:clear-scheduler-cache]
|
||||||
process_name = %(program_name)s_%(process_num)s
|
process_name = %(program_name)s_%(process_num)s
|
||||||
command = php %(ENV_ROOT)s/artisan schedule:clear-cache
|
command = php %(ENV_ROOT)s/artisan schedule:clear-cache
|
||||||
user = %(ENV_USER)s
|
|
||||||
autostart = %(ENV_WITH_SCHEDULER)s
|
autostart = %(ENV_WITH_SCHEDULER)s
|
||||||
autorestart = false
|
autorestart = false
|
||||||
startsecs = 0
|
startsecs = 0
|
||||||
@@ -51,7 +47,6 @@ stderr_logfile_maxbytes = 200MB
|
|||||||
[program:reverb]
|
[program:reverb]
|
||||||
process_name = %(program_name)s_%(process_num)s
|
process_name = %(program_name)s_%(process_num)s
|
||||||
command = php %(ENV_ROOT)s/artisan reverb:start
|
command = php %(ENV_ROOT)s/artisan reverb:start
|
||||||
user = %(ENV_USER)s
|
|
||||||
priority = 2
|
priority = 2
|
||||||
autostart = %(ENV_WITH_REVERB)s
|
autostart = %(ENV_WITH_REVERB)s
|
||||||
autorestart = true
|
autorestart = true
|
||||||
|
|||||||
@@ -1,6 +1,240 @@
|
|||||||
#!/usr/bin/env sh
|
#!/bin/bash
|
||||||
set -e
|
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"}
|
container_mode=${CONTAINER_MODE:-"http"}
|
||||||
octane_server=${OCTANE_SERVER}
|
octane_server=${OCTANE_SERVER}
|
||||||
auto_db_migrate=${AUTO_DB_MIGRATE:-false}
|
auto_db_migrate=${AUTO_DB_MIGRATE:-false}
|
||||||
@@ -8,14 +242,16 @@ auto_db_migrate=${AUTO_DB_MIGRATE:-false}
|
|||||||
initialStuff() {
|
initialStuff() {
|
||||||
echo "Container mode: $container_mode"
|
echo "Container mode: $container_mode"
|
||||||
|
|
||||||
if [ ${auto_db_migrate} = "true" ]; then
|
if [ "${auto_db_migrate}" = "true" ]; then
|
||||||
echo "Auto database migration enabled."
|
echo "Auto database migration enabled."
|
||||||
php artisan migrate --isolated --force
|
php artisan migrate --isolated --force
|
||||||
fi
|
fi
|
||||||
|
|
||||||
php artisan storage:link; \
|
if [ ! -L "${APP_PATH}/public/storage" ]; then
|
||||||
php artisan optimize:clear; \
|
php artisan storage:link
|
||||||
php artisan optimize;
|
fi
|
||||||
|
php artisan optimize:clear
|
||||||
|
php artisan optimize
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ "$1" != "" ]; then
|
if [ "$1" != "" ]; then
|
||||||
@@ -23,11 +259,11 @@ if [ "$1" != "" ]; then
|
|||||||
elif [ "${container_mode}" = "http" ]; then
|
elif [ "${container_mode}" = "http" ]; then
|
||||||
initialStuff
|
initialStuff
|
||||||
echo "Octane Server: $octane_server"
|
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
|
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
|
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
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.roadrunner.conf
|
||||||
else
|
else
|
||||||
echo "Invalid Octane server supplied."
|
echo "Invalid Octane server supplied."
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
[supervisord]
|
[supervisord]
|
||||||
nodaemon = true
|
nodaemon = true
|
||||||
user = %(ENV_USER)s
|
|
||||||
logfile = /var/log/supervisor/supervisord.log
|
logfile = /var/log/supervisor/supervisord.log
|
||||||
pidfile = /var/run/supervisord.pid
|
pidfile = /var/run/supervisord.pid
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
[program:horizon]
|
[program:horizon]
|
||||||
process_name = %(program_name)s_%(process_num)s
|
process_name = %(program_name)s_%(process_num)s
|
||||||
command = php %(ENV_ROOT)s/artisan horizon
|
command = php %(ENV_ROOT)s/artisan horizon
|
||||||
user = %(ENV_USER)s
|
|
||||||
autostart = true
|
autostart = true
|
||||||
autorestart = true
|
autorestart = true
|
||||||
stdout_logfile = /dev/stdout
|
stdout_logfile = /dev/stdout
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
[program:reverb]
|
[program:reverb]
|
||||||
process_name = %(program_name)s_%(process_num)s
|
process_name = %(program_name)s_%(process_num)s
|
||||||
command = php %(ENV_ROOT)s/artisan reverb:start
|
command = php %(ENV_ROOT)s/artisan reverb:start
|
||||||
user = %(ENV_USER)s
|
|
||||||
autostart = true
|
autostart = true
|
||||||
autorestart = true
|
autorestart = true
|
||||||
stdout_logfile = /dev/stdout
|
stdout_logfile = /dev/stdout
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
[program:scheduler]
|
[program:scheduler]
|
||||||
process_name = %(program_name)s_%(process_num)s
|
process_name = %(program_name)s_%(process_num)s
|
||||||
command = supercronic -overlapping /etc/supercronic/laravel
|
command = supercronic -overlapping /etc/supercronic/laravel
|
||||||
user = %(ENV_USER)s
|
|
||||||
autostart = true
|
autostart = true
|
||||||
autorestart = true
|
autorestart = true
|
||||||
stdout_logfile = /dev/stdout
|
stdout_logfile = /dev/stdout
|
||||||
@@ -12,7 +11,6 @@ stderr_logfile_maxbytes = 0
|
|||||||
[program:clear-scheduler-cache]
|
[program:clear-scheduler-cache]
|
||||||
process_name = %(program_name)s_%(process_num)s
|
process_name = %(program_name)s_%(process_num)s
|
||||||
command = php %(ENV_ROOT)s/artisan schedule:clear-cache
|
command = php %(ENV_ROOT)s/artisan schedule:clear-cache
|
||||||
user = %(ENV_USER)s
|
|
||||||
autostart = true
|
autostart = true
|
||||||
autorestart = false
|
autorestart = false
|
||||||
startsecs = 0
|
startsecs = 0
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
[program:worker]
|
[program:worker]
|
||||||
process_name = %(program_name)s_%(process_num)s
|
process_name = %(program_name)s_%(process_num)s
|
||||||
command = %(ENV_WORKER_COMMAND)s
|
command = %(ENV_WORKER_COMMAND)s
|
||||||
user = %(ENV_USER)s
|
|
||||||
autostart = true
|
autostart = true
|
||||||
autorestart = true
|
autorestart = true
|
||||||
stdout_logfile = /dev/stdout
|
stdout_logfile = /dev/stdout
|
||||||
|
|||||||
Reference in New Issue
Block a user