Snipe-IT » History » Version 4
Redmine Admin, 10/31/2026 07:01 PM
deploy-publish.sh
| 1 | 4 | Redmine Admin | *Chapter 4 of the dmz-web documentation · headings are numbered like the cheat sheets · index: [[Wiki]]* |
|---|---|---|---|
| 2 | |||
| 3 | {{>toc}} |
||
| 4 | |||
| 5 | 2 | Redmine Admin | Deployed by `~/deploy/snipeit-install.sh [domain] [--force] [--skip-npm-api]` (same |
| 6 | pattern as `add-wordpress-site.sh`; restarts NPM at step 6). |
||
| 7 | Containers `site-snipeit` (`docker.io/snipe/snipe-it`, Apache :80) + |
||
| 8 | `db-snipeit` (`mariadb:11`) on `net-snipeit`. Data volume `app-snipeit-data` |
||
| 9 | at **`/var/lib/snipeit`** (uploads, backups, OAuth keys) — app code is in the |
||
| 10 | image. Secrets: `snipeit-db-root-password`, `snipeit-db-password`, |
||
| 11 | 1 | Redmine Admin | `snipeit-app-key`. |
| 12 | |||
| 13 | 2 | Redmine Admin | Key env (behind NPM): `APP_URL=https://assets.mdsdefense.com`, |
| 14 | `APP_FORCE_TLS=true`, `APP_TRUSTED_PROXIES=*`, `SECURE_COOKIES=true`, |
||
| 15 | `DB_HOST=db-snipeit`, `APP_KEY` from secret. **`APP_KEY` must never change** |
||
| 16 | 1 | Redmine Admin | — it encrypts fields in the DB; back the secret up with the DB. |
| 17 | |||
| 18 | 2 | Redmine Admin | ```bash |
| 19 | 1 | Redmine Admin | # generate APP_KEY once (before first start) |
| 20 | podman run --rm docker.io/snipe/snipe-it:latest php artisan key:generate --show |
||
| 21 | printf '%s' 'base64:...' | podman secret create snipeit-app-key - |
||
| 22 | |||
| 23 | # first start runs migrations; watch it |
||
| 24 | podman logs -f site-snipeit |
||
| 25 | |||
| 26 | # artisan inside the container |
||
| 27 | podman exec -it site-snipeit php artisan about |
||
| 28 | podman exec -it site-snipeit php artisan snipeit:backup |
||
| 29 | podman exec -it site-snipeit php artisan config:clear # after env changes + restart |
||
| 30 | |||
| 31 | # DB (same MariaDB recipe as WordPress) |
||
| 32 | podman exec -it db-snipeit sh -c 'mariadb -uroot -p"$MYSQL_ROOT_PASSWORD"' |
||
| 33 | podman exec db-snipeit sh -c 'mariadb-dump -uroot -p"$MYSQL_ROOT_PASSWORD" snipeit' > ~/tmp/snipeit-$(date +%F).sql |
||
| 34 | ``` |
||
| 35 | |||
| 36 | 4 | Redmine Admin | ## 4.1 First-run pre-flight (Configuration Check) |
| 37 | 1 | Redmine Admin | |
| 38 | 2 | Redmine Admin | Screenshot (2026-10-25, Snipe-IT v8.8.3): |
| 39 | 1 | Redmine Admin | |
| 40 | 2 | Redmine Admin |  |
| 41 | 1 | Redmine Admin | |
| 42 | 2 | Redmine Admin | Behind NPM the check shows **two red rows that are expected and can be ignored**; |
| 43 | "Next: Create database tables" stays enabled: |
||
| 44 | |||
| 45 | | Row | What it says | Why it is red here | Verdict | How it was verified | |
||
| 46 | |-----|--------------|--------------------|---------|---------------------| |
||
| 47 | | **URL** | "thinks your URL is https://… but your real URL is http://…" | NPM owns 443 and terminates TLS; it talks plain HTTP to `site-snipeit:80`, so the app sees `http`. `APP_URL=https://assets.mdsdefense.com` in the unit is correct. There is no `.env` to edit — config is the unit's `Environment=` lines. | Normal behind a TLS-terminating proxy — **ignore** | After setup: login works without a redirect loop and every link/redirect is `https://`. Header test below. | |
||
| 48 | | **Config File** | "cannot determine if your config file is exposed" | The check can't probe from outside. In the image `.env` is outside Apache's document root (`public/`). | **ignore** | `/.env` returns **403** through NPM (command below). | |
||
| 49 | | Email | test button | `MAIL_MAILER=log` | skip until the SMTP relay is on — but the From address must be valid even now (see Mail) | — | |
||
| 50 | |||
| 51 | Verification commands (websvcmaster): |
||
| 52 | |||
| 53 | ```bash |
||
| 54 | # Config File: .env must not be served -> 403/404 |
||
| 55 | curl -sI --resolve assets.mdsdefense.com:444:127.0.0.1 https://assets.mdsdefense.com:444/.env | head -1 |
||
| 56 | |||
| 57 | # URL: the app must build https redirects when NPM's forwarded header is present |
||
| 58 | podman exec site-snipeit sh -c 'curl -sI -H "X-Forwarded-Proto: https" -H "Host: assets.mdsdefense.com" http://127.0.0.1/ | grep -i ^location' |
||
| 59 | # https://assets.mdsdefense.com/... -> OK |
||
| 60 | # http://... -> APP_TRUSTED_PROXIES not honoured: set it to the |
||
| 61 | # net-snipeit subnet (podman network inspect net-snipeit | jq -r '.[0].subnets[0].subnet'), |
||
| 62 | # cp unit, daemon-reload, restart |
||
| 63 | # If OK here but the browser still gets http links: NPM isn't sending the header -> |
||
| 64 | # proxy host -> Advanced -> proxy_set_header X-Forwarded-Proto https; |
||
| 65 | |||
| 66 | # Overall: 302 to /login (or /setup before setup) is the healthy answer |
||
| 67 | check-sites.sh | grep snipeit |
||
| 68 | 1 | Redmine Admin | ``` |
| 69 | 2 | Redmine Admin | |
| 70 | 4 | Redmine Admin | ## 4.2 Mail (SMTP relay) |
| 71 | 2 | Redmine Admin | |
| 72 | Same Exchange Online IP connector as Redmine — no credentials. **From and Reply-To |
||
| 73 | must always be set to real addresses, even while the mailer is `log`.** The image's |
||
| 74 | default is the unresolved placeholder `${MAIL_ENV_FROM_ADDR}`, which is not a valid |
||
| 75 | address, so Snipe-IT answers HTTP 500 whenever it builds *any* email (creating a user, |
||
| 76 | the Test Email button, a password reset) — whatever the mailer. Found 2026-10-25: |
||
| 77 | 500 while creating the first admin; the user was saved, only the email step failed. |
||
| 78 | The setup form pre-fills the same placeholder in its address field and rejects it (no `@`); |
||
| 79 | typing a real address there fixes the form only — the From used when a mail is sent still |
||
| 80 | comes from the environment variable, so the 500 stays until the unit is fixed. |
||
| 81 | |||
| 82 | `snipeit-install.sh` v1.1 writes the unit like this (v1.0 had the From lines commented out): |
||
| 83 | |||
| 84 | ```ini |
||
| 85 | Environment=MAIL_FROM_ADDR=sysadmin@mdsdefense.com |
||
| 86 | Environment=MAIL_FROM_NAME=Snipe-IT |
||
| 87 | Environment=MAIL_REPLYTO_ADDR=sysadmin@mdsdefense.com |
||
| 88 | Environment=MAIL_REPLYTO_NAME=Snipe-IT |
||
| 89 | Environment=MAIL_MAILER=log |
||
| 90 | #Environment=MAIL_MAILER=smtp |
||
| 91 | #Environment=MAIL_HOST=mdsdefense-com.mail.protection.outlook.com |
||
| 92 | #Environment=MAIL_PORT=25 |
||
| 93 | #Environment=MAIL_ENCRYPTION=tls |
||
| 94 | ``` |
||
| 95 | |||
| 96 | To fix a unit generated by v1.0 **and** switch SMTP on in one go (repo copy, websvcmaster; |
||
| 97 | for a v1.1 unit drop the last two `-e` lines, they would duplicate Reply-To): |
||
| 98 | |||
| 99 | ```bash |
||
| 100 | cd ~/deploy |
||
| 101 | sed -i -e '/^Environment=MAIL_MAILER=log$/d' \ |
||
| 102 | -e 's/^#\(Environment=MAIL_\)/\1/' \ |
||
| 103 | -e 's/<tenant>\.mail\.protection\.outlook\.com/mdsdefense-com.mail.protection.outlook.com/' \ |
||
| 104 | -e '/^Environment=MAIL_FROM_NAME=/a Environment=MAIL_REPLYTO_ADDR=sysadmin@mdsdefense.com' \ |
||
| 105 | -e '/^Environment=MAIL_FROM_NAME=/a Environment=MAIL_REPLYTO_NAME=Snipe-IT' \ |
||
| 106 | quadlet/site-snipeit.container |
||
| 107 | grep -n MAIL quadlet/site-snipeit.container # eight active MAIL_ lines (MAILER, HOST, PORT, ENCRYPTION, FROM x2, REPLYTO x2), no =log |
||
| 108 | ``` |
||
| 109 | |||
| 110 | Commit (root: `deploy-commit.sh -m "snipeit: From/Reply-To + SMTP via Exchange Online IP connector"`), |
||
| 111 | then go live — restarts only `site-snipeit` (a few seconds, other sites untouched): |
||
| 112 | |||
| 113 | ```bash |
||
| 114 | cp ~/deploy/quadlet/site-snipeit.container ~/.config/containers/systemd/ |
||
| 115 | systemctl --user daemon-reload && systemctl --user restart site-snipeit.service |
||
| 116 | podman exec site-snipeit printenv | grep ^MAIL_ |
||
| 117 | ``` |
||
| 118 | |||
| 119 | **Testing mail.** GUI: Admin Settings → **General Settings** → *Test Email* (not under |
||
| 120 | Notifications). Command line alternative, with the actual SMTP error printed (websvcmaster; |
||
| 121 | it only sends a single message, nothing is changed). Use an **`@mdsdefense.com` recipient**: |
||
| 122 | |||
| 123 | ```bash |
||
| 124 | podman exec site-snipeit php artisan tinker --execute='Mail::raw("Snipe-IT SMTP test", fn($m) => $m->to("sysadmin@mdsdefense.com")->subject("Snipe-IT test")); echo "sent\n";' |
||
| 125 | ``` |
||
| 126 | |||
| 127 | `sent` plus the mail in the mailbox = relay works. Another end-to-end check: log out → |
||
| 128 | *Forgot password*; that form asks for the **username**, not the e-mail address, and shows |
||
| 129 | "sent" even for an unknown username, so a wrong entry silently sends nothing. If the TLS handshake fails, set |
||
| 130 | `MAIL_ENCRYPTION=` (empty) and restart. |
||
| 131 | |||
| 132 | **Result 2026-10-26:** mail to an `@mdsdefense.com` mailbox is delivered (Redmine, which uses the |
||
| 133 | same connector, delivers too). The same test to an external address (gmail.com) is refused by |
||
| 134 | Exchange Online: `451 4.4.4 Mail received as unauthenticated, incoming to a recipient domain |
||
| 135 | configured in a hosted tenant which has no mail-enabled subscriptions. ATTR5`. TLS and |
||
| 136 | From/Reply-To are fine and the connector does recognise the host's IP (internal mail passes); |
||
| 137 | the refusal is on Exchange's side for non-tenant recipients. Cause not yet established — check |
||
| 138 | the connector in the Exchange admin center (type "Your organization's email server", status |
||
| 139 | 1 | Redmine Admin | On, and whether relay to external recipients is permitted) and use *Mail flow → Message trace* |
| 140 | 2 | Redmine Admin | for the failing attempt. Until then Snipe-IT can notify `@mdsdefense.com` addresses only. |
| 141 | |||
| 142 | 4 | Redmine Admin | ## 4.3 Image versions and upgrades |
| 143 | 2 | Redmine Admin | |
| 144 | **Pinned state (2026-10-26, commit `4a9a464`):** `site-snipeit` runs |
||
| 145 | `docker.io/snipe/snipe-it:v8.8.3` (build 26143-g80b2dc5768, same build that `latest` was at install); |
||
| 146 | `db-snipeit` runs MariaDB 11.8.9 pinned by digest (`mariadb@sha256:2439dcd7…`, the image that was |
||
| 147 | running — the `11.8` tag had already moved to a different image). `snipeit-install.sh` v1.1 still |
||
| 148 | defaults to `:latest`; for a new install pass `--app-image docker.io/snipe/snipe-it:v8.8.3`. |
||
| 149 | |||
| 150 | Why pinned: `latest` does not update by itself (podman does not re-pull an image that exists |
||
| 151 | locally), but it silently moves whenever the image is pulled or the container is recreated, and |
||
| 152 | Snipe-IT runs database migrations at start — an unplanned version jump changes the schema. With a |
||
| 153 | pin, the version in use is in the unit file and in git, and an upgrade is a deliberate edit. |
||
| 154 | |||
| 155 | **There is no upgrade from the GUI.** Snipe-IT shows a "new version available" notice (it checks |
||
| 156 | GitHub), but the code lives in the image; the only way to upgrade is to replace the image. |
||
| 157 | Anything changed inside the container is lost when the image changes. |
||
| 158 | |||
| 159 | Upgrade procedure (websvcmaster unless stated): |
||
| 160 | |||
| 161 | 1. New release noticed (GUI notice, or the `snipe/snipe-it` tags on Docker Hub): note the tag, e.g. `v8.8.4`. Tags are `v<major>.<minor>.<patch>`; `-alpine` variants exist, the unit uses the non-alpine one. |
||
| 162 | 2. **Backup first:** DB dump, the `app-snipeit-data` volume, the `snipeit-app-key` secret (the migration at step 5 cannot be undone without the dump). |
||
| 163 | 3. Change one line in the repo copy: `Image=docker.io/snipe/snipe-it:v8.8.4`. |
||
| 164 | 4. Commit (root: `deploy-commit.sh -m "snipeit: upgrade to v8.8.4"`), then `cp` the unit to `~/.config/containers/systemd/`, `systemctl --user daemon-reload`, `systemctl --user restart site-snipeit.service`. Only Snipe-IT is down for a few seconds. |
||
| 165 | 5. Migrations run on start: `podman logs -f site-snipeit`, then `~/deploy/check-sites.sh` (expect 302 for assets), then log in and check Admin → System information for the version. |
||
| 166 | 6. If it breaks: put the old `Image=` line back, restart; if the schema already changed, restore the DB dump. |
||
| 167 | |||
| 168 | MariaDB patch updates are the same idea: change the digest (or the `11.8` tag) on purpose, with a dump |
||
| 169 | taken first. Rules for unit files: a comment must be on its own line — `Image=… # note` makes the |
||
| 170 | comment part of the image name. Read the running digest with |
||
| 171 | `podman image inspect <image-id> --format '{{.Digest}}'`. |
||
| 172 | |||
| 173 | --- |
||
| 174 | 3 | Redmine Admin | _Source: `dmz-web-podman-cheatsheet.md` v1.5 in the `dmz-web-deploy` repo ([[Releases]]). Edit the wiki and the repo copy together._ |