# 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`.

