Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
This repository was archived by the owner on Jun 27, 2026. It is now read-only.

netresearch/snipe-it-docker-compose-stack

Open more actions menu

Snipe-IT Docker Compose Stack

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.

Build Lint Code: MIT Content: CC-BY-SA-4.0 Image: AGPL-3.0

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.

Try it in 60 seconds

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:8000

make help lists every target (artisan, backup, upgrade, health, …). For a full walkthrough see Quick start below.

Table of contents

What's in the stack

                          ┌────────────────────────────────────────────────┐
                          │  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.

What's in our image

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)

Why does this exist?

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:

  1. PHP 8.5 (upstream-supported via composer.json ^8.2)
  2. Scheduler + queue worker run by default via ofelia (Netresearch's fork) and a dedicated worker service — no in-container cron, no silently-dropped emails
  3. 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.

Quick start

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:8000

For 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.

Common operations

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.

Dev mode

cp compose.override.yml.example compose.override.yml
docker compose up -d

Brings 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

TLS / reverse proxy

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 -d

For 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.

Image tags

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.txtdocker exec <container> cat /var/lib/snipeit/deps.txt to see exactly which versions are installed.

Configuration

.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).

Error tracking (Sentry / Bugsink)

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.

Security posture

  • Non-root executionwww-data runs php-fpm and queue:work; entrypoint drops privileges with su-exec after repairing volume permissions
  • No new privilegessecurity_opt: no-new-privileges:true on every long-running service (db/valkey/app/web/worker/scheduler/backup, plus the bugsink overlay)
  • Capability dropcap_drop: ALL on app, web, and worker with minimal re-adds (see compose.yml for 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 same no-new-privileges + cap_drop: ALL posture on every long-running service they add
  • Read-only mounts — nginx reads app-public and app-storage read-only
  • tmpfs/tmp, /var/cache/nginx, /var/run are tmpfs on web; bootstrap/cache is tmpfs on app (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 in Dockerfile (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 (BuildKit sbom=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.

Backups

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.

Upgrading

make upgrade           # pulls latest images, recreates containers, follows logs

The 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.

Operating the stack

When something breaks, docs/runbook-day2-ops.md catalogues the failure modes we know about — symptom → first check → recovery. Common ones:

  • App returns 500make logs-app (Laravel logs to stdout as JSON via LOG_CHANNEL=stderr + LOG_STDERR_FORMATTER)
  • Users randomly logged out — Valkey LRU eviction; tune --maxmemory in compose.yml or switch to SESSION_DRIVER=file
  • make up complains about missing .env — run make init first; the Makefile guard prevents the empty-root-password footgun
  • Backup-volume fullmake backup-verify flags it; tune retention in config/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.

Migrating from the upstream single-container image

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).

Related projects

Contributing

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.

License

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.

About

Opinionated Snipe-IT stack — daily-built PHP 8.5 / Alpine php-fpm image plus 6-service compose stack (MariaDB, Valkey, nginx, ofelia-driven scheduler). Multi-arch (amd64/arm64), supply-chain-signed.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages

Morty Proxy This is a proxified and sanitized view of the page, visit original site.