mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-17 17:15:08 +00:00
8d896191e4fc2b390ce3f22b5fbdec2d2f8a8a7c
13 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5a7c9938dd |
Work properly behind a reverse proxy
Three findings from one report of intermittent 502s behind Nginx Proxy Manager, all of them ours. Stop sending the Link: preload header. AddLinkHeadersForPreloadedAssets copied every Vite preload into a response header, duplicating tags the document already carried in its head — twenty on the login page. nginx buffers a response's headers into a single block defaulting to 4 KB, so /files, at 6060 bytes of headers, was refused with "upstream sent too big header" and the proxy answered 502. Which pages went over depended on how many assets they loaded, which is why it read as intermittent rather than as a header that is always too big: the login screen fitted, the application did not. Removing it takes /files to 1247 bytes and /dashboard from 4544 to 1247. Nothing is lost — the browser reads the tags in the document, and we send no 103 Early Hints. Send nginx's logs to the container's streams. supervisord captures what each program writes to its own stdout, but nginx opens the files named in the package's nginx.conf as soon as it reads its config, so access and error logs went to /var/log/nginx/ inside the container. That is where the reason for every 502 and every 403 was written, and docker logs never showed it — so a proxy problem presented as no logs on either side, which is exactly how it was reported. Document the thing neither guide covered. DOCKER.md had no reverse-proxy section at all: no mention of proxies, of 502s, or of TRUSTED_PROXIES, which until now was explained only in a comment in the compose example. It gains one, including that TRUSTED_PROXIES cannot cause a 502 and is the wrong place to dig. INSTALL.md's nginx-in-front-of-Apache path gains the proxy_* buffer settings its fastcgi_* equivalents already had. Reported by @denkfabrik-li (#1664), who traced it to the middleware independently, and separately by a user running Nginx Proxy Manager who found the too-big-header line in the proxy's own log. |
||
|
|
8f12c83d21 |
Tell a clone-and-build install to rebuild, not to pull
ProjectSend prints the update instructions for the way this server was installed, and it knew two answers where it needed three: anything inside a container was handed `docker compose pull && docker compose up -d`. On the Compose stack that builds from a checkout there is no image behind those containers, so `pull` skips every ProjectSend service and `up -d` then finds them all current — the update reports success, changes nothing, and the dashboard goes on offering the same release. Reported by @mueller7382, who stayed on 2.0.0 that way while 2.1.0 was out (#1661). Those installations are now their own kind, told to `git pull` and rebuild, with the two steps a checkout needs that an image does not: its dependencies and its compiled frontend live outside git, so a release that moved either leaves them stale. Two signals decide it, in that order. The published image now declares itself with PROJECTSEND_IMAGE, which is the only evidence an operator bind-mounting over /var/www/html can neither hide nor forge; failing that — images published before this — a working tree in the install directory, which the image never has and the repository's own stack always does. getenv() rather than env(), because a cached configuration makes env() outside a config file return null, and the answer would flip silently on exactly the installs most likely to have cached it. The stale-code banner keeps treating both container kinds alike: what clears it is recreating the container, whichever way its image was built. The changelog also credits the reporter of #1663, which was missed when that entry was written. |
||
|
|
1c62036ed2 |
Let the setup screen be what the quickstart actually shows
The example compose file shipped with ADMIN_NAME/ADMIN_EMAIL/ADMIN_PASSWORD filled in, so the entrypoint created the first administrator and nobody ever reached the setup screen the README, the Docker Hub page and the website all promise. Someone who followed the instructions literally — edit APP_URL and the passwords — also ended up with a publicly reachable administrator on admin@example.com with a password printed in a public file. Comment the three variables out. Unattended provisioning still works for anyone who wants it, it is just opt-in now, and the first thing a new install shows is the setup screen again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
202a1d7ad5 |
Point the Docker guide at the image people actually run
#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:
|
||
|
|
e279e83fd0 |
Show the dashboard on the Docker Hub page
The page described the application in three paragraphs and then went straight to a compose file. Somebody deciding whether to pull it had no idea what it looks like — and for a thing whose whole job is a screen your clients use, that is the question they are actually asking. The dashboard, after the paragraphs that say what this is and before the quick start, which is the point in the page where a reader has decided they are interested and not yet decided to spend ten minutes. The same image the README uses, and the same alt text, which was written to describe the screen rather than to name the file. One screenshot, not three: the README has the other two and the caption says so, and a registry description that scrolls past its own install instructions has stopped being an install page. Absolute raw.githubusercontent URL, because a repository-relative path resolves to nothing on hub.docker.com — the same reason the badges point there. .github/ is stripped from the release artifact, which does not matter here: this file is pasted into a description, not shipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9c991f495d |
Give the Docker Hub page a masthead
The page opened on a bare H1, which on a registry listing reads as an unfinished description rather than a product. Docker Hub's own search results, and every well-kept image beside ours, lead with a mark and a row of badges — and the badges are not decoration there: version, size and where to get help are the questions somebody has before they decide to pull. Six of them, each answering one of those: the current release, pull count, compressed image size, stars, Discord, and the licence. Four are live values rather than static text, so the page stops being something anybody has to remember to update — the release badge already reads v2.1.0, and image size already reads 78.4 MiB. Pure markdown, no HTML. Docker Hub sanitises HTML out of descriptions, so the centred layouts people write for GitHub silently collapse there; the badges are consecutive markdown links, which is what actually renders as a row. Each link carries a title, so hovering says what it is for. The mark is apple-touch-icon.png and not the wordmark or the favicon, for a reason worth writing down: favicon.svg has a viewBox and no width, so it has no intrinsic size and renders at whatever the container offers — which on a wide column is enormous. The PNG is 180x180 and renders as a mark. That also matches README.md, which puts a small icon above the title rather than a banner. Colours are README.md's, not the ones on the page this was modelled after: 3b5bdb for the project, 0b7285 for the Docker facts, and Discord's own brand colour where the badge is a Discord badge. The point is that the two front doors look like the same project. Every URL checked: twelve, all 200, and the four dynamic badges verified to render real values rather than shields.io's "invalid". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
be44b4b6aa |
Track the Docker Hub description, and send migrators to the guide
This file is what Docker Hub shows above the pull command, and it has been sitting untracked since it was written — nothing to review it against, nothing to stop it drifting from the compose file and the entrypoint it describes, and nothing in the repository if the machine holding it died. Verified against what it claims rather than read for tone. The compose block has the same services, images and environment variables as compose.example.yaml, with nothing extra in either. supervisord really does run nginx, php-fpm, the queue worker and the scheduler. APP_KEY really is generated once and kept on the storage volume with .env symlinked to it. The container really does wait for MySQL and then run `php artisan projectsend:update`. `projectsend:admin --if-none` is what makes "ignored once any user exists" true. nginx really does serve protected downloads with X-Accel-Redirect, which is the reason the image carries a web server at all. MySQL 8.0-or-newer matches INSTALL.md word for word, and every external link resolves. The one thing that was wrong for its audience: the Legacy section pointed at the migration tool's repository, and everybody reading this page is on Docker — where the tool cannot be installed the way its README implies, because the image ships the application already built and carries no Composer. It now points at the guide, and names the section written for this image today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9192779ee4 |
Close four small gaps before the release goes out
Four unrelated one-liners, each already written down and none of them worth a branch of its own. The lock was still pinned to the community package's previous commit, which is the one before it started shipping its own sixteen catalogues. The mechanism that carries a package's translations to the browser landed here last week; without this bump the release would have shipped that mechanism with nothing to carry, and the Custom Assets screen would have stayed half-English in every language. The stock `local` disk had `serve` left on. Nothing in this application writes to it, so the framework's /storage route was a door with nothing behind it — but it was still a door, and closing it costs one word. nginx evaluated `\.php$` before `/protected-files/`, so a protected path ending in .php would have reached the PHP handler instead of streaming under the sandbox headers that block sets. Not reachable on a default install — the upload allowlist refuses php and X-Accel paths are UUIDs — but the guarantee read stronger than it was. `^~` makes it true. And `.release-build` is now ignored by eslint, so linting after building a zip stops walking the vendored minified JS inside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f446398dfd |
Say which step is missing instead of failing blankly (#1633)
Somebody followed the README's Docker quickstart, which starts the development stack, and got three failures in a row with nothing to search for (#1627): the worker died once a second on a missing autoloader, the site answered a bare 500, and once dependencies were installed by hand the setup screen threw ViteManifestNotFoundException. None of that is wrong behaviour for a clone — vendor/ and public/build/ are deliberately not in git — but every one of those failures kept its cause to itself. The preflight guard exists to turn "this was never set up" into a sentence, and it runs before the autoloader precisely so it can. It now answers two more questions: dependencies not installed, and frontend not built. The dependency check goes first, before the .env one, because the fix that branch prints — php artisan key:generate — cannot itself run without the autoloader, so reporting the key first hands somebody a second and more confusing error. A running vite dev server counts as built: public/hot means the assets come from there, and blocking a developer mid-session would be worse than the exception this replaces. The worker and scheduler exec straight into artisan, so before composer install they died instantly and restarted forever, filling the log that had to be read to fix it. They now print what is missing and exit slowly, and recover on their own once it is there. The scheduler gains the restart policy the worker already had — without one it exits during that window and stays exited, and scheduled work then silently never happens. Rehearsed on a genuine clone of the public repository, following the reporter's exact path: worker prints instructions instead of fatals (2 restarts in 30s, not 30), the browser gets "ProjectSend is not installed yet" naming composer install, then "not configured yet", then "not built yet" naming npm run build, then the setup screen. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ed0d36de25 |
Reduce a manual update to one command that asks first (#1628)
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>
|
||
|
|
4f10401c36 |
Run nginx as the user php-fpm writes as (#1622)
The image creates a fixed-uid www-data for php-fpm but left nginx on the alpine package's own `user nginx;` (uid 100), while nginx.conf's header claimed the two already matched. They did not, and the claim is why nobody looked. It matters because php-fpm hands nginx files to serve: protected downloads and thumbnails go out by X-Accel-Redirect. Directories the application creates on demand come out at Flysystem's private mode, 0700, owned by www-data — and traversing one of those means being its owner, since there are no group or other bits to fall back on. So the first thumbnail an installation ever renders answers 403, from nginx, with nothing in the application's log to show for it (#1614). Pointing the directive at www-data is half the fix. /var/lib/nginx and its tmp/ arrive from the package owned by the old user, and nginx recreates only the leaf temp directories at boot — as the new user, so they look correct while their parent stays untraversable. Every request nginx buffers to disk then fails with a bare 500 that never reaches php-fpm, which is every upload chunk, which is every upload (#1618). Fixing one without the other trades a broken thumbnail for a broken upload, so both land together. Verified by building the image three ways and driving a real upload and thumbnail through each: unchanged, the part PUT succeeds and the thumbnail 403s; with the user directive alone, the thumbnail works and the part PUT is a bare nginx 500 with "open() /var/lib/nginx/tmp/client_body/0000000001 failed (13: Permission denied)"; with both, the chunked upload completes, the thumbnail renders, and the file downloads back byte for byte. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
6e47d76ba6 |
ProjectSend 2.0.0
Client file sharing, rebuilt from the ground up: a private area per client, resumable uploads, folders, groups and categories, sharing with expiry dates and download limits, comments, file versions, an activity log, a REST API, and sixteen languages. This repository begins here. ProjectSend 2 was developed privately, and that development history is not published — the previous generation remains available, with its own history, at projectsend/legacy. Free software under the GNU General Public License v2, or (at your option) any later version. |