main was 16 days and 259 commits behind the line the product is actually built on, at contract 0.30.0 against 0.35.0, with no per-PC credential code at all. It was neither current nor curated - just stale. This brings it forward and, at the same time, makes it mean something: main is what a receiving site installs, and nothing else. WHAT IT CARRIES. Twelve plugins - backups, computers, endpoints, guides, knowledgebase, machines, network, notifications, printers, slides, tools, warranty - plus applications, which is manifest-less core and always ships. The only manifest dependency inside that set is endpoints on computers, and both are in it, so the closure is self-contained. WHAT IT DOES NOT. employees, geenforce, measuringtools, printedparts and usb stay on the site line. So do the Windows installer tree, the VM rehearsal helpers, the agent tooling config, one site's data loader, the internal audit and findings records, and the planner spreadsheet. 1,135 tracked files against 1,438. A REMOVAL THAT BREAKS THE APP IS NOT A REMOVAL, and two nearly shipped: deploy/site-profile-universal.json still named all seventeen plugins. It is the file docs/DEPLOY.md tells a site to provision from, so a fresh install would have failed at `flask plugin apply-profile` with PluginError - the first thing a receiving site does, broken by the branch meant to serve them. It now names the twelve that exist, and its comment says the two must agree. agent/ was never refreshed. Whoever populated this branch copied shopdb/, plugins/, frontend/ and tests/ from the site line and left the agent tree at its September state: 22 C# and PowerShell sources missing, including AgentCredentials, EnrollmentService, AgentHttpFactory and the fleet-alert client, plus one file the site line had deleted still present. The old tree is internally consistent, so it compiles, and nothing in CI builds it - every gate was green over a server that speaks enrollment and per-PC credentials to a bundled agent source that cannot. Resynced whole. DECISIONS THIS ENCODES, rather than defers: - The plugin-authoring lab builds printedparts, whose manifest depends on employees, so it could not stay. The lab remains on the site line and main ships no build-along tutorial. That is a real loss for an adopting developer and is recorded here rather than discovered later. - Recognition, Training and Recertification notifications are no longer seeded. Each splits one notification into a card per employee and renders a photo, which needs a plugin this branch does not bundle. Seeding them would offer an author a type that cannot resolve anybody. A site that installs employees gets them from that plugin's own seed. - Windows/IIS is documented as the legacy path throughout, and the README leads with Docker, because that is what a receiving site gets. The API description is regenerated for THIS branch - 347 paths and 483 operations, down from 415 and 573 - so the published contract describes what is here rather than what the site line serves. docs/CONFIG.md lost the two measuring-tool label keys the code no longer declares, which two lanes had decided opposite ways. Suite: 3124 passed, 6 skipped. Naming and style gates pass. The docs gates run rather than skip, and every index line count is derived from the file it names.
7.3 KiB
Air-gapped Docker deploy
Topology: Docker, at a site with no internet at all. The connected equivalent is DEPLOY. Where this sits: install -> operate -> upgrade. Later versions: Upgrading to a new version on this page, not UPGRADE, which assumes a checkout to pull.
The site cannot pip install, npm ci, or docker pull, so nothing is built
or pulled there. You build a fully self-contained image on a connected box,
ship it as one tarball, and docker load + run it at the site.
docker-compose.airgap.yml is the counterpart to docker-compose.yml and
differs in four ways that matter:
image:notbuild: .- the site runs a loaded image; it never builds.- No
./pluginsbind mount - the image already carries every plugin baked in. Binding a host./plugins(which does not exist at an image-only site) would mask the baked plugins with an empty dir and load zero plugins. - A one-shot
migrateservice runsdb upgrade+plugin upgrade-all+ seed beforeapistarts, soup -dalone brings up a working site. - No
./certsmount and no TLS flags -apiruns the image's own gunicorn command and serves plain http on 5001. Put TLS in front of it, or in the container the waydocker-compose.ymldoes.
A ready-made package exists too. scripts/build-channel-installer.ps1
wraps this same tarball with docker-compose.airgap.yml, a pre-filled .env
and an Install-ShopDB.ps1, for an operator who has no repository at all. Read
the script's own header for what it expects on the build box.
1. Build the bundle (connected box)
On a box with clean access to Docker Hub + PyPI + npm, from the repo root:
pwsh scripts/build-offline-bundle.ps1 -Version <version>
This builds shopdb-flask:<version> (frontend + all Python deps baked in), pulls
mysql:8.4, and writes:
shopdb-stack-<version>.tar.gz- both images in one archiveshopdb-stack-<version>.tar.gz.sha256- checksum to verify after transfer
The image is self-contained: no pip/npm/registry access is needed at the site.
Building behind an inspecting proxy
If the build box is itself behind an inspecting corporate proxy, the in-build
pip/npm fails with CERTIFICATE_VERIFY_FAILED: the build container has its
own certificate store, and the host's PIP_CERT / NODE_EXTRA_CA_CERTS do NOT
carry into a docker build. Easiest fix is to build from a box with clean
internet. Otherwise the corporate root CA must be trusted inside the build,
which is a Dockerfile change and is not wired today - see
DEVELOPMENT-SETUP section 0b for the all-roots PEM
bundle.
2. Transfer + verify
Carry shopdb-stack-<version>.tar.gz (+ the .sha256), plus
docker-compose.airgap.yml and .env.example, to the site on approved media.
Verify the archive survived the trip:
# PowerShell
(Get-FileHash shopdb-stack-<version>.tar.gz -Algorithm SHA256).Hash.ToLower()
# compare against the .sha256 file
# Linux site
sha256sum -c shopdb-stack-<version>.tar.gz.sha256
3. Load + run (air-gapped site)
# 1) Load both images into the local Docker.
docker load -i shopdb-stack-<version>.tar.gz
docker image ls | grep -E 'shopdb-flask|mysql' # confirm both present
# 2) Configure the site.
cp .env.example .env
# Edit .env - REQUIRED:
# IMAGE_TAG=<version> # MUST match the loaded image tag
# MYSQL_ROOT_PASSWORD=... # strong, unique
# MYSQL_PASSWORD=... # strong, unique (the app's db user)
# SECRET_KEY=... # 32+ random bytes
# JWT_SECRET_KEY=... # 32+ random bytes, different from SECRET_KEY
# CORS_ORIGINS=https://shopdb.site.example # the site's browser origin(s)
# Optional: API_PORT (default 5001), LOG_LEVEL, ZABBIX_URL/ZABBIX_TOKEN.
# 3) Bring it up. The migrate one-shot runs the schema + seed, then api starts.
docker compose -f docker-compose.airgap.yml up -d
# 4) Watch the one-shot finish (it exits 0 when the schema + seed are done).
docker compose -f docker-compose.airgap.yml logs -f migrate
IMAGE_TAG in .env must equal the loaded tag; otherwise compose looks for an
image that was never loaded and api/migrate will not start.
docker-compose.airgap.yml falls back to 0.7.0 when IMAGE_TAG is unset,
which is almost certainly not the version you just built - so set it, rather
than relying on the default.
4. Create the first admin
Seeding creates permissions, settings, and reference data, but not a login. Make one admin (choose the password; it is not automatable):
docker compose -f docker-compose.airgap.yml exec api \
flask seed admin --username <username> --email <email> --password <password>
# Omit --password to have a strong one generated and printed once.
5. Verify
docker compose -f docker-compose.airgap.yml ps # db + api "running", migrate "exited (0)"
docker compose -f docker-compose.airgap.yml logs api # gunicorn started, no tracebacks
curl -sf http://localhost:${API_PORT:-5001}/api/dashboard/health # or browse the site origin
Then log in at the site origin with the admin created in step 4.
Upgrading to a new version
Build a new bundle on the connected box (-Version <newversion>), transfer, then at the
site:
docker load -i shopdb-stack-<newversion>.tar.gz
# set IMAGE_TAG=<newversion> in .env
docker compose -f docker-compose.airgap.yml up -d
The migrate one-shot re-runs db upgrade + plugin upgrade-all + seed (all
idempotent) against the existing data before the new api starts. Back up first
(BACKUP-RESTORE); the db_data volume persists across
upgrades.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
service api is not running / manifest ... not found |
The image was not loaded, or IMAGE_TAG in .env does not match a loaded image. docker image ls, fix IMAGE_TAG. Also: never use docker-compose.yml here - its build: . needs internet. |
| Site loads but no plugins / empty nav | You used the wrong compose file. docker-compose.yml bind-mounts ./plugins (absent here) over the baked plugins. Use docker-compose.airgap.yml. |
api never starts, migrate shows an error |
Read logs migrate. A DB-connection error means db is not healthy yet (logs db) or MYSQL_PASSWORD in .env differs from what the db volume was first initialised with. A fresh site with a stale db_data volume needs the volume removed (docker compose ... down -v - DESTROYS data). |
SECRET_KEY must be set (and similar) on up |
A required .env var is empty. Fill every REQUIRED key in step 3. |
Build fails on the connected box at pip/npm |
Proxy certificate - see "Building behind an inspecting proxy" above. |
See also
- DEPLOY - the connected stack, the environment variables, and the reverse-proxy and client-address reference
- BACKUP-RESTORE - back up before every image swap
- CONFIG - what each
.envvalue means