Use case · IT asset management

Snipe-IT self hosted on a dedicated server: Docker, MariaDB and a tested restore

A self-hosted Snipe-IT is the official snipe/snipe-it Docker image next to a MariaDB container, configured through one .env file (APP_KEY, APP_URL, the DB_* and MAIL_* lines), with the first administrator created before anything is public and a backup that carries the database, the uploads and that .env. This page builds it on one dedicated server running Ubuntu 24.04, from Snipe-IT's own documentation and source, and marks each place where we depart from them.

See the machines

What Snipe-IT runs on

Snipe-IT is a Laravel (PHP) application on MySQL 5.7+ or MariaDB (requirements page). The Docker image bundles it all (ubuntu:24.04, PHP 8.3 and Apache, per its Dockerfile) and keeps its state in one volume, /var/lib/snipeit. The docs state no CPU, memory or disk figure; disk is the number to plan, for the database, attachments and however many backup copies you retain.

PieceRuns asData
Snipe-IT, image snipe/snipe-it:v8.8.3container app, 127.0.0.1:8000 to port 80volume storage at /var/lib/snipeit
MariaDB 11.4.7container db, no published portvolume db_data
Scheduler (alerts, weekly backup)inside app, under supervisordsame volume
Caddyhost package, ports 80 and 443/etc/caddy/Caddyfile
Settings and secretsread by Compose/opt/snipeit/.env, mode 600

Picking the machine

An asset register holds serial numbers, licence keys and who has which laptop. Here it sits on disks no other tenant shares, on our own network, AS19624 (check it at bgp.tools).

Two classes from the dedicated server range suit it. A single-socket rack server is the base for an application with its database on the same box; every racked single-, dual- and quad-socket machine carries an out-of-band console (iLO, iDRAC or a Supermicro BMC). A Moonshot cartridge also works for one service per machine, but only the m510 and m710x cartridges carry a console (the m710x is the Xeon E3-1585L v5 line); the rest have power control only, and the console is the way back in after a bad firewall rule. Cartridges have no RAID controller; the rack and blade lines have a hardware one.

Excluded: the X-Gene ARM cartridge. Its installer list ends at Ubuntu 18.04, older than the 24.04 used here.

Delivery: about 30 minutes from payment approval for a racked machine (ready servers); custom builds need 4 to 24 hours.

Installing with the official compose file

  1. Put Ubuntu 24.04 on the machine from the control panel and add Docker Engine plus the Compose plugin by Docker's install guide; Snipe-IT's Docker page leaves that part to Docker.
  2. Create the DNS records for the name you will use, assets.example.com here: an A record, plus an AAAA if the server has an IPv6 address. Caddy asks for its certificate the moment it loads the site, so the name must resolve first: getent ahosts assets.example.com.
  3. As root, paste the first block. It stops at once unless Docker Engine is 28 or later, or if /opt/snipeit/.env already exists. Then, in order: ufw's IPv6 switch, OpenSSH allowed, firewall on, the v8.8.3 docker-compose.yml and .env.docker, with .env at mode 600 before any secret enters it and the shipped changeme1234 passwords replaced. It must end with ok.
if ! [ "$(docker version --format '{{.Server.Version}}' 2>/dev/null | cut -d. -f1)" -ge 28 ] 2>/dev/null; then
  echo 'STOP: Docker Engine 28 or later is required'
elif [ -e /opt/snipeit/.env ]; then
  echo 'STOP: /opt/snipeit/.env exists; already set up'
else
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw --force enable
apt-get update && apt-get install -y unzip
install -d -m 700 /opt/snipeit
R=https://raw.githubusercontent.com/grokability/snipe-it/v8.8.3
curl -fsSL -o /opt/snipeit/docker-compose.yml $R/docker-compose.yml
install -m 600 /dev/null /opt/snipeit/.env
curl -fsSL -o /opt/snipeit/.env $R/.env.docker
sed -i -e 's/^APP_VERSION=.*/APP_VERSION=v8.8.3/' \
  -e 's/^APP_PORT=.*/APP_PORT=127.0.0.1:8000/' \
  -e 's|^APP_URL=.*|APP_URL=http://127.0.0.1:8000|' \
  -e "s/^DB_PASSWORD=.*/DB_PASSWORD=$(openssl rand -hex 24)/" \
  -e "s/^MYSQL_ROOT_PASSWORD=.*/MYSQL_ROOT_PASSWORD=$(openssl rand -hex 24)/" \
  /opt/snipeit/.env
