INSTALL-WINDOWS-IIS.md has said MySQL 8.4 LTS is standard for new installs since 8.0 reached end of life in April 2026, while both compose files and the offline bundler still pinned 8.0. A site reading the Windows runbook and a site reading the Docker one were being told to run different servers, and the migration page written this week sent people onto the dead one. Verified against a real server rather than by editing a tag: 8.4.11, core chain plus five plugin chains applied clean, 66 tables at a single utf8mb4_unicode_ci collation, six alembic version tables. The image's PyMySQL authenticates against 8.4's caching_sha2_password, which is what requirements.in already pins cryptography for. Existing servers need one thing done FIRST: 8.4 removes mysql_native_password, so an account created on 5.6 or 5.7 must be moved to caching_sha2_password before the upgrade or it cannot authenticate afterwards. In-place also has no downgrade path, and 5.7 cannot reach 8.4 in one hop. For databases this size a dump into a fresh 8.4 server is the better trade: same outage, and the old server stays as the rollback. Air-gapped sites need a fresh offline bundle, because the tarball carries the MySQL image alongside the app image. Also here, found by having it bite during that verification: the db healthcheck pinged over the unix socket, and the entrypoint's init pass answers on the socket while running the server with --skip-networking. The probe therefore reported healthy DURING init, which is what `depends_on: service_healthy` gates api and migrate on. A ping passed at 8 seconds and the next query failed because the server was mid-restart. Probing 127.0.0.1 keeps it red until the real server is listening.
150 lines
5.9 KiB
Markdown
150 lines
5.9 KiB
Markdown
# Air-gapped Docker deploy
|
|
|
|
For a site with **no internet**. 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.
|
|
|
|
This is the counterpart to `docker-compose.yml` (the connected/build template).
|
|
The air-gapped stack uses `docker-compose.airgap.yml`, which differs in three
|
|
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.
|
|
|
|
---
|
|
|
|
## 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 0.7.0
|
|
```
|
|
|
|
This builds `shopdb-flask:0.7.0` (frontend + all Python deps baked in), pulls
|
|
`mysql:8.4`, and writes:
|
|
|
|
- `shopdb-stack-0.7.0.tar.gz` - both images in one archive
|
|
- `shopdb-stack-0.7.0.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 Zscaler
|
|
|
|
If the build box is itself behind GE Zscaler, the in-build `pip`/`npm` may fail
|
|
with `CERTIFICATE_VERIFY_FAILED` - the build container has its own cert store
|
|
and does not trust GE's re-signing root (host `PIP_CERT` / `NODE_EXTRA_CA_CERTS`
|
|
do NOT carry into a `docker build`). Easiest: build from a box with clean
|
|
internet (home, cloud, or CI). Otherwise the corp root CA must be trusted
|
|
**inside** the build (a Dockerfile change to accept the CA - not wired today;
|
|
see docs/DEVELOPMENT-SETUP.md section 0b for the all-roots PEM bundle).
|
|
|
|
---
|
|
|
|
## 2. Transfer + verify
|
|
|
|
Carry `shopdb-stack-0.7.0.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-0.7.0.tar.gz -Algorithm SHA256).Hash.ToLower()
|
|
# compare against the .sha256 file
|
|
```
|
|
|
|
```bash
|
|
# Linux site
|
|
sha256sum -c shopdb-stack-0.7.0.tar.gz.sha256
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Load + run (air-gapped site)
|
|
|
|
```bash
|
|
# 1) Load both images into the local Docker.
|
|
docker load -i shopdb-stack-0.7.0.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=0.7.0 # 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 (`0.7.0` here); otherwise compose
|
|
looks for an image that was never loaded and `api`/`migrate` will not start.
|
|
|
|
---
|
|
|
|
## 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 0.8.0`), transfer, then at the
|
|
site:
|
|
|
|
```bash
|
|
docker load -i shopdb-stack-0.8.0.tar.gz
|
|
# set IMAGE_TAG=0.8.0 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
|
|
(see docs/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` | Zscaler cert - see "Building behind Zscaler" above. |
|