# Development containers (M1) Four-service Docker Compose stack: nginx, unprivileged PHP-FPM, PostgreSQL and an assets container for Tailwind CSS builds. The application layer (Slim 4, PHP-DI, Monolog) is wired, but there is no catalog schema, migration runner or asset pipeline beyond Tailwind. Do not expose this stack to the Internet. ## Prerequisites - Docker Engine/Desktop with Linux containers and Docker Compose v2+. - No host PHP, Composer or Node is required. ## First start From the repository root, copy `.env.example` to `.env` and edit `MESHVAULT_DB_PASSWORD` to a private, nonempty value. Never commit `.env` or print expanded Compose configuration in shared logs; use `docker compose config --quiet`. Create a **new dedicated** `library` directory (`mkdir library`). Do not use an existing collection. On Linux, give container UID/GID 33 ownership of that new empty directory, for example `sudo chown 33:33 ./library` and `sudo chmod 750 ./library`. Alternatively, after building, use the narrowly scoped one-off command below **only for this new empty directory**: ```sh docker compose build app docker compose run --rm --no-deps --user root app chown 33:33 /var/lib/meshvault/library docker compose config --quiet docker compose up -d --wait ``` Also ensure `public/build` is writable by the assets container user (`node`, UID 1000). On Linux, run: ```sh mkdir -p public/build chown 1000:1000 public/build ``` Docker Desktop bind permissions differ; verify writability using the runtime identity rather than assuming host chmod works. The stack never automatically creates or changes ownership of a configured library directory. ## Task wrappers Use `dev.sh` (POSIX) or `dev.ps1` (PowerShell) from the repository root. They are independent of your current working directory and propagate exit codes. ```sh ./dev.sh up # build and start services ./dev.sh down # stop services (volume removal is forbidden) ./dev.sh test # run composer test inside the app container ./dev.sh lint # run composer lint inside the app container ./dev.sh assets # build minified CSS inside the assets container ./dev.sh watch # start Tailwind watch inside the assets container ./dev.sh migrate # unavailable until M2 (#12); exits nonzero ./dev.sh seed # unavailable until M2 (#12); exits nonzero ``` `down --volumes` and `down -v` are blocked by the wrapper. Never remove the named `pgdata` volume or the library bind. ## Asset builds Assets are built inside the `assets` service using a locked `package-lock.json`. The generated `public/build/app.css` is served statically by nginx. ```sh ./dev.sh assets ``` Watch mode rebuilds on template changes: ```sh ./dev.sh watch ``` ## Verification Open http://127.0.0.1:8090/ for the landing page. `docker compose exec app id` should report UID/GID 33. Check a disposable library write without becoming root: ```sh docker compose exec app php -r '$p = getenv("MESHVAULT_LIBRARY_DIR") . "/write-check"; if (file_put_contents($p, "ok") === false) { exit(1); } unlink($p);' ``` `down` preserves the named `pgdata` volume and library bind. Changing database credentials in `.env` does not change credentials in an initialized PostgreSQL volume. Back up data before upgrades. ## Settings and boundaries `.env.example` documents every current setting: Compose project, HTTP host/port, DB name/user/password, host library path and absolute container library path. The container library path must stay outside `/var/www/html` and must not overlap system paths. The only required non-default value is the database password. The default project is `meshvault-m1`; HTTP binds localhost:8090. Only nginx has a published port. PostgreSQL readiness gates app startup. FPM itself runs as `www-data`, not just its workers. nginx mounts only `public/` and executes only `/index.php`; source, credentials and library are not web mounts. Source mounts are explicit and read-only. No host `vendor` or `node_modules` is mounted; dependencies are installed inside images from lockfiles. No Docker socket is mounted. Startup configuration validation is provided by the entrypoint script. Image branches: PHP 8.3 FPM Bookworm (security support to December 2027), PostgreSQL 17 Bookworm (support to November 2029), nginx 1.30 Alpine (stable), Node 24 Bookworm slim (LTS). Tags are versioned but move with patch updates, not digest locks. PHP 8.3 compatibility is retained. Remote verification uses only `/home/keith/Projects/MeshVault` after confirming HOME, explicit project `meshvault-m1`, and an explicit source-only copy manifest. Access from another machine via `ssh -L 8090:127.0.0.1:8090 keith@192.168.2.23`. Never attach original collections or change unrelated services.