README.md
Rama main · réplica de lectura del repositorio original.
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->
<!-- Copyright (c) 2026 Netresearch DTT GmbH -->
# glpi-docker-compose-stack
An opinionated, production-grade Docker Compose stack for **[GLPI](https://glpi-project.org) 11**, built around a hardened, single-purpose **php-fpm image** (published to `ghcr.io/netresearch/glpi-php-fpm`) with nginx, MariaDB, Valkey, a cron scheduler and nightly backups — wired together with the security and supply-chain rigor you'd want for an IT asset-management system that holds your whole estate.
Sibling of [`netresearch/snipe-it-docker-compose-stack`](https://github.com/netresearch/snipe-it-docker-compose-stack) and [`netresearch/phpbu-docker`](https://github.com/netresearch/phpbu-docker); same house style, GLPI internals.
> **Not affiliated with the GLPI project / Teclib'.** This packages upstream GLPI (GPL-3.0-or-later); the stack tooling here is MIT, the docs CC-BY-SA-4.0.
---
## Why this instead of the official image?
The official `glpi/glpi` image is Apache + mod_php in a single container with a built-in supervisor. This stack instead:
- **Decouples** PHP (php-fpm) from the web server (nginx) so each scales and updates independently, and php-fpm listens on a **unix socket only** (no FastCGI-over-TCP bypass surface).
- Builds GLPI from the **bundled release tarball** into a minimal, **multi-stage, non-root** image with the code layer **read-only** and only the state dirs writable.
- Serves from GLPI's **`public/` docroot** (mandatory since GLPI 11) with a strict **CSP** and security-header set.
- Ships **MariaDB with binlog** (point-in-time recovery), **Valkey** as GLPI's cache backend, **ofelia** running GLPI's `front/cron.php`, and **phpbu** for nightly DB + files + **encryption-key** backups.
- Multi-arch images (amd64/arm64) with **SLSA provenance, an in-image SBOM, and keyless cosign signatures**, rebuilt daily for base-image CVEs.
## Architecture
```mermaid
flowchart TB
browser(["browser"]) --> ingress["host :8080 (loopback)<br>or reverse proxy"]
subgraph stack["compose stack"]
direction TB
web["web<br>nginx · public/"]
assets["app-assets<br>one-shot public/ sync"]
app["app<br>glpi-php-fpm · GLPI 12.0.0"]
db[("db<br>mariadb · binlog")]
cache[("valkey<br>cache")]
scheduler["scheduler<br>ofelia"]
dsp["docker-socket-proxy"]
backup["backup<br>phpbu · profile backup"]
assets -. "glpi-public volume" .-> web
web <== "unix socket<br>/run/php-fpm/glpi.sock" ==> app
app -- mysqli --> db
app -- cache --> cache
scheduler --> dsp
dsp -. "exec front/cron.php<br>every 2 min" .-> app
dsp -. "exec phpbu<br>nightly 03:00" .-> backup
backup -- dump --> db
end
ingress --> web
```
| Service | Image | Role |
|---|---|---|
| `db` | `mariadb:11` | GLPI database, `ROW` binlog + durable fsync (PITR-ready) |
| `valkey` | `valkey:9-alpine` | GLPI core cache (RESP/redis-compatible), AOF persistence |
| `app-assets` | this image | one-shot: syncs `public/` into a volume nginx serves |
| `app` | `ghcr.io/netresearch/glpi-php-fpm` | GLPI on php-fpm, socket-only, non-root |
| `web` | `nginx:alpine` | serves `public/` + FastCGI to `app`, CSP/security headers |
| `scheduler` | `ghcr.io/netresearch/ofelia` | runs GLPI `front/cron.php` (2 min) + phpbu (nightly) |
| `docker-socket-proxy` | `ghcr.io/tecnativa/docker-socket-proxy` | the only path `scheduler` has to the Docker API — scoped to `CONTAINERS`/`EXEC`/`POST`, no raw socket in `scheduler` |
| `backup` _(opt-in: `--profile backup`)_ | `ghcr.io/netresearch/phpbu-docker` | nightly DB dump + `files/` + **config (crypt key)** archive |
## Quickstart
```bash
git clone https://github.com/netresearch/glpi-docker-compose-stack
cd glpi-docker-compose-stack
make init # writes .env with random DB passwords (idempotent)
make up # start the stack; first boot auto-installs GLPI
# wait until healthy, then:
open http://localhost:8080 # login: glpi / glpi ← CHANGE IMMEDIATELY
```
First boot runs `bin/console database:install`, generating the schema **and** the
encryption key (`glpicrypt.key`, kept in the `glpi-config` volume). Every later
boot runs `database:update` to migrate the schema to the image's GLPI version.
GLPI ships several default accounts (`glpi`, `tech`, `normal`, `post-only`) all
with the password matching the username — **change or disable every one** before
exposing the instance.
## Configuration
All configuration is in `.env` (copy from `.env.example`; `make init` fills the
passwords). Key variables:
| Variable | Default | Purpose |
|---|---|---|
| `GLPI_IMAGE_TAG` | `latest` | image tag to run (pin to a version in prod) |
| `GLPI_DB_NAME` / `GLPI_DB_USER` / `GLPI_DB_PASSWORD` | `glpi` / `glpi` / — | database |
| `DB_ROOT_PASSWORD` | — | MariaDB root (init only) |
| `GLPI_CACHE_DSN` | `redis://valkey:6379` | GLPI cache backend |
| `GLPI_ENVIRONMENT_TYPE` | `production` | `production` hides debug surfaces |
| `GLPI_AUTOINSTALL` | `true` | install on first boot / upgrade thereafter |
| `SKIP_DB_UPDATE` | `false` | skip on-boot `database:update` (multi-replica) |
| `GLPI_HTTP_PORT` | `8080` | loopback host port for `web` |
| `GLPI_HOST` | `glpi.example.com` | public hostname (reverse-proxy overlays) |
| `TZ` | `Europe/Berlin` | container clock |
The image relocates GLPI's mutable state off the read-only code root via
`GLPI_CONFIG_DIR=/etc/glpi`, `GLPI_VAR_DIR=/var/lib/glpi/files`,
`GLPI_MARKETPLACE_DIR=/var/lib/glpi/plugins`, `GLPI_LOG_DIR=/var/log/glpi` —
each on its own named volume.
> **Uploads:** php and nginx allow **64 MB** request bodies by default (GLPI
> stores documents as attachments). Raise `upload_max_filesize`/`post_max_size`
> (php) and `client_max_body_size` (nginx) together for larger files. GLPI also
> only accepts uploads whose extension is in its *Document types* allow-list
> (Setup → Dropdowns → Document types) — add any custom types you need.
## Persistent data
| Volume | Mount | Contents | Back up? |
|---|---|---|---|
| `glpi-db-data` | db `/var/lib/mysql` | the database | yes (phpbu dumps it) |
| `glpi-config` | app `/etc/glpi` | `config_db.php` + **`glpicrypt.key`** | **yes — critical** |
| `glpi-var` | app `/var/lib/glpi/files` | uploads, cache, sessions, pictures | yes |
| `glpi-plugins` | app `/var/lib/glpi/plugins` | marketplace plugins | optional |
| `glpi-logs` | app `/var/log/glpi` | application logs | no |
> ⚠️ **Never delete `glpi-config`.** `glpicrypt.key` is the AES key for every
> encrypted DB field (stored credentials, plugin secrets). Lose it and that data
> is unrecoverable — which is why the nightly backup archives the config dir and
> the entrypoint refuses to regenerate the key.
## Operations
```bash
make logs # tail all services
make console CMD="cache:clear" # run any bin/console command
make ps # status
make down # stop (keep volumes)
```
- **Cron** — `ofelia` execs `php front/cron.php` in `app` every 2 minutes
(GLPI's automatic actions). A heartbeat job feeds the `app` healthcheck, so a
dead scheduler turns the container unhealthy. Set GLPI's *Automatic actions* to
**CLI** mode (Setup → General → Automatic actions) for deterministic scheduling.
- **Upgrades** — bump `.glpi-version` (+ `GLPI_IMAGE_TAG`), `make up`; the
entrypoint runs a maintenance-wrapped `database:update`.
- **Backups / restore** — see [`docs/runbook-restore.md`](docs/runbook-restore.md). The `backup` service (phpbu) is **opt-in** behind the `backup` compose profile — start it with `make backup-up` (or `docker compose --profile backup up -d`).
- **Day-2 ops** — see [`docs/runbook-day2-ops.md`](docs/runbook-day2-ops.md).
- **ofelia is host-wide:** with the docker socket mounted it runs the labelled
jobs of **every** stack on the host — run only one labelled stack per host, or
isolate the scheduler.
## TLS / reverse proxy
The `web` port is published on **loopback only**. Front it with one of the
overlays:
```bash
make enable-traefik # or: enable-caddy
$EDITOR .env # set GLPI_HOST
make up
```
See `examples/compose.traefik.yml`, `examples/compose.caddy.yml`, and
`examples/compose.observability.yml`.
## The image
Built from GLPI's bundled release tarball (`glpi-X.Y.Z.tgz`, vendor included) on
`php:8.5-fpm-alpine`. PHP extensions: `bcmath bz2 exif gd intl ldap mbstring
mysqli opcache redis sodium zip` plus the GLPI-required builtins. Runs as
`www-data` (root-owned, read-only code), `tini` PID 1, `SIGQUIT` graceful stop,
cgi-fcgi `/ping` healthcheck.
```bash
# Run a one-off console command against the published image:
docker run --rm ghcr.io/netresearch/glpi-php-fpm:latest \
php /var/www/glpi/bin/console --version
```
Verify provenance and signatures:
```bash
gh attestation verify oci://ghcr.io/netresearch/glpi-php-fpm:12.0.0 \
--owner netresearch
cosign verify ghcr.io/netresearch/glpi-php-fpm:12.0.0 \
--certificate-identity-regexp 'https://github.com/netresearch/glpi-docker-compose-stack/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
```
## Security posture
- Non-root, read-only code layer; php-fpm workers drop to `www-data`.
- `cap_drop: ALL` + `no-new-privileges` + `tmpfs` on app/web; socket-only FastCGI.
- nginx serves only `public/`; config, vendor and the files tree are unreachable.
- Enforcing CSP + standard security headers (see `config/nginx/snippets/`).
- MariaDB durable-by-default with binlog for PITR.
- Multi-arch images with SLSA provenance, SBOM and keyless cosign signatures,
rebuilt daily for base-image CVEs.
Report vulnerabilities per [`SECURITY.md`](SECURITY.md).
## Development & contributing
```bash
make build # build the image locally
make lint # hadolint + shellcheck + yamllint + actionlint
make test # container-structure-test + hardening-check + bats
```
See [`CONTRIBUTING.md`](CONTRIBUTING.md).
## Licensing
- Stack tooling (Dockerfile, compose, scripts, CI): **MIT** — [`LICENSE-MIT`](LICENSE-MIT)
- Documentation: **CC-BY-SA-4.0** — [`LICENSE-CC-BY-SA-4.0`](LICENSE-CC-BY-SA-4.0)
- The packaged GLPI application: **GPL-3.0-or-later** (© the GLPI project / Teclib')