14 KiB
Docker Development Environment
Docker / Sail setup for NNTmux — a Laravel 12 Usenet indexer.
Prerequisites
- Docker & Docker Compose v2.22+ (the Makefile uses
pull --ignore-buildable) - A GitHub (scopeless) token for Composer private packages
- Laravel Sail (included as a dev dependency)
Quick Start
# 1. Copy and configure your environment
cp .env.example .env
# Edit .env — set DB credentials, COMPOSER_AUTH, and uncomment the
# "Docker service hostnames" block at the bottom.
# 2. Build and start
make build
make up
# 3. Install NNTmux (first run only)
make artisan cmd="nntmux:install"
# 4. Build frontend assets
make npm-build
Environment Configuration
When running via Docker, uncomment the Docker service hostnames block
at the bottom of .env so the app resolves container names:
DB_HOST=mariadb
REDIS_HOST=redis
MANTICORESEARCH_HOST=manticore
ELASTICSEARCH_HOST=elasticsearch
MAIL_HOST=mailpit
MAIL_PORT=1025
Search Engine Selection
Set COMPOSE_PROFILES in .env to match your SEARCH_DRIVER:
SEARCH_DRIVER |
COMPOSE_PROFILES |
|---|---|
manticore |
manticore |
elasticsearch |
elasticsearch |
Only the selected search service container will start. The other remains dormant and consumes no resources.
ManticoreSearch Image Updates
Both docker-compose.yml and docker-compose.yml.prod-dist intentionally use
the unpinned manticoresearch/manticore image. This keeps ManticoreSearch on
the latest published Docker image whenever images are pulled, but it also means
production can receive ManticoreSearch changes without a repository diff.
Before pulling and restarting production, validate the new image in staging by creating indexes, indexing a small batch, and running representative searches, filters, sorting, pagination, deletes, suggestions, fuzzy searches, and any raw SQL diagnostics/reconciliation commands.
Manticore Search 27.1.5 is part of the 25.x-27.x release line that introduced
built-in authentication/authorization, sharded tables, conversational search,
vector-search improvements, and replication layout changes. The app does not
enable Manticore auth by default, but if auth is enabled on the server set either
MANTICORESEARCH_USERNAME/MANTICORESEARCH_PASSWORD or
MANTICORESEARCH_TOKEN before running the app, index-creation commands, or raw
HTTP diagnostics. Roll auth out in staging first; anonymous local Docker usage
continues to work with these variables blank.
Native Ubuntu/Debian package installs are managed by systemd, not Docker. If
the package install fails while starting manticore.service, especially with
Status: "Replaying binlogs..." and accept() failed ... Too many open files,
or if searchd prints a backtrace during binlog replay, use
docs/manticore-ubuntu-package.md to raise
the service file descriptor limit, preserve crash evidence, quarantine bad
binlogs if needed, and finish dpkg configuration.
Make Targets
Run make or make help to see all targets, grouped by section.
Lifecycle
| Target | Description |
|---|---|
make up |
Start all services (detached) |
make down |
Stop all services |
make restart |
Restart all services |
make recreate |
Force-recreate containers without rebuilding |
make build |
Build the app image (cached) |
make rebuild |
Pull fresh base images + --no-cache build + force-recreate |
make pull |
Pull latest enabled-profile base images (skips buildable ones) |
make update |
Infra-only: pull + --pull build + restart |
make upgrade |
App upgrade: update + composer + npm + migrate + caches |
make fresh |
Destroy volumes, pull, rebuild, recreate (DATA LOSS) |
Development
| Target | Description |
|---|---|
make shell |
Bash shell in the app container |
make root-shell |
Root bash shell in the app container |
make tinker |
Laravel Tinker REPL |
make artisan cmd= |
Run any artisan command |
make migrate |
Run pending migrations |
make migrate-fresh |
Drop all tables and re-migrate (DATA LOSS) |
make seed |
Run database seeders |
make cache-clear |
Clear config / route / view / app caches (and stale bootstrap/cache files) |
make optimize |
Dev-safe: warm view cache + spatie/laravel-data |
make optimize-deploy |
Production: also caches config/routes (bakes absolute paths) |
make queue-work |
Foreground queue worker |
make queue-restart |
Signal queue workers to restart |
make tmux-start |
Start the NNTmux tmux processing engine |
make tmux-stop |
Stop the tmux processing engine |
make tmux-attach |
Attach to the running tmux session |
make horizon |
Show Horizon queue status |
make fix-permissions |
Chown project to host UID + register git safe.directory |
make fix-permissions |
Chown project to sail + register git safe.directory |
Testing & Quality
| Target | Description |
|---|---|
make test |
PHPUnit tests (filter=Name optional) |
make pint |
Pint formatter on dirty files |
make pint-all |
Pint formatter on all files |
make phpstan |
PHPStan static analysis (2G memory limit) |
make rector |
Rector dry-run (no changes) |
make rector-fix |
Apply Rector refactorings |
Frontend & Types
| Target | Description |
|---|---|
make npm-build |
npm install + npm run build |
make npm-dev |
Start Vite dev server |
make ts-types |
Regenerate TypeScript types |
make ts-types-check |
CI: fail if generated types drift |
make data-cache |
Cache spatie/laravel-data structures |
Logs & Status
| Target | Description |
|---|---|
make logs |
Tail all container logs (or SERVICE=name for one) |
make tail-laravel |
Tail storage/logs/laravel.log inside the app container |
make status / ps |
Show running containers |
make top |
Show processes inside each container |
make images |
Show images used by each service |
make health |
Healthcheck status per service |
Cleanup
| Target | Description |
|---|---|
make clean |
Prune stopped containers / dangling images |
make nuke |
Remove ALL project containers, images, volumes (DATA LOSS) |
Flags
These can be combined with the targets above:
| Flag | Effect |
|---|---|
FORCE=1 |
Skip confirmation prompts on fresh, nuke, migrate-fresh (CI use) |
MAINTENANCE=1 |
Wrap upgrade migrations in artisan down / artisan up |
SERVICE=name |
Restrict logs to a specific compose service |
CMD="…" |
Free-form command for artisan (alternative to cmd=) |
filter=Name |
Pass --filter=Name to make test |
Examples:
make fresh FORCE=1 # non-interactive teardown + rebuild
make upgrade MAINTENANCE=1 # zero-downtime-ish upgrade with maint mode
make logs SERVICE=mariadb # tail only mariadb
make test filter=ReleaseSearchTest # run a single test
Note:
make pull/update/rebuildusedocker compose pull --ignore-buildable, which only pulls images for services in the activeCOMPOSE_PROFILESand skips images that are built locally (e.g.sail-8.5/app). Requires Docker Compose v2.22+.
You can also use ./sail directly for anything not covered above —
unknown commands are passed through to docker compose.
Networking
Sail creates a Docker network called sail with these default port mappings:
| Service | Container Port | Host Port (default) |
|---|---|---|
| HTTP (nginx) | 80 | APP_PORT (80) |
| Vite | 5173 | VITE_PORT (5173) |
| MariaDB | 3306 | FORWARD_DB_PORT (3306) |
| Redis | 6379 | FORWARD_REDIS_PORT (6379) |
| Manticore SQL | 9306 | 9306 |
| Manticore HTTP | 9308 | 9308 |
| Elasticsearch | 9200 | 9200 |
| Mailpit SMTP | 1025 | FORWARD_MAILPIT_PORT (1025) |
| Mailpit Web | 8025 | FORWARD_MAILPIT_DASHBOARD_PORT (8025) |
If a port is already in use on your host, change the corresponding
FORWARD_* / APP_PORT variable in .env.
Database
On first run MariaDB creates a user/database from your .env:
DB_USERNAME=
DB_PASSWORD=
DB_DATABASE=nntmux
Import an existing SQL dump or run make artisan cmd="nntmux:install" for
a fresh installation.
Backups
Add this optional service to docker-compose.yml for automated MariaDB backups:
backup:
image: fradelg/mysql-cron-backup
depends_on:
- mariadb
restart: always
volumes:
- ./docker/backups:/backup
environment:
- MYSQL_USER=${DB_USERNAME}
- MYSQL_PASS=${DB_PASSWORD}
- MYSQL_DB=${DB_DATABASE}
- CRON_TIME=0 3 * * *
- MYSQL_HOST=mariadb
- MYSQL_PORT=3306
- TIMEOUT=10s
- GZIP_LEVEL=9
- MAX_BACKUPS=5
- INIT_BACKUP=0
- EXIT_BACKUP=1
networks:
- sail
Production
For production deployments use docker-compose.yml.prod-dist as a starting
point. It includes separate webapp, worker, and scheduler services
and uses the root Dockerfile (FrankenPHP-based).
cp docker-compose.yml.prod-dist docker-compose.yml
# Edit .env for production settings
docker compose up -d
Troubleshooting
| Problem | Solution |
|---|---|
| Permission errors on storage/ | make fix-permissions (chowns project to your host UID) |
composer install permission denied |
make fix-permissions, then re-run make composer-install |
npm/ncu EACCES on host (package.json) |
make fix-permissions chowns to host UID, not container sail |
fatal: detected dubious ownership (git) |
make fix-permissions registers safe.directory in the container |
Container sail UID ≠ host UID |
Set WWWUSER=$(id -u) / WWWGROUP=$(id -g) in .env, then make rebuild |
Host artisan/npm fails with /var/www/html/... paths |
Stale bootstrap/cache/config.php from a container config:cache. Run make cache-clear and prefer make optimize (dev-safe) over optimize-deploy in development |
| Port already in use | Change APP_PORT, FORWARD_DB_PORT, etc. in .env |
| Containers won't start | make logs to inspect, or make rebuild to start fresh |
| Stale images after upgrade | make update (pulls base images + rebuild + restart) |
| Need a completely clean slate | make fresh (destroys all volumes; add FORCE=1 for non-interactive) |
| CI / scripted teardown | make fresh FORCE=1 or make nuke FORCE=1 to skip prompts |
| supervisorctl not connecting | make root-shell then supervisorctl status to verify socket path |
Nginx (GetPageSpeed + Brotli)
The app container's nginx is installed from the GetPageSpeed apt repo
(stable branch) instead of the Ubuntu archive. This gives us a current nginx
build with ABI-matched dynamic modules. The nginx-module-brotli package is
installed and the brotli directives in docker/8.5/nginx.conf are enabled by
default (gzip is kept as a fallback for clients without br support).
The repo is apt-pinned at priority 1001 via
/etc/apt/preferences.d/getpagespeed-nginx.pref so nginx and module
packages always resolve from GetPageSpeed even if Ubuntu publishes a newer
version. A nginx -t is run during docker build to fail fast on any config
drift.
To revert to stock Ubuntu nginx, remove the GetPageSpeed apt key, list, and
preferences entries from docker/8.5/Dockerfile, drop nginx-module-brotli
from the install line, re-comment the brotli block in docker/8.5/nginx.conf,
and run make rebuild.