feat(docker): single-command Docker stack for local development - #24
Merged
paulocastellano merged 9 commits intoMay 17, 2026
Merged
Conversation
paulocastellano
requested changes
May 10, 2026
andrefrd
force-pushed
the
feat/docker-dev-environment
branch
4 times, most recently
from
May 12, 2026 23:17
8fd34ab to
254fc9f
Compare
Introduces a self-contained Docker build under docker/ that boots the
full Laravel + Vite + Reverb + Horizon + scheduler stack inside a
single container.
The Dockerfile exposes two targets sharing a common system-base layer
(PHP 8.4-FPM Alpine + Postgres/Redis/intl/sockets/redis extensions):
- dev: bind-mount source at runtime, runs Vite via supervisord, hot
reloads PHP via opcache.validate_timestamps=1, UID/GID build args
align container writes with the host user.
- production: ships the prebuilt application — composer --no-dev,
npm run build + build:ssr, wayfinder TS pre-generated, OpCache
hardened, fixed UID 1000.
Sidecar configs (nginx, php.{dev,prod}.ini, supervisord.{dev,prod}.conf,
entrypoint, postgres-init for the test DB) live next to the Dockerfile
so the build context is self-describing. The entrypoint is idempotent
and handles APP_KEY generation, migrations, storage:link, Passport
keys, Wayfinder regen, and dependency reinstall on every boot.
The previous compose.yaml referenced ./vendor/laravel/sail/runtimes/8.5
which was never wired up (sail isn't installed and the runtime path
doesn't exist), so docker compose up failed out of the box. It also
lacked Reverb, queue workers, and the scheduler.
The new compose.yaml:
- Builds the dev target of docker/Dockerfile so a single
`docker compose up --build` brings up the entire stack with no host
PHP or Node prerequisites.
- Bind-mounts the project root for hot-reload, with named volumes for
vendor/ and node_modules/ so image-baked deps survive the bind.
- Pins Postgres to 16-alpine (matches CI) and Redis to 7-alpine.
- Healthchecks gate `app` startup until pgsql and redis are ready.
- Sets DB_HOST=pgsql, DB_USERNAME=postgres, REDIS_HOST=redis as
process env so .env.testing's pre-existing CI defaults
(DB_HOST=127.0.0.1, DB_USERNAME=root) don't break tests run from
inside the container.
compose.override.yaml.example documents per-developer customizations
(UID/GID alignment on Linux, port conflicts, Xdebug, Selenium) without
forcing a specific workflow. .env.testing.local is added to .gitignore
as a safety net for any future per-developer testing override.
Adds a "Run with Docker" section between "Get Started" and "Contributing" covering: single-command boot, common commands, compose.override.yaml.example, troubleshooting (port conflicts, UID/GID on Linux, first-boot expectations), and the production image build hint for self-hosters.
Two adjustments from the PR review. Stripe placeholders (STRIPE_KEY, STRIPE_SECRET, STRIPE_WEBHOOK_SECRET, and the eight price IDs) only matter when SELF_HOSTED=false. The Docker stack defaults to SELF_HOSTED=true, so keeping the block in .env.docker.example just adds noise for first-time contributors. upload_max_filesize and post_max_size go from 100M to 1G in both php.dev.ini and php.prod.ini to accept full-length video uploads. Bumping nginx client_max_body_size to match — otherwise the proxy would cut the request off before PHP ever sees it.
User-facing documentation lives in trypostit/trypost-docs. Per maintainer feedback, the Docker setup walkthrough belongs there rather than inline in this repo's README, which is meant to stay focused on product positioning. Compose stack and docker/ assets stay in place.
- Add libwebp-dev + libavif-dev and --with-webp --with-avif to GD
configure (fixes imagewebp() undefined at runtime)
- Merge wayfinder-gen into asset-build stage: PHP + Node together
so @laravel/vite-plugin-wayfinder can invoke artisan during build
- Pass VITE_* as build args and ENV so they bake into the bundle
- Stub APP_URL=http://localhost so artisan boot doesn't crash on null
- mkdir -p storage/framework/{cache/data,sessions,views} storage/logs
bootstrap/cache before artisan runs (dirs stripped by .dockerignore)
- Production stage: rm vendor + bootstrap/cache/*.php before swapping
to prod-only deps (prevents missing-provider errors from dev cache)
- Switch production chown from app:app to www-data:www-data to match
Alpine php-fpm default pool user
Add /app/ (Pusher protocol) and /apps/ (REST API) location blocks with Upgrade/Connection headers so browsers can connect to the in-container Reverb server via the same port 80/443 endpoint.
andrefrd
force-pushed
the
feat/docker-dev-environment
branch
from
May 13, 2026 02:45
254fc9f to
ef501dd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
docker compose up --buildworkflow that brings the full Laravel + Vite + Reverb + Horizon + scheduler stack online inside one container, plus Postgres 16, Redis 7, and Mailpit. No host PHP / Node prerequisites.compose.yaml(a stale Sail file pointing at./vendor/laravel/sail/runtimes/8.5/— Sail isn't installed and the path doesn't exist) with a self-describing build that uses a newdocker/Dockerfile.docker/Dockerfileexposes aproductiontarget so self-hosters get a hardened image (composer--no-dev, npm production build, SSR build, OpCache locked) from the same source contributors use in dev.What changed
docker/Dockerfilesystem-base(PHP 8.4-FPM Alpine + Postgres / Redis / intl / sockets / redis extensions) →composer-deps/composer-deps-prod→wayfinder-gen→asset-build→dev/productiontargets. UID/GID build args.docker/{nginx,php.dev,php.prod,supervisord.dev,supervisord.prod}.{conf,ini}docker/entrypoint.sh.envfrom.env.docker.example, generatesAPP_KEY, waits for Postgres, runs migrations, links storage, generates Passport keys, regenerates Wayfinder helpers, and re-installs deps if anonymous volumes were wiped.docker/.env.docker.example.env.example, so the existing Herd workflow is untouched.docker/postgres-init.shtrypost_testdatabasephpunit.xmlexpects, on first Postgres boot.compose.yamlvendor/andnode_modules/so image-baked deps survive. Healthchecks gateappstartup until Postgres / Redis are ready. SetsDB_HOST=pgsql,DB_USERNAME=postgres,REDIS_HOST=redisas process env so.env.testing's pre-existing CI defaults don't break in-container tests.compose.override.yaml.example.dockerignore.gitignore.env.testing.localas a safety net for any per-developer testing override.README.mdWhy these specific choices
@laravel/vite-plugin-wayfinderplugin shells out tophp artisanat runtime — splitting Vite into a Node-only container would break it. Co-locating mirrors the production supervisord layout and gives one mental model..env.ci.docker/postgres-init.shcovers thetrypost_testdatabase thatphpunit.xmlpins..env.docker.exampleseparate from.env.exampleso Herd /composer devusers keep their flow unchanged. The entrypoint only seeds the Docker variant whenTRYPOST_DOCKER_BOOTSTRAP=1(set by compose).UID=$(id -u) GID=$(id -g).Test plan
docker compose up --buildfrom a fresh clone reaches healthy in ~3-4 min on first boot, seconds on subsequent boots.curl -fsS http://localhost:8000/up→ 200.http://localhost:8000renders (302 → /login).http://localhost:5173/@vite/client→ 200.http://localhost:8025→ 200.docker compose exec app php artisan test --compact tests/Feature/Auth/PasswordResetTest.php→ 5 passed (10 assertions).docker compose exec app php artisan horizon:status→ running.docker compose exec app php artisan schedule:list→ 4 scheduled commands.docker compose down && docker compose up -d→ fast restart, no re-migration thanks tomigrate --graceful.docker compose down -v && docker compose up --build→ full reset works;composer install+npm cire-run inside the container.docker build --target production -t trypost:prod -f docker/Dockerfile .→ production image builds cleanly without source-mount.compose.yamlvalidated:docker compose config --quietexits 0;docker buildx --checkreports no warnings.Out of scope
productiontarget is offered, but orchestration is left to self-hosters.compose.yamlto keep first-boot small.compose.override.yaml.exampleshows how to add it..env.ciand.github/workflows/are unchanged.🤖 Generated with Claude Code