grep -q '^APP_PORT=127.0.0.1:8000$' /opt/snipeit/.env && ! grep -q changeme1234 /opt/snipeit/.env && echo ok
fi

Our changes, all in .env (docker-compose.yml is unchanged): APP_VERSION=v8.8.3, since the Docker page warns against the latest tag; APP_PORT=127.0.0.1:8000, so the compose line ${APP_PORT:-8000}:80 publishes on loopback only; APP_URL on the tunnel address for now. A mapping with no address is published on 0.0.0.0 and [::], and that traffic skips ufw's rules on IPv4 (on IPv6 too when Docker's own IPv6 networking is on); a 127.0.0.1 mapping is reachable from the host alone on Docker Engine 28 and later. The generated passwords pass through sed's command line, so ps shows them for an instant: harmless on a single-admin box, otherwise type them in with nano.

Before the first start, open /opt/snipeit/.env in nano and fill the outgoing mail block: MAIL_HOST, MAIL_PORT (the docs' example uses 587), MAIL_USERNAME, MAIL_PASSWORD, MAIL_FROM_ADDR, MAIL_FROM_NAME and MAIL_REPLYTO_ADDR. Per the configuration page, a null MAIL_FROM_ADDR can throw 500 errors and the reply-to is required; without mail there are no password resets, no expiry or inventory alerts and no asset acceptance or EULA confirmations. Set APP_TIMEZONE too. The second block generates APP_KEY with the documented php artisan key:generate --show (our -T keeps a carriage return out) only while the sample key is still in .env, and starts the stack only if the key check passes.

C="docker compose -f /opt/snipeit/docker-compose.yml"
if grep -q 3ilviXqB9u6 /opt/snipeit/.env; then
  KEY=$($C run --rm -T app php artisan key:generate --show)
  sed -i "s|^APP_KEY=.*|APP_KEY=$KEY|" /opt/snipeit/.env
else
  echo 'APP_KEY already set: left alone'
fi
grep -qE '^APP_KEY=base64:[A-Za-z0-9+/]{43}=$' /opt/snipeit/.env && ! grep -q 3ilviXqB9u6 /opt/snipeit/.env && $C up -d && $C port app 80

It must end by printing 127.0.0.1:8000. docker compose run also starts MariaDB, which creates its users from .env on first start; hence the passwords came first. Both blocks are for the first install only. Never regenerate APP_KEY on a running install: the configuration page calls it the key that stores encrypted data, and says to keep a copy safe, such as in a password manager.

The first administrator, over your own tunnel

Snipe-IT creates its first user on a web setup screen, and whoever gets there first is a superuser (SetupController.php), so finish it while the app answers only on loopback. On your workstation, start a tunnel through your own login: ssh -N -L 8000:127.0.0.1:8000 you@your-server. On the first connection only, SSH asks Are you sure you want to continue connecting (yes/no/[fingerprint])?; answer after checking the host key. With password login it also asks for your password. Keep the local port at 8000: in our reading of AppServiceProvider.php, Snipe-IT builds every link from APP_URL, so the browser must use exactly http://127.0.0.1:8000.

  1. Browse to http://127.0.0.1:8000; it redirects to the pre-flight page, which checks the database connection, APP_URL against the address you used, and storage permissions.
  2. One check may read red: it fetches APP_URL/.env from inside the container and, per SetupController.php, reports the file exposed when that fetch fails. Nothing in the container listens on 8000, so that is expected here; you test it for real once the name is live.
  3. Create the database tables, then the first user: site name, your name, email, username and password. The screen offers to email you the credentials; leaving it off keeps the password out of your mailbox (our advice).
  4. Test the mail settings from the pre-flight, as the configuration page suggests.

Going public: Caddy, APP_URL and trusted proxies

With the admin in place, point Snipe-IT at its real name and tell it about the proxy. Run sed -i -e 's|^APP_URL=.*|APP_URL=https://assets.example.com|' -e 's/^APP_TRUSTED_PROXIES=.*/APP_TRUSTED_PROXIES=REMOTE_ADDR/' -e 's/^SECURE_COOKIES=.*/SECURE_COOKIES=true/' /opt/snipeit/.env, then echo 'APP_TRUSTED_HEADERS=HEADER_X_FORWARDED_FOR,HEADER_X_FORWARDED_HOST,HEADER_X_FORWARDED_PROTO' >> /opt/snipeit/.env and docker compose -f /opt/snipeit/docker-compose.yml up -d. The Docker page is explicit that a changed .env needs the container recreated (up -d), not restarted.

The first two values are the Docker page's own for a TLS proxy. APP_TRUSTED_HEADERS is newer than that page: config/trustedproxy.php in v8.8.3 says that when it is unset no forwarded header is trusted, so every visitor would look like the proxy. Trusting whatever address connects is acceptable because the only path to the container's port 80 is the loopback-published 8000, which Engine 28+ keeps to the host (our reasoning).

Now the proxy. Ubuntu 24.04 ships Caddy in its archive: ufw allow 80,443/tcp, apt-get install -y caddy, then printf 'assets.example.com {\n\treverse_proxy 127.0.0.1:8000\n}\n' > /etc/caddy/Caddyfile and systemctl reload caddy. Caddy runs on the host as an ordinary process, so without the ufw rule it is blocked on IPv4 and IPv6 alike. It obtains a Let's Encrypt certificate for the name and sends the X-Forwarded headers to the app by default (Caddy docs). Finish with curl -s -o /dev/null -w '%{http_code}\n' https://assets.example.com/.env: anything but 200 means the file is not served.

Backups that hold everything

Snipe-IT's backup, from Admin > Backups or php artisan snipeit:backup, writes a zip with an SQL dump plus uploaded images and attachments. In the image it lands in /var/lib/snipeit/dumps, in the data volume, and the scheduler already makes one weekly (docker/supervisord.conf runs artisan schedule:run every minute). What the zip lacks: config/backup.php adds .env only when BACKUP_ENV=true, and in the container that is the image's own copy, which the Dockerfile copies from docker/docker.env, not your settings. Your APP_KEY and passwords live only in /opt/snipeit/.env, so the script copies it beside each zip (our reading of the two files).

# /opt/snipeit/backup.sh -- run as root: bash /opt/snipeit/backup.sh
set -euo pipefail
umask 077
C="docker compose -f /opt/snipeit/docker-compose.yml"
B=/var/backups/snipeit
N=nightly-$(date +%F-%H%M)
mkdir -p -m 700 "$B"
$C exec -T -u docker app php artisan snipeit:backup --filename="$N"
$C cp "app:/var/lib/snipeit/dumps/$N.zip" "$B/$N.zip"
unzip -Z1 "$B/$N.zip" | grep '\.sql$' > /dev/null
unzip -tq "$B/$N.zip" > /dev/null
tar -czf "$B/$N-config.tar.gz" -C /opt/snipeit .env docker-compose.yml
find "$B" -name 'nightly-*' -mtime +30 -delete

pipefail stops a failed step leaving a valid-looking set, and the script refuses a zip with no .sql file or one failing unzip -t. Thirty days locally is our choice. Schedule it: install -d -m 700 /var/log/snipeit-backup, then printf '20 3 * * * root umask 077; bash /opt/snipeit/backup.sh > /var/log/snipeit-backup/last.log 2>&1\n' > /etc/cron.d/snipeit-backup. The zip includes the OAuth keys but not LDAP client certificates kept in /var/lib/snipeit/keys; copy those by hand if you use them. Then copy /var/backups/snipeit to another machine yourself with a plain rsync -a (never --delete, which would let an emptied source empty the remote copy too) and prune there with its own job. The nightly zips also stay in the volume, where the scheduled backup:clean (app/Console/Kernel.php) prunes them.

Restoring over the running install

php artisan snipeit:restore brings back the database and uploaded files from a Snipe-IT zip without modifying .env; the backup page warns it signs everyone out. It hands the database password to the client via the environment, not the command line (RestoreFromBackup.php). Run every step as root (sudo -i first). STAMP is the backup's time, for example 2026-10-23-0320.

  1. unzip -tq /var/backups/snipeit/nightly-STAMP.zip && echo ok must print ok.
  2. Save the current state first: docker compose -f /opt/snipeit/docker-compose.yml exec -T -u docker app php artisan snipeit:backup --filename=before-restore, then docker compose -f /opt/snipeit/docker-compose.yml cp app:/var/lib/snipeit/dumps/before-restore.zip /root/ && unzip -tq /root/before-restore.zip && echo ok. Stop here unless it prints ok.
  3. docker compose -f /opt/snipeit/docker-compose.yml cp /var/backups/snipeit/nightly-STAMP.zip app:/tmp/restore.zip && docker compose -f /opt/snipeit/docker-compose.yml exec -T app chown docker /tmp/restore.zip
  4. docker compose -f /opt/snipeit/docker-compose.yml exec -T -u docker app php artisan snipeit:restore --force /tmp/restore.zip. Without --force it asks: Are you sure you wish to restore from the given backup file? This can lead to MASSIVE DATA LOSS!
  5. Check recent check-outs, then docker compose -f /opt/snipeit/docker-compose.yml exec -T app rm /tmp/restore.zip. To roll back, repeat steps 3 and 4 with /root/before-restore.zip.
  6. On a new server instead: unpack the matching -config.tar.gz into /opt/snipeit before the first up -d, because MariaDB creates its users from .env only on an empty volume and the restored data needs the old APP_KEY; start it with up -d, then follow steps 3 to 5.

Updates and the licence

The upgrade page's rule is to back up the database and .env first. upgrade.php is built for a git install, where php upgrade.php (--branch tags/vX.Y.Z for a given release) backs up, enters maintenance mode, pulls, runs composer and migrates; on other installs it only backs up and clears caches. In Docker the image carries the code, so an update is: run backup.sh, set APP_VERSION in .env to the new tag, then docker compose -f /opt/snipeit/docker-compose.yml pull and docker compose -f /opt/snipeit/docker-compose.yml up -d. Per docker/startup.sh the container runs php artisan migrate --force on every start; docker compose -f /opt/snipeit/docker-compose.yml exec app php artisan migrate --force afterwards should report nothing to migrate.

Snipe-IT is licensed AGPL-3.0, a copyleft licence (LICENSE in the repository). Our general reading, not legal advice: no duty arises from running it unmodified for your own organisation. Hand copies to anyone, modified or not, and the licence and the corresponding source go with them. Run a modified build that users reach over a network, and those users must be offered its source.

Questions

Can I install Snipe-IT on Ubuntu without Docker?

Yes. The docs recommend a git clone, with its public directory as the web root and PHP plus MySQL or MariaDB on the host; a manual install needs its database and user made by hand. There is also install.sh, meant only for a fresh Ubuntu, Debian or CentOS system with no other sites on it. Updates then go through upgrade.php.

Why does the setup page complain about APP_URL?

The pre-flight compares APP_URL with the address in your browser. Set APP_URL to exactly that address, no trailing slash, then recreate with up -d; per the Docker page, docker compose restart keeps old values.

How soon can the server be running?

About 30 minutes from payment approval for a racked machine; payment review is the part that varies. A build to order takes 4 to 24 hours.

Run your asset register on your own hardware

Choose a single-socket server or a cartridge with a console, install Ubuntu 24.04 from the panel and follow the steps above.

See the machines

Related on Server Room

Other use cases