Files
projectsend/compose.yaml
T
Ignacio Nelson 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>
2026-08-15 02:21:29 -03:00

145 lines
4.3 KiB
YAML

name: projectsend
services:
app:
build:
context: .
dockerfile: docker/app/Dockerfile
args:
WWWUSER: ${WWWUSER:-1000}
WWWGROUP: ${WWWGROUP:-1000}
volumes:
- .:/var/www/html
# Shared dev clones of the companion packages — never nested inside this
# repo. Relative to keep host and container paths symmetric with
# the ../packages symlink at this repo's own root.
- ../packages:/var/www/packages
environment:
PHP_IDE_CONFIG: serverName=projectsend
# Optional unattended first-admin creation; without these the web
# setup screen prompts on first visit.
ADMIN_NAME: ${ADMIN_NAME:-}
ADMIN_EMAIL: ${ADMIN_EMAIL:-}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-}
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
web:
build:
context: .
dockerfile: docker/web/Dockerfile
args:
WWWUSER: ${WWWUSER:-1000}
WWWGROUP: ${WWWGROUP:-1000}
ports:
- "${APP_PORT:-8090}:80"
volumes:
- .:/var/www/html
- ./docker/web/nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
worker:
build:
context: .
dockerfile: docker/app/Dockerfile
args:
WWWUSER: ${WWWUSER:-1000}
WWWGROUP: ${WWWGROUP:-1000}
command: php artisan queue:work --tries=3 --backoff=3
volumes:
- .:/var/www/html
- ../packages:/var/www/packages
# Required so `queue:restart` (triggered when mail provider settings
# are saved) actually brings the worker back instead of leaving the
# queue dead until someone runs `docker compose up -d` by hand.
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
scheduler:
build:
context: .
dockerfile: docker/app/Dockerfile
args:
WWWUSER: ${WWWUSER:-1000}
WWWGROUP: ${WWWGROUP:-1000}
command: php artisan schedule:work
volumes:
- .:/var/www/html
- ../packages:/var/www/packages
# Same reason the worker has one, plus a second: on a fresh clone this
# exits until `composer install` has run, and without a restart policy it
# then stays exited — scheduled work silently never happens, on the one
# setup where nobody would think to check.
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: mysql:8.4
command: --mysql-native-password=OFF
environment:
MYSQL_DATABASE: ${DB_DATABASE:-projectsend}
MYSQL_USER: ${DB_USERNAME:-projectsend}
MYSQL_PASSWORD: ${DB_PASSWORD:-secret}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root}
volumes:
- db-data:/var/lib/mysql
ports:
# Loopback only: this forward exists for host-side DB GUIs, not for
# the network. Without the prefix Docker publishes on 0.0.0.0 and
# bypasses most host firewalls — a LAN-reachable MySQL with the
# compose-file default password on any host that runs the stack.
- "127.0.0.1:${DB_PORT_FORWARD:-33061}:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${DB_ROOT_PASSWORD:-root}"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
# Dev-only DB GUI (brief §12: ship Adminer as a dev-only Compose service)
adminer:
image: adminer:latest
ports:
# Loopback only — an unauthenticated DB panel must not be reachable
# from the network just because someone brought the dev profile up
# on a machine with a routable address.
- "127.0.0.1:${ADMINER_PORT:-8091}:8080"
environment:
ADMINER_DEFAULT_SERVER: db
depends_on:
- db
profiles:
- dev
# Dev-only SMTP catcher: lets email notifications be sent and inspected
# locally (web UI + HTTP API) without a real mail server.
mailpit:
image: axllent/mailpit:latest
ports:
# Loopback only, same reasoning as Adminer: captured mail is readable
# without authentication.
- "127.0.0.1:${MAILPIT_SMTP_PORT:-1025}:1025"
- "127.0.0.1:${MAILPIT_WEB_PORT:-8025}:8025"
profiles:
- dev
volumes:
db-data:
redis-data: