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:-} # The whole default stack declares one, so it comes back after a reboot # or a Docker restart instead of half of it coming back — the confusing # state, where the queue runs and the site is down (#1658). The dev-only # profile services below deliberately do not: you bring those up for a # session, not for the life of the machine. restart: unless-stopped 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 restart: unless-stopped depends_on: - app worker: build: context: . dockerfile: docker/app/Dockerfile args: WWWUSER: ${WWWUSER:-1000} WWWGROUP: ${WWWGROUP:-1000} command: php artisan queue:work --queue=default --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 # Zip builds get their own worker: BuildZipDownloadJob allows itself an # hour, and on a shared queue one large archive holds up every # notification email behind it. worker-zips: build: context: . dockerfile: docker/app/Dockerfile args: WWWUSER: ${WWWUSER:-1000} WWWGROUP: ${WWWGROUP:-1000} command: php artisan queue:work --queue=zips --tries=1 volumes: - .:/var/www/html - ../packages:/var/www/packages 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 restart: unless-stopped 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 restart: unless-stopped 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: