diff --git a/DOCKER.md b/DOCKER.md index 88d4c3721..218cfe1ec 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -4,7 +4,7 @@ ## Prerequisites -- Docker & Docker Compose v2+ +- 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) @@ -55,35 +55,106 @@ dormant and consumes no resources. ## Make Targets -Run `make` or `make help` to see all 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 up` | Start all services (detached) | -| `make down` | Stop all services | -| `make restart` | Restart all services | -| `make build` | Build the app image | -| `make rebuild` | Build from scratch (no cache) | -| `make pull` | Pull latest base images | -| `make update` | Pull + rebuild + restart | -| `make fresh` | **Destroy volumes**, rebuild, start clean (DATA LOSS) | | `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 | +| `make optimize` | Cache config / routes / views + spatie/laravel-data | +| `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 test` | Run PHPUnit tests (`filter=Name` optional) | -| `make pint` | Pint formatter on dirty files | -| `make npm-build` | `npm install` + `npm run build` | -| `make npm-dev` | Start Vite dev server | -| `make db` | MariaDB CLI session | -| `make redis-cli` | Redis CLI session | -| `make logs` | Tail all container logs | -| `make status` | Show container status | -| `make clean` | Prune stopped containers / dangling images | -| `make nuke` | **Remove ALL** project containers, images, volumes | + +### 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`. @@ -167,7 +238,8 @@ docker compose up -d | Permission errors on storage/ | `make root-shell` then `chown -R sail:sail storage bootstrap/cache` | | 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 + rebuilds + restarts) | -| Need a completely clean slate | `make fresh` (destroys all volumes!) | +| 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 | diff --git a/Makefile b/Makefile index 09a166ae1..c0f6b83a0 100644 --- a/Makefile +++ b/Makefile @@ -4,193 +4,257 @@ # # Run `make` or `make help` to see all available targets. # See DOCKER.md for full documentation. +# +# Common flags: +# FORCE=1 Skip confirmation prompts on destructive targets +# MAINTENANCE=1 Wrap `upgrade` migrations in artisan down/up +# SERVICE=name Restrict a target (e.g. logs) to a single service +# CMD="…" Free-form command for `artisan`, `exec`, etc. +# +# Requires Docker Compose v2.22+ (uses `pull --ignore-buildable`). # ────────────────────────────────────────────────────────────── - .DEFAULT_GOAL := help - # Source SEARCH_DRIVER from .env and export as COMPOSE_PROFILES # so the correct search engine container starts automatically. -include .env export COMPOSE_PROFILES ?= $(SEARCH_DRIVER) - -SAIL := ./sail +SAIL := ./sail DOCKER_COMPOSE := docker compose - +# Optional parameters with safe defaults +SERVICE ?= +CMD ?= +FORCE ?= +MAINTENANCE ?= # Colors CYAN := \033[36m GREEN := \033[32m YELLOW := \033[33m +RED := \033[31m +BOLD := \033[1m RESET := \033[0m - +# ── Confirmation helper ────────────────────────────────────── +# Usage: $(call confirm,Are you sure?) +# Honours FORCE=1 for non-interactive / CI usage. +define confirm + @if [ "$(FORCE)" = "1" ]; then \ + echo "$(YELLOW)⚠ FORCE=1 set — skipping confirmation.$(RESET)"; \ + else \ + printf "$(YELLOW)⚠ $(1) [y/N] $(RESET)"; \ + read confirm; [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ] || (echo "Aborted." && exit 1); \ + fi +endef +# ── Pre-flight ─────────────────────────────────────────────── +.PHONY: check-env +check-env: ## Verify .env exists before running docker targets + @if [ ! -f .env ]; then \ + echo "$(RED)✘ .env not found. Run: cp .env.example .env$(RESET)"; \ + exit 1; \ + fi # ── Lifecycle ──────────────────────────────────────────────── - .PHONY: up -up: ## Start all services in the background +up: check-env ## Start all services in the background @$(SAIL) up -d - .PHONY: down down: ## Stop all services @$(SAIL) down - .PHONY: stop stop: down ## Alias for 'down' - .PHONY: restart restart: ## Restart all services @$(SAIL) restart - +.PHONY: recreate +recreate: check-env ## Force-recreate containers without rebuilding + @$(SAIL) up -d --force-recreate --remove-orphans .PHONY: build -build: ## Build the app image +build: check-env ## Build the app image (cached layers OK) @$(SAIL) build - .PHONY: rebuild -rebuild: ## Build the app image from scratch (no cache) - @$(SAIL) build --no-cache - +rebuild: check-env pull ## Rebuild from scratch with fresh base images and recreate + @$(SAIL) build --no-cache --pull + @$(SAIL) up -d --force-recreate --remove-orphans + @echo "$(GREEN)✔ Rebuild complete.$(RESET)" .PHONY: pull -pull: ## Pull the latest versions of all base images - @$(DOCKER_COMPOSE) pull - +pull: check-env ## Pull latest enabled-profile base images (skips buildable) + @$(DOCKER_COMPOSE) pull --ignore-buildable .PHONY: update -update: pull rebuild ## Pull latest images, rebuild, and restart - @$(SAIL) up -d - +update: check-env pull ## Infra-only: pull base images, rebuild with --pull, restart + @$(SAIL) build --pull + @$(SAIL) up -d --remove-orphans + @echo "$(GREEN)✔ Infra updated.$(RESET)" +.PHONY: upgrade +upgrade: update composer-install npm-build ## Full app upgrade: update + composer + npm + migrate + caches + @if [ "$(MAINTENANCE)" = "1" ]; then $(SAIL) artisan down; fi + @$(SAIL) artisan migrate --force + @if [ "$(MAINTENANCE)" = "1" ]; then $(SAIL) artisan up; fi + @$(MAKE) data-cache + @$(MAKE) optimize + @echo "$(GREEN)✔ Upgrade complete.$(RESET)" .PHONY: fresh -fresh: ## Destroy ALL volumes, rebuild, and start clean (DATA LOSS!) - @echo "$(YELLOW)⚠ This will destroy all Docker volumes (database, redis, search index).$(RESET)" - @read -p "Are you sure? [y/N] " confirm && [ "$$confirm" = "y" ] || exit 1 +fresh: check-env ## Destroy ALL volumes, rebuild, and start clean (DATA LOSS!) + $(call confirm,This will destroy all Docker volumes (database, redis, search index). Continue?) @$(SAIL) down -v - @$(SAIL) build --no-cache - @$(SAIL) up -d + @$(DOCKER_COMPOSE) pull --ignore-buildable + @$(SAIL) build --no-cache --pull + @$(SAIL) up -d --force-recreate --remove-orphans @echo "$(GREEN)✔ Fresh environment is up. Run 'make artisan cmd=nntmux:install' to initialise.$(RESET)" - # ── Shell Access ───────────────────────────────────────────── - .PHONY: shell shell: ## Open a bash shell in the app container @$(SAIL) shell - .PHONY: root-shell root-shell: ## Open a root bash shell in the app container @$(SAIL) root-shell - .PHONY: tinker tinker: ## Open a Laravel Tinker session @$(SAIL) tinker - # ── Artisan / PHP ──────────────────────────────────────────── - .PHONY: artisan artisan: ## Run an artisan command (usage: make artisan cmd="migrate") - @$(SAIL) artisan $(cmd) - + @$(SAIL) artisan $(cmd)$(CMD) +.PHONY: migrate +migrate: ## Run pending database migrations + @$(SAIL) artisan migrate --force +.PHONY: migrate-fresh +migrate-fresh: ## Drop all tables and re-run migrations (DATA LOSS!) + $(call confirm,This will DROP ALL TABLES and re-run every migration. Continue?) + @$(SAIL) artisan migrate:fresh --force +.PHONY: seed +seed: ## Run database seeders + @$(SAIL) artisan db:seed --force +.PHONY: cache-clear +cache-clear: ## Clear config / route / view / application caches + @$(SAIL) artisan config:clear + @$(SAIL) artisan route:clear + @$(SAIL) artisan view:clear + @$(SAIL) artisan cache:clear + @echo "$(GREEN)✔ Caches cleared.$(RESET)" +.PHONY: optimize +optimize: ## Cache config / routes / views and warm spatie/laravel-data + @$(SAIL) artisan config:cache + @$(SAIL) artisan route:cache + @$(SAIL) artisan view:cache + @$(SAIL) artisan data:cache-structures + @echo "$(GREEN)✔ Optimized.$(RESET)" +.PHONY: queue-work +queue-work: ## Run a foreground queue worker (Ctrl-C to stop) + @$(SAIL) artisan queue:work +.PHONY: queue-restart +queue-restart: ## Signal all queue workers to restart gracefully + @$(SAIL) artisan queue:restart .PHONY: tmux-start tmux-start: ## Start the NNTmux tmux processing engine @$(SAIL) artisan tmux:start - .PHONY: tmux-stop tmux-stop: ## Stop the NNTmux tmux processing engine @$(SAIL) artisan tmux:stop - .PHONY: tmux-attach tmux-attach: ## Attach to the running tmux session @$(SAIL) artisan tmux:attach - .PHONY: horizon horizon: ## Show Horizon status @$(SAIL) artisan horizon:status - # ── Testing & Quality ──────────────────────────────────────── - .PHONY: test test: ## Run the PHPUnit test suite (usage: make test filter=TestName) @$(SAIL) test $(if $(filter),--filter=$(filter),) - .PHONY: pint pint: ## Run Laravel Pint code formatter on dirty files @$(SAIL) pint --dirty - .PHONY: pint-all pint-all: ## Run Laravel Pint on all files @$(SAIL) pint - +.PHONY: phpstan +phpstan: ## Run PHPStan static analysis + @$(SAIL) php vendor/bin/phpstan analyse --memory-limit=2G +.PHONY: rector +rector: ## Run Rector in dry-run mode (no changes written) + @$(SAIL) php vendor/bin/rector process --dry-run +.PHONY: rector-fix +rector-fix: ## Apply Rector refactorings + @$(SAIL) php vendor/bin/rector process # ── Frontend ───────────────────────────────────────────────── - .PHONY: npm-build npm-build: ## Run npm install and build inside the container @$(SAIL) npm install @$(SAIL) npm run build - .PHONY: npm-dev npm-dev: ## Start Vite dev server inside the container @$(SAIL) npm run dev - .PHONY: ts-types ts-types: ## Regenerate TypeScript types from PHP DTOs/Enums @$(SAIL) artisan typescript:transform - .PHONY: ts-types-check ts-types-check: ## CI: regenerate TS types and fail if working tree drifts @$(SAIL) artisan typescript:transform --quiet @git diff --exit-code resources/js/types/generated.d.ts \ || (echo "❌ resources/js/types/generated.d.ts is out of date — run 'make ts-types' and commit." && exit 1) - .PHONY: data-cache data-cache: ## Cache spatie/laravel-data structures (run on deploy) @$(SAIL) artisan data:cache-structures - # ── Dependencies ───────────────────────────────────────────── - .PHONY: composer-install composer-install: ## Run composer install inside the container @$(SAIL) composer install - .PHONY: composer-update composer-update: ## Run composer update inside the container @$(SAIL) composer update - # ── Database / Services ────────────────────────────────────── - .PHONY: db db: ## Open a MariaDB CLI session @$(SAIL) mariadb - .PHONY: redis-cli redis-cli: ## Open a Redis CLI session @$(SAIL) redis - # ── Logs & Status ──────────────────────────────────────────── - .PHONY: logs -logs: ## Tail logs from all containers - @$(DOCKER_COMPOSE) logs -f --tail=100 - +logs: ## Tail logs (usage: make logs [SERVICE=mariadb]) + @$(DOCKER_COMPOSE) logs -f --tail=100 $(SERVICE) +.PHONY: tail-laravel +tail-laravel: ## Tail storage/logs/laravel.log inside the app container + @$(SAIL) exec laravel.test tail -f storage/logs/laravel.log .PHONY: status status: ## Show running containers and their status @$(DOCKER_COMPOSE) ps - +.PHONY: ps +ps: status ## Alias for 'status' +.PHONY: top +top: ## Show running processes inside each container + @$(DOCKER_COMPOSE) top +.PHONY: images +images: ## Show images used by each service + @$(DOCKER_COMPOSE) images +.PHONY: health +health: ## Show healthcheck status for each service (Compose v2.20+) + @$(DOCKER_COMPOSE) ps --format json 2>/dev/null \ + | awk -F'"' '/"Service"/ {svc=$$4} /"Health"/ {print svc": "$$4}' \ + | sort -u \ + || $(DOCKER_COMPOSE) ps # ── Cleanup ────────────────────────────────────────────────── - .PHONY: clean clean: ## Remove stopped containers and dangling images @docker system prune -f @echo "$(GREEN)✔ Cleaned up dangling resources.$(RESET)" - .PHONY: nuke nuke: ## Remove ALL project containers, images, and volumes (DATA LOSS!) - @echo "$(YELLOW)⚠ This will remove ALL project containers, images, and volumes.$(RESET)" - @read -p "Are you sure? [y/N] " confirm && [ "$$confirm" = "y" ] || exit 1 + $(call confirm,This will remove ALL project containers/images/volumes. Continue?) @$(SAIL) down -v --rmi all @echo "$(GREEN)✔ All project Docker resources removed.$(RESET)" - # ── Help ───────────────────────────────────────────────────── - .PHONY: help -help: ## Show this help message +help: ## Show this help message, grouped by section @echo "" - @echo "$(CYAN)NNTmux Docker / Sail Commands$(RESET)" + @echo "$(CYAN)$(BOLD)NNTmux Docker / Sail Commands$(RESET)" @echo "$(CYAN)─────────────────────────────$(RESET)" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ - awk 'BEGIN {FS = ":.*?## "}; {printf " $(GREEN)%-20s$(RESET) %s\n", $$1, $$2}' + @awk ' \ +/^# ── .* ──/ { \ +line=$$0; sub(/^# ── /, "", line); sub(/ ──.*/, "", line); \ +printf "\n\033[1;33m%s\033[0m\n", line; next \ +} \ +/^[a-zA-Z_-]+:.*?## / { \ +split($$0, a, ":.*?## "); \ +split(a[1], b, ":"); \ +printf " \033[32m%-20s\033[0m %s\n", b[1], a[2] \ +}' $(MAKEFILE_LIST) + @echo "" + @echo "$(CYAN)Flags:$(RESET) FORCE=1 MAINTENANCE=1 SERVICE=name CMD=\"…\"" @echo "" -