Files
projectsend/docker/production/entrypoint.sh
T
Ignacio Nelson d53bb9a2f7 Own the application directory in the official image (#1620)
The base php:*-fpm image creates /var/www/html owned by its own www-data
(uid 82) and mode 1777, so that an image can run as an arbitrary user.
This image replaces www-data with a fixed uid 1000 and copies the
release in with COPY --chown — which re-owns what it copies into the
directory, never the directory itself. It was left world-writable,
sticky, and owned by a uid the container no longer has.

fs.protected_symlinks — on by default on Ubuntu, Debian and most current
distributions — then refuses to let a non-root process follow a symlink
in such a directory, and .env is exactly that: the entrypoint keeps it
on the storage volume so a generated APP_KEY survives container
replacement, and links it into place. So every request 503'd with
"ProjectSend is not configured yet" while `docker exec ... cat .env`,
run as root, printed the file back perfectly (#1615).

Three changes, each independently sufficient for the reported case, and
deliberately so — this failure is silent and its symptom points away
from its cause:

  - the image owns /var/www/html as the runtime user, at mode 755;
  - the entrypoint owns the symlink it creates, so it stays followable
    even if that directory's mode ever drifts back;
  - preflight distinguishes "no .env" from ".env is there and cannot be
    read", instead of reporting the second as the first and sending the
    operator off to create a file they already have.

Verified by building the production image before and after: every
request 503s beforehand, with /var/www/html at uid 82 mode 1777 and
www-data denied on the symlink while root reads it; afterwards /up
answers 200, the container reports healthy, and / redirects to /setup.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:20:57 -03:00

92 lines
3.9 KiB
Bash
Executable File

#!/bin/sh
set -e
# First-boot bootstrap for the official image. Adapted from
# docker/app/entrypoint.sh, with the differences that matter in production:
#
# - The database is external and may not be up yet, so migrations wait
# for it instead of failing the container into a restart loop.
# - APP_KEY is generated once and persisted, because a key that changes
# between restarts invalidates every session and makes every encrypted
# column unreadable.
# - Nothing is bind-mounted, so there is no uid juggling to do.
#
# Deliberately NOT run: `config:cache`. It stops .env from being read, which
# silently disables TRUSTED_PROXIES — every visitor then appears to come
# from the proxy, the login rate limiter treats all users as one attacker,
# and the download log records the wrong address. See INSTALL.md.
cd /var/www/html
# storage/ is the declared volume, so anything that must outlive the
# container lives there. Recreate the tree first: a bind-mounted host
# directory arrives empty, unlike a named volume which Docker seeds from
# the image.
mkdir -p storage/app/files \
storage/framework/cache \
storage/framework/sessions \
storage/framework/views \
storage/logs
chown -R www-data:www-data storage
# .env is kept on that volume and symlinked into place, so a generated
# APP_KEY survives container replacement. A key that changes between
# restarts invalidates every session and makes every encrypted column
# permanently unreadable — silently, since nothing errors at boot.
#
# Operators who set APP_KEY (and the rest) in the environment need none of
# this: Laravel reads the environment directly and it wins over .env.
if [ ! -f storage/.env ]; then
cp .env.example storage/.env
chown www-data:www-data storage/.env
fi
#
# The symlink is owned by the runtime user, not by root who creates it:
# fs.protected_symlinks lets a process follow a symlink in a world-writable
# sticky directory only when it owns the link (or the directory). The image
# no longer leaves /var/www/html in that state, so this is the second lock
# on the same door — cheap, and it is what keeps the failure from coming
# back silently if that directory's mode ever drifts.
[ -L .env ] || ln -sf storage/.env .env
chown -h www-data:www-data .env
if [ -z "$APP_KEY" ] && ! grep -q '^APP_KEY=base64:' storage/.env; then
echo "projectsend: generating APP_KEY (first boot, persisted to the storage volume)"
su-exec www-data php artisan key:generate --force --no-interaction
fi
# Wait for the database. `migrate` against a database still starting up is
# the single most common first-run failure, and a bare failure here would
# restart-loop the container with a stack trace instead of a clear message.
if [ "$1" = "supervisord" ] || [ "$1" = "/usr/bin/supervisord" ]; then
i=0
until su-exec www-data php artisan db:show --quiet >/dev/null 2>&1; do
i=$((i + 1))
if [ "$i" -ge 60 ]; then
echo "projectsend: database unreachable after 60s — check DB_HOST, DB_DATABASE and credentials" >&2
exit 1
fi
[ "$i" = 1 ] && echo "projectsend: waiting for the database..."
sleep 1
done
su-exec www-data php artisan migrate --force
su-exec www-data php artisan storage:link --force
su-exec www-data php artisan projectsend:ensure-roles
# Unattended provisioning: create the first administrator from the
# environment. Without these, the web setup screen prompts instead.
if [ -n "$ADMIN_EMAIL" ] && [ -n "$ADMIN_PASSWORD" ]; then
su-exec www-data php artisan projectsend:admin --if-none \
--name="${ADMIN_NAME:-Administrator}" \
--email="$ADMIN_EMAIL" \
--password="$ADMIN_PASSWORD"
fi
# Cheap and safe to repeat; keeps a restarted container from serving
# stale compiled views after an image upgrade.
su-exec www-data php artisan view:clear
fi
exec "$@"