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.
181 lines
7.3 KiB
Markdown
181 lines
7.3 KiB
Markdown
<!-- docmeta
|
|
purpose: howto
|
|
audience: adopter, developer
|
|
status: current
|
|
summary: Build the stack on a connected box, carry one tarball in, and run a Docker site with no internet at all.
|
|
loadwith: none
|
|
-->
|
|
# Air-gapped Docker deploy
|
|
|
|
> **Topology: Docker, at a site with no internet at all.** The connected
|
|
> equivalent is [DEPLOY](DEPLOY.md).
|
|
> **Where this sits:** **install** -> operate -> upgrade.
|
|
> Later versions: [Upgrading to a new version](#upgrading-to-a-new-version) on
|
|
> this page, not [UPGRADE](UPGRADE.md), 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:` not `build: .`** - the site runs a loaded image; it never builds.
|
|
- **No `./plugins` bind 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 `migrate` service** runs `db upgrade` + `plugin upgrade-all` +
|
|
seed before `api` starts, so `up -d` alone brings up a working site.
|
|
- **No `./certs` mount and no TLS flags** - `api` runs the image's own gunicorn
|
|
command and serves plain http on 5001. Put TLS in front of it, or in the
|
|
container the way `docker-compose.yml` does.
|
|
|
|
**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:
|
|
|
|
```powershell
|
|
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 archive
|
|
- `shopdb-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](DEVELOPMENT-SETUP.md) 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
|
|
# PowerShell
|
|
(Get-FileHash shopdb-stack-<version>.tar.gz -Algorithm SHA256).Hash.ToLower()
|
|
# compare against the .sha256 file
|
|
```
|
|
|
|
```bash
|
|
# Linux site
|
|
sha256sum -c shopdb-stack-<version>.tar.gz.sha256
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Load + run (air-gapped site)
|
|
|
|
```bash
|
|
# 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):
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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](BACKUP-RESTORE.md)); 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](DEPLOY.md) - the connected stack, the environment variables, and the
|
|
reverse-proxy and client-address reference
|
|
- [BACKUP-RESTORE](BACKUP-RESTORE.md) - back up before every image swap
|
|
- [CONFIG](CONFIG.md) - what each `.env` value means
|