# 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](https://laravel.com/docs/12.x/sail) (included as a dev dependency) ## Quick Start ```bash # 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: ```dotenv 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`](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: ```bash 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` / `rebuild` use `docker compose pull --ignore-buildable`, > which only pulls images for services in the active `COMPOSE_PROFILES` and 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`: ```dotenv 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: ```yaml 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). ```bash 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](https://extras.getpagespeed.com/ubuntu/) (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`.