Shared, tool-neutral brief for any AI agent (Claude Code, Junie, Copilot,
Cursor, …) working in this repository. Claude Code also reads CLAUDE.md
(deeper Claude-specific detail); Junie also reads .junie/guidelines.md;
GitHub Copilot also reads .github/copilot-instructions.md. This file is
the canonical source for "what ivpldock is" and the hard rules — the
others point back here. Keep them in sync when the service set, scripts, or
permissions model change.
ivpldock is a Docker Compose development stack for InvoicePlane, forked and heavily slimmed down from Laradock. It is not an application — it is the PHP/database/web infrastructure that InvoicePlane (and sibling PHP projects on this host) run on top of.
- Upstream Laradock ships 90+ optional services. This fork keeps nine,
plus
docker-in-docker(see below). Everything else has been deleted fromdocker-compose.ymland.docker/on purpose — do not reintroduce a service by copying it back from upstream Laradock without a concrete need. - Target runtime: PHP 8.4 (the stack still supports 7.4–8.4 via
PHP_VERSION), MariaDB 10.11 (pinned — MariaDB 11 broke things), Redis, nginx, Beanstalkd. - The whole point of the fork is painless file permissions: everything a
container writes to a host-mounted path lands owned by the host user, with
no
sudo chownafterward. Preserving that property is a hard requirement of every change (see "Permissions model").
| Service | Purpose | Host access |
|---|---|---|
workspace |
CLI container: PHP, Composer, Node/npm/Yarn, headless Chromium | ./workmeup.sh |
php-fpm |
Application PHP runtime (PHP_VERSION) |
via nginx |
php-worker |
supervisord container for queue workers / schedulers |
background only |
nginx |
Web server, one vhost per project under sites/*.conf |
http://localhost (80/443) |
mariadb |
MySQL-compatible database, 10.11 pinned | localhost:${MARIADB_PORT} (default 3306) |
redis |
Cache / session store | localhost:6379 |
beanstalkd |
Job queue | background |
beanstalkd-console |
Beanstalkd web UI | background |
phpmyadmin |
Database admin UI | http://localhost:8081 |
docker-in-docker (image docker:29-dind) is still defined and wired into
workspace/php-fpm (via DOCKER_HOST, TLS env vars, links, and a
/certs/client volume). Upstream-plan intent was to remove it; it was kept
by an explicit decision. Treat it as "deliberately deferred, not blessed" —
if you are cleaning up, removing it and its DOCKER_* wiring is on the
table, but ask first.
All four "up" scripts start the identical nine-service set
(beanstalkd beanstalkd-console mariadb nginx php-fpm php-worker phpmyadmin redis workspace). If you add or rename a service, grep all the scripts
plus the Makefile, not just one — they have silently drifted apart before.
| Script | Effect |
|---|---|
./startmeup.sh |
start the 9 services, detached, no rebuild |
./starmeup.sh |
same, foreground |
./builddmeup.sh |
rebuild images + start, detached |
./buildmeup.sh |
rebuild images + start, foreground |
./workmeup.sh |
shell into workspace as the ivpldock user |
./down.sh |
stop and remove containers (no -v — volumes, including MariaDB's, are kept) |
The Makefile is the fuller interface — make help lists ~30 targets
(make start, make build, make build-workspace, make shell,
make logs, make status, make db-shell, make redis-cli,
make fix-permissions, make validate, …). make down stops without
-v; make down-volumes is the explicit destructive variant.
Raw commands always take the env file explicitly and use modern
docker compose (space, not the legacy docker-compose binary):
docker compose --env-file .env.docker ps
docker compose --env-file .env.docker build workspace
docker compose --env-file .env.docker exec --user=ivpldock workspace php -vWORKSPACE_PUID / WORKSPACE_PGID default to 1000:1001, matching the host
user. Two different mechanisms deliver "files come out host-owned", and
mixing them on one service breaks that service:
- Container-level
user: '${WORKSPACE_PUID}:${WORKSPACE_PGID}'— for services whose entrypoint runs app code directly with no privileged setup step:php-fpm,redis. (The whole process tree runs as the host UID/GID from PID 1.) - Entrypoint starts as root, drops privileges internally — for services
whose entrypoint needs root first:
mariadb— the official entrypoint initialises/var/lib/mysqlas root, thenexecs asmysql. Addinguser:here makes init fail (this was reverted once already, commit01e1532).php-worker—supervisord.confhasuser=rootsosupervisordcansetuid()each supervised program touser=ivpldock. Addinguser:makes it crash instantly:Error: Can't drop privilege as nonroot user.workspace— starts as root for build/setup, drops toivpldockfor interactive use; scripts and Make targets pass--user=ivpldock.
Before adding user: to any service, check whether its entrypoint expects
to start as root.
The Dockerfiles for workspace/php-fpm/php-worker recreate the
ivpldock user with the build-arg PUID/PGID, deleting any pre-existing
user/group that squats on those ids. Keep that logic intact.
-
./down.shruns plaindocker compose down(no-v) — it keeps theivpldock_mariadbvolume, i.e. the database. It used to pass-vand delete it; that has been fixed. For the explicit destructive variant usemake down-volumesordocker compose --env-file .env.docker down -v, and always double-check whichCOMPOSE_PROJECT_NAMEa given checkout resolves to before running-vby hand — a scratch clone that happens to load the same.env.dockertargets this stack's volumes, not its own. -
Never add
access_log/error_logdirectives pointing at/var/log/nginx/*.login asites/*.conffile. nginx's master runs as root and opens the log path before dropping towww-data, so a not-yet-existing per-site log file is created root-owned on the host-mountedlogs/nginx/directory and you can't rotate/delete it withoutsudo.nginx.confalready logs globally to/dev/stdout//dev/stderr(docker compose logs nginx). Base new sites onsites/default.conf, not the other*.conf.exampletemplates. -
This host runs several unrelated Docker Compose projects side by side.
- Do not look containers up with
docker ps -aqf "name=<substring>"— it can match the wrong project or several at once. Usedocker compose --env-file .env.docker exec <service>. (workmeup.shwas fixed this way;phpmeup.sh,rootmeup.sh,worker.sh,redismeup.shstill have the brittle pattern.) make cleanis scoped to ivpldock's own label. Do not run a globaldocker system prune/docker volume prune— that hits every stack on the host. (make clean-allexists for the deliberate case.)- If
mariadbfails with "port is already allocated", another stack ownsMARIADB_PORT(default 3306):docker ps --format '{{.Names}}\t{{.Ports}}' | grep 3306.
- Do not look containers up with
-
Shell-continuation comments eat commands. A
#comment \line inside a backslash-continued command gets swallowed once bash joins the logical line. This silently droppedphpmyadmin/redis/workspacefrom two scripts. Don't put#comments inside\-continued command blocks. -
MARIADB_VERSION=10.11is a deliberate pin. MariaDB 11 broke this stack.docker-compose.ymlinterpolates${MARIADB_VERSION}; keep the value at 10.11 unless you're doing a deliberate, tested upgrade. -
.env.dockeris gitignored (local, may hold secrets)..env.exampleis the tracked template —cp .env.example .env.docker. They should stay structurally identical apart fromAPP_CODE_PATH_HOST(local absolute path vs. the../projectsdefault). -
Don't
-include .env.dockerin theMakefile. GNU Make re-exports any variable that started in the environment once a makefile assignment touches it, which silently breaks overrides likeMARIADB_PORT=3316 make start. Recipes that need a value read it from.env.dockerat run time withgrep.
- Minimal changes. This is infrastructure; churn is expensive. Only touch a Dockerfile if a required capability needs it.
- Adding a workspace/php-fpm capability (a PHP extension, a tool):
- Add an
ARG INSTALL_<THING>=false+ guardedRUNblock with a#####section header and a comment explaining why, in the relevant.docker/*/Dockerfile. - Add the build arg to
docker-compose.ymlunder that service. - Add the flag to
.env.exampleand.env.docker(same key). - Keep it opt-out-able; don't change existing defaults without reason.
- Sanity-check at least PHP 8.2 and 8.4 build.
- Add an
- Env var naming: service prefix + descriptive name
(
WORKSPACE_INSTALL_CHROMIUM, notCHROMIUM). - Xdebug never autostarts — trigger mode only
(
xdebug.start_with_request=trigger). - Verify, then report honestly.
docker compose config --quietfor syntax; actually build/run the affected service; if you couldn't test something, say so. - Commit style: conventional commits (
feat:,fix:,docs:,refactor:,chore:).
workspace installs Google Chrome stable from Google's apt repo
(WORKSPACE_INSTALL_CHROMIUM=true), symlinked as chromium /
chromium-browser / chrome, with CHROME_BIN set. Ubuntu 24.04's own
chromium apt package is a broken snap stub, hence Google's .deb. amd64
only. In a container, callers must pass --no-sandbox (and
--headless=new --disable-dev-shm-usage for CI); the binary is left generic.
WORKSPACE_INSTALL_PUPPETEER=true additionally installs the puppeteer npm
package globally under /usr/local (on NODE_PATH), so Spatie Browsershot's
node .../browser.cjs can require('puppeteer'). It does not download
its own Chromium (PUPPETEER_SKIP_DOWNLOAD); PUPPETEER_EXECUTABLE_PATH
points it at the Chrome above. Requires WORKSPACE_INSTALL_CHROMIUM=true.
/data/prompt-ivpldock.md is the onboarding doc for agents working in
/data/Projects/*. It tells them this stack already provides PHP, Composer,
Node, MariaDB, Redis, and a web server, so they route work through
./workmeup.sh / docker compose exec instead of installing anything on
the host. Keep it in sync with this file.