#1658 reports that app, web, db and redis have no restart policy, so the
stack does not come back after a reboot. True, and fixed here — but the
file it is about is the development stack, and the production example has
had the policy all along. The reporter got there by following DOCKER.md,
which is #1627 again: 745f24c fixed the README's pointer and left this
page's body describing a stack no user should be running.
Against an image install almost every procedure on it was wrong. It said
uploads live in `storage/app/files/` "in the project directory" and `.env`
beside it — both are on the storage volume, and the entrypoint generates
that `.env` itself. Its compose.override.yaml recipe bind-mounted into
app, web, worker and scheduler, which are one container under supervisord
in the image, at a path one level too deep to carry APP_KEY. It told
people to chown a directory the entrypoint already chowns, to rsync from a
host path that does not exist, and to `git pull` to upgrade. Its mysqldump
read ${DB_ROOT_PASSWORD} from a .env an image install does not have, so
the documented backup silently fell back to `root` and failed. Docker Hub
links this page as "where your data lives, backups, moving to another
server".
So it is now about the image, and shorter for it: two volumes instead of
three loose things, the key explained where people actually lose it, no
override file because the compose file is the operator's own, and a
reboot section — the answer to the issue for anyone who wrote their own
compose. The clone-and-build stack keeps one pointer to CONTRIBUTING.md,
which has been the correct place for it since #1627.
The Docker Hub page keeps the two facts a reader who never leaves it
needs and hands off the procedures, so the drift that caused this has one
copy to go wrong instead of two.
Adminer and mailpit stay without a restart policy on purpose: those come
up for a session, not for the life of the machine.
Refs #1658
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An update finished and nothing said so. The dashboard looked identical to
yesterday's, and whatever the release brought was in a file nobody opens.
The first time the installation's administrator opens ProjectSend after
an update, they now land on a page that names the version they are on,
invites them to the Discord — the same invitation update.sh prints, made
again where they are actually looking — and then lays out what the
release brought.
The notes come from CHANGELOG.md inside the release, not from GitHub: the
one moment this page exists for is the moment after an update, possibly
on a server with no outbound access, describing code already on disk.
Parsed rather than rendered, so nothing in it can become HTML.
Once, and to one person. The update happened to the installation, so
greeting five staff members — each having to dismiss a page they did not
ask for — would turn a pleasant moment into a support question. It goes
to the oldest active administrator, which on any installation that went
through setup is whoever set it up. No owner flag was invented for this:
administrators are equal in authority, and changing that for a greeting
is not a trade worth making.
Only forwards, and only for a real update. A fresh install has nothing to
catch up on, a container reboot has not updated anything, and somebody
restoring an older release is dealing with a problem rather than
celebrating. Managed installations never see it at all — nobody signed in
there performed the update it thanks them for, which is the same gate the
System card and About's environment block already carry.
The redirect is attached to the dashboard alone, not the web group: it
catches a login and the sidebar logo both, without ever interrupting a
download to congratulate somebody. Reading the page clears the marker,
but the address keeps working — closing it by accident should not be
unrecoverable — and About now links to it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README told everybody to clone the repository and run the development
stack, which builds nothing and installs nothing — so a user following it
got a crash-looping worker and a 500, and reported it as a bug (#1627).
They were reading the right document; it was pointing at the wrong stack.
Getting started now says what each path is for. Users fetch
compose.example.yaml and run the published image, which ships vendored and
pre-built and needs no toolchain — it has existed all along and was linked
from nowhere in the README. Contributors get one paragraph saying a clone
is a development copy, not an installation, and a pointer to
CONTRIBUTING.md, whose sequence was already correct.
CONTRIBUTING gains the sentence its readers were missing — why those steps
exist at all — plus the two things a contributor now meets: containers
that name the missing step instead of dying silently, and the fact that a
second checkout shares one Compose project name and will take over the
first one's containers. That last one cost me this working copy's stack
for a few minutes.
Also: DOCKER.md and INSTALL.md both linked README.md#development, an
anchor that has never existed. And UPDATE.md's build-from-source path told
operators to pull and rebuild with no dependency step, which is the same
trap on every release that moves composer.lock.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Updating a server install cost nine artisan invocations plus a PHP-FPM
reload, written out in three places that had already drifted apart. One
of those steps is silently fatal to skip: with opcache.validate_timestamps
off — what production guides recommend and what our own image ships — the
database moves to the new version while every visitor keeps being served
the old code, and artisan reports the new version throughout.
`sudo ./update.sh` is now the whole procedure. It asks whether to check
GitHub, asks whether to download the release and verifies the checksum
published beside it, and asks whether there is a backup — offering to dump
the database when the answer is no. Then it takes the site down, replaces
the files, runs the update, reloads PHP-FPM, restarts the worker and
brings the site back. The application still has no self-updater: nothing
is fetched or applied unless somebody runs this and answers yes.
Underneath it is `php artisan projectsend:update`, which is everything an
update does that needs no root — and now the only definition of it. Both
container entrypoints call it instead of carrying their own copy of the
sequence, so the two paths cannot drift again.
Three findings worth keeping in the record, all from rehearsing rather
than reasoning:
- queue:restart has to come last. It writes its signal into the cache,
so clearing the cache afterwards deletes it and the worker runs old
code forever.
- optimize:clear is not safe to recommend. It runs cache:clear, which
on Redis is FLUSHDB — harmless on the default two-database layout,
but on a single-database Redis it takes the sessions and the queue
with it. The compiled caches are cleared individually instead.
- update.sh overwrites itself mid-run, because the zip contains it and
bash reads its own script lazily by byte offset. It re-execs from a
temporary copy before touching anything.
And when the reload is skipped anyway, the application now says so:
projectsend:update records the version it applied, and any staff page
compares that with what the running process actually compiled. The same
check catches the mirror image — new files in place, update never run.
Rehearsed end to end against real installs: a container upgrade (69 to 73
migrations, key and data intact, healthy), a scripted update on a real
nginx + php-fpm install with OPcache pinned (web process moved 2.1.0 to
2.1.1), the skipped-reload case (banner appears naming both versions, and
clears on reload), the refusals (downgrade, non-release zip, truncated
zip, URL passed to --zip, non-root), a database taken down mid-update
(site comes back out of maintenance mode by itself), and a real download
of the published 2.0.0 zip with its checksum verified.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Updating had a numbered list inside INSTALL.md and a code block in the
dashboard, and between them they were missing the step that decides
whether an update works at all.
Rehearsed both paths end to end against a real 2.0.0 install before
writing any of this down. On Docker the whole update is `docker compose
pull && docker compose up -d`: the entrypoint waits for the database,
migrates, ensures the roles, and starts the workers, and it does not
start nginx until that finishes — so a failed migration is a container
that stays down rather than a half-updated site taking traffic. The
generated APP_KEY on the storage volume is left alone. Verified: 69
migrations to 73, key unchanged, data intact, healthcheck healthy.
The manual path is where the gap was. Replace the files, run the four
documented commands, and the site still serves the old version: with
`opcache.validate_timestamps=0` — what every production guide recommends,
and what our own image ships — PHP never re-reads a file it has already
compiled. The database is on the new version, every visitor is on the
old code, and `php artisan` cheerfully reports the new version the whole
time you are trying to work out why. Reproduced exactly that, then
watched a php-fpm reload fix it.
UPDATE.md is now the whole procedure for both, INSTALL.md keeps the short
sequence with the reload added, and the in-app instructions gained the
same line — in the code block, which is not translated, so no locale is
left saying something different.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>