Warning
This project is no longer maintained.
Netresearch has moved away from Snipe-IT to GLPI for asset management. As a result, this repository is archived and will receive no further updates, security patches, or support.
The code remains available for reference and you are welcome to fork it, but use it at your own risk.
Opinionated, hardened, daily-built self-hosted Snipe-IT asset-management deployment — a Docker Compose stack with our own slim PHP 8.5 / Alpine php-fpm image, an always-on Laravel scheduler + queue worker, scheduled phpbu backups, and dev overrides with mailpit and adminer for friction-free local development.
Made by Netresearch DTT GmbH on the back of a real Snipe-IT inventory evaluation. Battle-scarred defaults, not a barebones starter.
git clone https://github.com/netresearch/snipe-it-docker-compose-stack.git
cd snipe-it-docker-compose-stack
make init # bootstraps .env (APP_KEY + random DB passwords, idempotent)
make up # docker compose up -d
# First boot pulls ~700 MB of images (~5 min on a typical link) and runs
# `php artisan migrate --force` (~60-90 s). When `make health` is green:
# visit http://localhost:8000make help lists every target (artisan, backup, upgrade, health, …). For a full walkthrough see Quick start below.
Table of contents
- What's in the stack
- What's in our image
- Why does this exist?
- Quick start
- Common operations
- Dev mode
- TLS / reverse proxy
- Image tags
- Configuration
- Error tracking (Sentry / Bugsink)
- Security posture
- Backups
- Upgrading
- Operating the stack
- Migrating from the upstream single-container image
- Related projects
- Contributing
- License
┌────────────────────────────────────────────────┐
│ internal compose network (`snipeit`) │
│ │
client ──HTTP──► web ──┼──► app ── ghcr.io/netresearch/snipe-it-php-fpm ◄── built daily
(8000) nginx │ │ (php-fpm, handles web requests)
:alpine │ │
│ ├──► worker ── same image, runs
│ │ `php artisan queue:work`
│ │
│ ├──► db ── mariadb:11 (binlog enabled)
│ │
│ └──► valkey ── valkey/valkey:9-alpine
│ │
│ scheduler ── ghcr.io/netresearch/ofelia │
│ │ (runs `artisan schedule:run` per │
│ │ minute and triggers nightly phpbu) │
│ │ │
│ └─► backup ── ghcr.io/netresearch/phpbu-docker
│ │
│ one-shot init: app-assets populates the
│ shared `app-public` volume so nginx can
│ serve Snipe-IT's public/ files statically.
└────────────────────────────────────────────────┘
Only `web` is reachable from the host (port 8000 by default). Every other
service (db, valkey, app, worker, scheduler, backup) has no host-published
port in the default stack — they talk to each other on the internal network.
Seven long-running services plus a one-shot init:
| Service | Image | Purpose |
|---|---|---|
| db | mariadb:11 |
Primary store, binlog enabled for PITR |
| valkey | valkey/valkey:9-alpine |
Cache + sessions + queue backend (Redis-compatible) |
| app | ghcr.io/netresearch/snipe-it-php-fpm |
Our php-fpm image, Snipe-IT app code |
| web | nginx:alpine |
Static asset serving + fastcgi → app (unix socket) |
| worker | (same as app) |
php artisan queue:work daemon — drains async jobs from valkey (emails, EOL reminders, etc.) |
| scheduler | ghcr.io/netresearch/ofelia |
Label-driven cron for artisan schedule:run (per minute) and the nightly phpbu backup |
| backup | ghcr.io/netresearch/phpbu-docker |
Nightly DB dump + uploads/storage tarball with retention policy |
| app-assets | (same as app) |
One-shot init: syncs public/ into the shared app-public volume |
compose.yml is the canonical reference — heavily commented with sizing, security, and resource-limit rationale.
The php-fpm image (ghcr.io/netresearch/snipe-it-php-fpm) is intentionally narrow — just PHP + Snipe-IT app code:
| Base | php:8.5-fpm-alpine |
| PHP extensions | bcmath, exif, gd, intl, ldap, mbstring, opcache, pdo_mysql, redis, xml, zip |
| Runtime user | www-data (non-root) |
| Init | tini as PID 1 → entrypoint → php-fpm |
| Healthcheck | cgi-fcgi ping on /run/php-fpm/snipeit.sock |
| Multi-arch | linux/amd64, linux/arm64 |
| Supply chain | SLSA build provenance (in-toto via attest-build-provenance) + SBOM + keyless cosign signature (OIDC, Rekor-logged) on every push / scheduled build |
| License | AGPL-3.0-or-later (matches Snipe-IT upstream) |
The official snipe/snipe-it image is fine but conservative:
- ships PHP 8.3 (Ubuntu) or 8.4 (Alpine) — not the upstream-recommended 8.5
- the Alpine variant has no built-in scheduler, so Laravel's scheduled tasks (audit reminders, expected-checkin alerts, license expiry warnings) silently don't run
- new versions ship every 3-6 months — base-OS CVEs accrue between releases
This stack fixes all three:
- PHP 8.5 (upstream-supported via
composer.json^8.2) - Scheduler + queue worker run by default via ofelia (Netresearch's fork) and a dedicated
workerservice — no in-container cron, no silently-dropped emails - Daily rebuild picks up Alpine + PHP + Composer-dep patches without waiting for an upstream Snipe-IT release
Already running upstream snipe/snipe-it? See Migrating from the upstream single-container image.
git clone https://github.com/netresearch/snipe-it-docker-compose-stack.git
cd snipe-it-docker-compose-stack
make init # bootstraps .env (APP_KEY + random DB passwords, idempotent)
make up # docker compose up -d
make logs-app # Ctrl-C stops the tail (does NOT stop the stack)
# First boot pulls ~700 MB of images (~5 min on a typical link)
# and runs `php artisan migrate --force` (~60-90 s) — wait for the
# "ready — exec into CMD" line before opening the URL.
# Then open: http://localhost:8000For public deployments, set APP_URL in .env (no trailing slash) AFTER make init and run make down && make up to re-read env across all services (make restart only restarts app + web, so worker would keep emitting stale URLs in queued emails). The reverse-proxy overlay at examples/compose.traefik.yml handles TLS termination.
| Goal | Command |
|---|---|
| Start / stop the stack | make up / make down |
Restart app + web only (no env re-read) |
make restart |
| Tail logs | make logs-app (one service) or make logs (all) |
| Aggregated health (wire into monitoring) | make health |
| One-shot artisan command | make artisan CMD="route:list" |
| Interactive Laravel REPL | make tinker |
Shell inside app |
make shell |
| On-demand backup | make backup (ofelia auto-runs nightly at 03:00) |
| Pull + recreate + tail | make upgrade |
| Show enabled overlays | make overlays |
| Enable an overlay | make enable-traefik / make enable-caddy / make enable-observability / make enable-bugsink |
| Disable an overlay | make disable-traefik / make disable-caddy / make disable-observability / make disable-bugsink |
| Wire external Sentry/Bugsink | make enable-sentry DSN=https://... / make disable-sentry |
make help is the canonical, always-current list.
cp compose.override.yml.example compose.override.yml
docker compose up -dBrings up the same stack plus:
- mailpit at http://localhost:8025 — SMTP sink + web UI to catch outgoing notifications
- adminer at http://localhost:8081 — DB browser
- Exposed db (3306) + valkey (6379) host ports for external clients
APP_DEBUG=true,APP_ENV=local
The default web service binds plain HTTP on ${SNIPEIT_HTTP_PORT:-8000}. Front it with your TLS terminator of choice. Two overlays ship in examples/:
# Traefik (requires an existing traefik network)
docker compose -f compose.yml -f examples/compose.traefik.yml up -d
# Caddy
docker compose -f compose.yml -f examples/compose.caddy.yml up -dFor host-side nginx / HAProxy / your existing terminator, point it at 127.0.0.1:${SNIPEIT_HTTP_PORT} and set APP_URL=https://… in .env. A Prometheus-scraping overlay also ships at examples/compose.observability.yml.
Built daily, multi-arch (linux/amd64 + linux/arm64), in two variants. Pinned (default) honours Snipe-IT's composer.lock; rolling (suffix -rolling) re-resolves Composer ranges so CVE fixes in transitive deps land faster.
| Tag | Variant | Resolves to |
|---|---|---|
latest |
pinned | Latest stable Snipe-IT release (tracks .snipe-it-version) |
8.5.0 |
pinned | refs/tags/v8.5.0 |
8.5.0-YYYYMMDD |
pinned | Reproducible dated build — audit-friendly |
8.5 / 8 |
pinned | Auto-rolls on patch / minor bump |
master / develop / nightly |
pinned | Upstream branch HEAD |
master-rolling / develop-rolling / nightly-rolling |
rolling | Upstream branch HEAD with composer.lock deleted |
<tag>-rolling |
rolling | Same source ref as pinned, composer.lock deleted before install |
sha-pinned-<sha> / sha-rolling-<sha> |
both | Per-stack-commit build |
Pick pinned for production — reproducible, manifest-equivalent rebuilds (modulo base-image patches). Pick rolling if you'd rather catch transitive-dep CVEs early than match upstream's tested dependency graph.
Rolling-variant caveat: rolling builds can fail (and skip publishing) when upstream's composer.json references a major version of a dependency entirely covered by a Composer audit advisory — Composer refuses to install vulnerable packages by default. When this happens, the pinned image still ships because its lockfile pins the specific safe version upstream chose; the rolling tag lags until Snipe-IT bumps its constraint. Watch the failed rolling job in CI to see which advisory tripped it.
Each image (both variants) ships a manifest at /var/lib/snipeit/deps.txt — docker exec <container> cat /var/lib/snipeit/deps.txt to see exactly which versions are installed.
.env.example is the complete reference. Required vars:
| Variable | Description |
|---|---|
APP_KEY |
Laravel application key — generate once, never rotate |
APP_URL |
Public URL, no trailing slash |
DB_PASSWORD |
Application DB user password |
DB_ROOT_PASSWORD |
MariaDB root (only used at init) |
Operational toggles:
| Variable | Default | Description |
|---|---|---|
SNIPE_IT_IMAGE_TAG |
latest |
Pin to a specific image build |
CACHE_DRIVER / SESSION_DRIVER / QUEUE_CONNECTION |
redis |
Laravel driver names (RESP protocol). Flip to file/file/sync if you remove the valkey service |
SKIP_MIGRATIONS |
false |
Skip php artisan migrate --force at container start |
TZ |
UTC |
IANA timezone — sets the OS clock inside containers (log/cron timestamps) |
APP_TIMEZONE |
inherits TZ |
What Laravel reads for date/time display; override only if it must diverge from TZ |
SENTRY_LARAVEL_DSN |
(empty) | Error tracking DSN — empty disables; see Error tracking |
Docker secrets supported via *_FILE env vars (e.g. DB_PASSWORD_FILE=/run/secrets/db_password).
The image ships sentry/sentry-laravel pre-installed. Set SENTRY_LARAVEL_DSN to your project DSN (http(s)://<key>@<host>/<project-id>) to start collecting errors; an empty DSN keeps the SDK silent (no network calls). Bugsink is recommended for self-hosting — it speaks the Sentry wire protocol and avoids the SaaS data-egress concern for an internal asset-management tool.
| Goal | Command |
|---|---|
| Point at an external Sentry/Bugsink instance | make enable-sentry DSN=https://<key>@<host>/<project-id> |
| Run a self-hosted Bugsink in-stack with auto-wired DSN | make enable-bugsink |
| Stop external reporting | make disable-sentry |
| Remove the in-stack Bugsink overlay | make disable-bugsink |
| Show what's currently enabled | make overlays |
Both enable-* targets mutate .env; after enabling, make up brings the configured stack up — no manual -f chaining. The Bugsink overlay adds a SQLite-backed bugsink service plus a one-shot bugsink-init container that seeds a project and writes its DSN into a tmpfs-backed shared volume; app and worker pick it up via SENTRY_LARAVEL_DSN_FILE. Re-runs are idempotent.
When fronting Bugsink with TLS, set BUGSINK_PUBLIC_URL=https://bugsink.example.com and BUGSINK_BEHIND_HTTPS_PROXY=true — see the comments in examples/compose.bugsink.yml for the X-Forwarded-* trust caveat (Traefik strips client-supplied headers by default; Caddy needs trusted_proxies configured). BUGSINK_ADMIN_PASSWORD is a bootstrap-only value — rotate it via the Bugsink UI on first login, then clear it from .env.
- Non-root execution —
www-dataruns php-fpm and queue:work; entrypoint drops privileges withsu-execafter repairing volume permissions - No new privileges —
security_opt: no-new-privileges:trueon every long-running service (db/valkey/app/web/worker/scheduler/backup, plus the bugsink overlay) - Capability drop —
cap_drop: ALLonapp,web, andworkerwith minimal re-adds (seecompose.ymlfor the per-service list); db/valkey/scheduler/backup keep default caps the upstream images expect - Overlay parity — opt-in overlays in
examples/(Bugsink, Traefik, Caddy, observability) ship with the sameno-new-privileges+cap_drop: ALLposture on every long-running service they add - Read-only mounts — nginx reads
app-publicandapp-storageread-only - tmpfs —
/tmp,/var/cache/nginx,/var/runare tmpfs onweb;bootstrap/cacheis tmpfs onapp(prevents attacker-written PHP from persisting across restarts) - Unix-socket-only php-fpm — no TCP listener on 9000, so a sibling container on the snipeit network can't bypass nginx and speak FastCGI directly to
app - Pinned upstream — Snipe-IT git-tag-pinned via
.snipe-it-version; base PHP + Alpine versions tag-pinned inDockerfile(rebuilt daily so re-tagged registry content is picked up) - Daily rebuild — picks up base-image CVEs without waiting for upstream
- Supply chain — SLSA build provenance attestations (in-toto via
attest-build-provenance), SBOM (BuildKitsbom=true), and a keyless cosign signature on the image manifest (OIDC, Rekor-logged) — all attached on every push and scheduled build - CVE scanning — daily Trivy + osv-scanner runs (see Actions → security). Findings are informational, NOT CI gates — most flagged CVEs are in Snipe-IT's upstream-pinned
composer.lock(e.g.phpseclib,onelogin/php-saml) and need an upstream fix. Trivy SARIF uploads to GitHub code-scanning; subscribe via the repo Security tab for new-finding alerts.
phpbu runs nightly at 03:00 (ofelia-driven) and produces three artefact families in the backups volume:
| Path | Contents | Retention |
|---|---|---|
db/snipeit-db-*.sql.gz |
mariadb-dump (single-transaction) | rolling capacity (~5 GB) |
uploads/snipeit-uploads-*.tar.gz |
Snipe-IT uploads (app-data volume) |
30 days |
storage/snipeit-storage-*.tar.gz |
Laravel storage (app-storage volume) |
30 days |
On-demand backup: make backup. Off-host shipping: bind-mount the backups volume into a destination synced by your existing tool (restic, rclone, NAS-attached cron). Disaster-recovery procedure: docs/runbook-restore.md.
make upgrade # pulls latest images, recreates containers, follows logsThe app entrypoint runs php artisan migrate --force on every start. No DDL grant dance required — this stack's DB user is the app's own MariaDB account with full schema rights inside its database.
When something breaks, docs/runbook-day2-ops.md catalogues the failure modes we know about — symptom → first check → recovery. Common ones:
- App returns 500 —
make logs-app(Laravel logs to stdout as JSON viaLOG_CHANNEL=stderr+LOG_STDERR_FORMATTER) - Users randomly logged out — Valkey LRU eviction; tune
--maxmemoryincompose.ymlor switch toSESSION_DRIVER=file make upcomplains about missing.env— runmake initfirst; the Makefile guard prevents the empty-root-password footgun- Backup-volume full —
make backup-verifyflags it; tune retention inconfig/phpbu/backup.json
make health shows the aggregated health state of all containers and fails loudly when one is unhealthy — wire it into your existing monitoring.
If you're moving from snipe/snipe-it:vX.Y.Z-alpine (Apache + PHP-FPM + cron in one container), follow docs/migration-from-upstream-single-container.md. It covers volume mapping, env-var translation, and the cutover sequence (the upstream STORAGE_DIR and PRIVATE_UPLOADS_DIR aren't a 1:1 match for this stack's app-data / app-storage split).
- grokability/snipe-it — upstream Snipe-IT itself
- snipe/snipe-it — official Docker image
- netresearch/ofelia — the scheduler this stack uses
- netresearch/phpbu-docker — the backup engine this stack uses
PRs welcome. Standard community files (CONTRIBUTING / CODE_OF_CONDUCT / SECURITY) inherit from Netresearch's org-level .github repo.
Security issues: please report via the standard Netresearch security contact rather than as a public issue.
This repository uses split licensing — the right tool for each part:
| Path | License | Rationale |
|---|---|---|
Dockerfile, rootfs/, config/, compose*.yml, examples/, .github/, bin/, Makefile, tests/, renovate.json, .snipe-it-version |
MIT | Code and code-shaped configuration |
README.md, CHANGELOG.md, docs/** |
CC-BY-SA-4.0 | Prose and documentation — share-alike keeps forks open |
The built image (ghcr.io/netresearch/snipe-it-php-fpm:*) bundles AGPL-3.0 Snipe-IT application code from grokability/snipe-it. Redistribution of the image is bound by the upstream AGPL-3.0 terms in addition to MIT for our build glue.
This split follows the Netresearch skill-repo licensing pattern.