Makefile
Rama main · réplica de lectura del repositorio original.
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Netresearch DTT GmbH
SHELL := /bin/bash
.DEFAULT_GOAL := help
# ────────────────────────────────────────────────────────────────────
# Lifecycle
# ────────────────────────────────────────────────────────────────────
help: ## Show this help
@printf 'glpi-docker-compose-stack — make targets\n\nUsage: make <target>\n\n'
@awk 'BEGIN {FS = ":.*?##"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-22s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
init: ## Bootstrap .env (random DB passwords, idempotent)
@./bin/init.sh
up: .env ## Start the stack (detached)
docker compose up -d
# Guard: bringing the stack up without a populated .env poisons the db-data
# volume with an empty MARIADB_ROOT_PASSWORD that survives `make clean`.
# This rule fails before docker compose sees the empty env vars.
.env:
@printf '\033[1;31m[make]\033[0m No .env found — run `make init` first.\n' >&2
@exit 1
down: ## Stop the stack (preserves volumes)
docker compose down
restart: ## Restart app + web only
docker compose restart app web
logs: ## Tail logs (all services)
docker compose logs -f --tail=100
logs-app: ## Tail logs (app service)
docker compose logs -f --tail=100 app
ps: ## Show container status
docker compose ps
# ────────────────────────────────────────────────────────────────────
# Overlays & integrations
# ────────────────────────────────────────────────────────────────────
#
# Overlay state lives in .env as COMPOSE_FILE=<colon-separated paths>.
# docker compose reads this automatically, so `make up` works without
# manual `-f` chaining once an overlay is enabled.
overlays: .env ## Show currently-enabled compose overlays
@./bin/compose-file.sh list
enable-traefik: .env ## Add Traefik reverse-proxy overlay (TLS termination + label-based routing)
@./bin/compose-file.sh add examples/compose.traefik.yml
@printf '\033[1;34m[next]\033[0m Set GLPI_HOST in .env to your public hostname (e.g. glpi.example.com), then `make up`. Requires an existing `traefik` external network.\n'
disable-traefik: .env ## Remove Traefik overlay
@./bin/compose-file.sh remove examples/compose.traefik.yml
enable-caddy: .env ## Add Caddy reverse-proxy overlay (TLS termination + auto-HTTPS)
@./bin/compose-file.sh add examples/compose.caddy.yml
@printf '\033[1;34m[next]\033[0m Set GLPI_HOST in .env to your public hostname (e.g. glpi.example.com), then `make up`.\n'
disable-caddy: .env ## Remove Caddy overlay
@./bin/compose-file.sh remove examples/compose.caddy.yml
enable-observability: .env ## Add Prometheus + Grafana overlay (MariaDB + nginx metrics)
@./bin/compose-file.sh add examples/compose.observability.yml
@printf '\033[1;34m[next]\033[0m The nginx /stub_status endpoint is NOT enabled by default — see the overlay header for the two opt-in paths.\n'
disable-observability: .env ## Remove observability overlay
@./bin/compose-file.sh remove examples/compose.observability.yml
# ────────────────────────────────────────────────────────────────────
# Backup / restore
# ────────────────────────────────────────────────────────────────────
backup-up: .env ## Start the opt-in backup service (phpbu, behind the `backup` profile)
docker compose --profile backup up -d backup
backup: ## Run a backup now (requires `make backup-up`; normally ofelia at 03:00)
docker compose --profile backup exec -T backup phpbu --configuration=/config/backup.json
backup-list: ## List backup archives
docker compose --profile backup exec -T backup ls -lh /backups
backup-verify: ## Sanity-check that last night's backup is on disk + non-zero
@docker compose --profile backup exec -T backup sh -c '\
latest=$$(ls -t /backups/db/*.sql.gz 2>/dev/null | head -1); \
if [ -z "$$latest" ]; then \
echo "✗ no DB backups in /backups/db"; exit 1; \
fi; \
size=$$(stat -c%s "$$latest"); \
age_h=$$(( ($$(date +%s) - $$(stat -c%Y "$$latest")) / 3600 )); \
if [ "$$size" -lt 1024 ]; then \
echo "✗ $$latest is $$size bytes — likely empty"; exit 1; \
fi; \
if [ "$$age_h" -gt 26 ]; then \
echo "✗ newest dump is $${age_h}h old (should be < 26h)"; exit 1; \
fi; \
echo "✓ newest dump $$latest ($$size bytes, $${age_h}h old)"'
health: ## Aggregated health state of all services + non-healthy summary
@docker compose ps --format '{{.Service}}\t{{.Status}}' | column -ts $$'\t'
@unhealthy=$$(docker compose ps --filter "health=unhealthy" -q | wc -l | tr -d ' '); \
if [ "$$unhealthy" != "0" ]; then \
printf '\n\033[1;31m%s unhealthy service(s)\033[0m — see docker compose logs <service>\n' "$$unhealthy"; \
exit 1; \
else \
printf '\n\033[1;32mAll services healthy.\033[0m\n'; \
fi
# ────────────────────────────────────────────────────────────────────
# Testing (local)
# ────────────────────────────────────────────────────────────────────
test-image: ## Build runtime image + run container-structure-test (image-surface check)
docker buildx build --target runtime --platform linux/amd64 --load -t glpi-php-fpm:test .
@command -v container-structure-test >/dev/null 2>&1 \
|| { echo "Install: https://github.com/GoogleContainerTools/container-structure-test/releases"; exit 1; }
container-structure-test test --image glpi-php-fpm:test --config tests/container-structure-test.yaml
test-bats: ## Run bats regression suite for bin/ helpers (uses bats/bats Docker image — no local install)
docker run --rm -v "$(CURDIR)":/code -w /code bats/bats:latest tests/bin/
hardening-check: ## Assert every overlay keeps no-new-privileges + cap_drop:[ALL] (tests/lint/hardening-check.sh)
./tests/lint/hardening-check.sh
test: test-image test-bats hardening-check ## Run the local test suite (image surface + bats + overlay hardening)
# ────────────────────────────────────────────────────────────────────
# Dev convenience
# ────────────────────────────────────────────────────────────────────
dev: ## Seed compose.override.yml from the example (if missing) + `make up`
@if [ ! -f compose.override.yml ]; then \
cp compose.override.yml.example compose.override.yml; \
printf '\033[1;34m[make]\033[0m seeded compose.override.yml from example\n'; \
else \
printf '\033[1;34m[make]\033[0m compose.override.yml already present — keeping\n'; \
fi
$(MAKE) up
build: ## Build the runtime image locally (glpi-php-fpm:local, supply-chain-pinned) — no push
@v=$$(tr -d '[:space:]' < .glpi-version); \
url="https://github.com/glpi-project/glpi/releases/download/$$v/glpi-$$v.tgz"; \
printf '\033[1;34m[build]\033[0m resolving sha256 of %s\n' "$$url"; \
sha=$$(curl -fsSL "$$url" | sha256sum | cut -d' ' -f1); \
[ -n "$$sha" ] || { printf '\033[1;31m[build]\033[0m could not hash %s\n' "$$url" >&2; exit 1; }; \
printf '\033[1;34m[build]\033[0m GLPI %s sha256=%s\n' "$$v" "$$sha"; \
docker buildx build --target runtime --platform linux/amd64 --load \
-t glpi-php-fpm:local \
--build-arg GLPI_VERSION="$$v" \
--build-arg GLPI_SHA256="$$sha" .
lint: ## Run hadolint + shellcheck + yamllint via Docker (no local install)
@printf '\033[1;34m[lint]\033[0m hadolint Dockerfile\n'
docker run --rm -i -v $(CURDIR):/work -w /work \
hadolint/hadolint:latest-alpine \
hadolint --config .hadolint.yaml Dockerfile
@printf '\033[1;34m[lint]\033[0m shellcheck rootfs/usr/local/bin + bin\n'
docker run --rm -v $(CURDIR):/work -w /work \
koalaman/shellcheck:stable \
$$(find rootfs/usr/local/bin bin -type f \( -name '*.sh' -o -perm -u+x \) 2>/dev/null)
@printf '\033[1;34m[lint]\033[0m yamllint compose + workflows\n'
docker run --rm -v $(CURDIR):/work -w /work \
cytopia/yamllint:1 \
-d "{extends: default, rules: {line-length: disable, document-start: disable, truthy: {check-keys: false}, comments: {min-spaces-from-content: 1}}}" \
.github/workflows .hadolint.yaml compose.yml
@printf '\033[1;34m[lint]\033[0m actionlint .github/workflows\n'
docker run --rm -v $(CURDIR):/work -w /work \
rhysd/actionlint:latest -color
# ────────────────────────────────────────────────────────────────────
# Upgrade
# ────────────────────────────────────────────────────────────────────
pull: ## Pull latest images
docker compose pull
upgrade: pull up logs-app ## Pull + recreate + follow logs
# ────────────────────────────────────────────────────────────────────
# Maintenance
# ────────────────────────────────────────────────────────────────────
shell: ## Shell into the app container
docker compose exec app sh
console: ## Run a GLPI console command (use: make console CMD="cache:configure --help")
docker compose exec -u www-data app php bin/console $(CMD)
restore: ## Print pointer to the disaster-recovery runbook (not automated)
@printf '\033[1;33m[restore]\033[0m This target intentionally does NOT automate restore.\n'
@printf ' Restoring is destructive and context-specific — follow the\n'
@printf ' runbook step by step:\n\n'
@printf ' docs/runbook-restore.md\n\n'
@printf ' TL;DR: stop app+scheduler, pick a dump from the backups volume,\n'
@printf ' drop+recreate the database, gunzip | mariadb, restore the\n'
@printf ' files/ and config/ tarballs, then `make restart`.\n'
clean: ## DESTRUCTIVE: down + delete ALL volumes (db + uploads + backups)
@read -r -p "This deletes ALL data — the database, uploads AND the backups volume. Type 'yes' to proceed: " ans; \
[ "$$ans" = "yes" ] || { echo "aborted"; exit 1; }
docker compose down -v
.PHONY: \
help init up down restart logs logs-app ps \
backup backup-up backup-list backup-verify \
health test test-image test-bats hardening-check \
dev build lint pull upgrade \
shell console restore clean \
overlays \
enable-traefik disable-traefik \
enable-caddy disable-caddy \
enable-observability disable-observability