Implements MeshVault M1 (issues #6–11): - Four-service Docker Compose stack (nginx, PHP-FPM 8.3, PostgreSQL 17, Node assets) - Slim 4 + PHP-DI routing with escaped PHP views and production/dev error handling - Immutable validated AppConfig with redacted diagnostics - JSON stdout Monolog logging with job-scoped context - Tailwind CSS v4 deterministic locked builds with template scanning - POSIX and PowerShell dev wrappers (up/down/test/lint/assets/watch) - Startup config validation fails before FPM serves as unprivileged www-data - migrate/seed explicitly unavailable until M2 (#12); #10 remains open Verified via clean-start acceptance on remote host (keith@192.168.2.23).
4.7 KiB
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:
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:
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.
./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.
./dev.sh assets
Watch mode rebuilds on template changes:
./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:
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.