Assign printers to a MACHINE and let whichever PC controls it install them, with the observed half kept strictly apart from the assigned half: a collector report can never write an assignment row. Adoption is explicit. Adds a singular relationship type so defaultprinter can mean exactly one, client scripts that converge a bay's queues and correct a drifted one in place, and a wave gate so a fleet-wide driver rollout cannot stampede its own share. On the container side: the compose stack never persisted the instance directory, so a rebuild discarded plugins.json and every upload. It is a volume now, the image ships instance/ owned by the app user so the mountpoint is writable, and the database moves to MySQL 8.4 LTS, which the Windows runbook has recommended since 8.0 went end of life. The image also could not build at all: the frontend stage never copied the plugin staging script it runs. Adds a topology migration guide covering IIS to Docker and back.
106 lines
4.1 KiB
YAML
106 lines
4.1 KiB
YAML
# shopdb-flask AIR-GAPPED single-site stack.
|
|
#
|
|
# For a site with NO internet. Nothing is built or pulled here: the images are
|
|
# built on a connected box (scripts/build-offline-bundle.ps1), shipped as a
|
|
# tarball, and `docker load`ed at the site. This file only RUNS pre-loaded
|
|
# images. See docs/DEPLOY-AIRGAP.md for the full runbook.
|
|
#
|
|
# Differences from docker-compose.yml (the connected/build template):
|
|
# - api uses `image:` (a loaded image), never `build: .` (build needs the net).
|
|
# - 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 manual
|
|
# `docker compose exec ... flask db upgrade` to forget).
|
|
#
|
|
# Usage at the site:
|
|
# docker load < shopdb-stack-<version>.tar.gz
|
|
# cp .env.example .env # then edit: secrets, CORS_ORIGINS, IMAGE_TAG
|
|
# docker compose -f docker-compose.airgap.yml up -d
|
|
# docker compose -f docker-compose.airgap.yml exec api flask seed admin <user> <email> <password>
|
|
|
|
# Shared application environment, reused by the migrate one-shot and the api
|
|
# service so the two never drift. A YAML anchor, not a container.
|
|
x-app-env: &app-env
|
|
FLASK_APP: wsgi.py
|
|
FLASK_ENV: production
|
|
DATABASE_URL: mysql+pymysql://shopdb:${MYSQL_PASSWORD}@db:3306/shopdb_flask?charset=utf8mb4
|
|
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY must be set}
|
|
JWT_SECRET_KEY: ${JWT_SECRET_KEY:?JWT_SECRET_KEY must be set}
|
|
CORS_ORIGINS: ${CORS_ORIGINS:?CORS_ORIGINS must be set}
|
|
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
|
ZABBIX_URL: ${ZABBIX_URL:-}
|
|
ZABBIX_TOKEN: ${ZABBIX_TOKEN:-}
|
|
|
|
services:
|
|
db:
|
|
image: mysql:8.4
|
|
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
|
restart: unless-stopped
|
|
environment:
|
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD must be set}
|
|
MYSQL_DATABASE: shopdb_flask
|
|
MYSQL_USER: shopdb
|
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?MYSQL_PASSWORD must be set}
|
|
volumes:
|
|
- db_data:/var/lib/mysql
|
|
ports:
|
|
- "127.0.0.1:${MYSQL_PORT:-3306}:3306"
|
|
healthcheck:
|
|
# 127.0.0.1, not localhost: localhost means the unix socket, and the
|
|
# entrypoint's init pass answers on the socket while running the server
|
|
# with --skip-networking. A socket ping therefore reports healthy DURING
|
|
# init, and the api/migrate services start against a server that is about
|
|
# to restart. Over TCP the probe stays red until the real server listens.
|
|
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
# One-shot schema + seed. Runs to completion and exits; api waits for it.
|
|
# Every step is idempotent, so it is safe to run on every `up`.
|
|
migrate:
|
|
image: shopdb-flask:${IMAGE_TAG:-0.7.0}
|
|
restart: "no"
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
environment:
|
|
<<: *app-env
|
|
command:
|
|
- sh
|
|
- -c
|
|
- >
|
|
flask db upgrade &&
|
|
flask plugin upgrade-all &&
|
|
flask seed permissions &&
|
|
flask seed settings &&
|
|
flask seed reference-data
|
|
# plugin upgrade-all rewrites plugins.json, so migrate needs the same
|
|
# instance volume api uses. Without it the enabled-plugin list is written
|
|
# into a container that exits immediately afterwards.
|
|
volumes:
|
|
- instance_data:/app/instance
|
|
|
|
api:
|
|
image: shopdb-flask:${IMAGE_TAG:-0.7.0}
|
|
restart: unless-stopped
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
migrate:
|
|
condition: service_completed_successfully
|
|
environment:
|
|
<<: *app-env
|
|
ports:
|
|
- "${API_PORT:-5001}:5001"
|
|
# See docker-compose.yml for what lives here. Same reasoning: /app/instance
|
|
# is written state and does not survive a container recreate on its own.
|
|
volumes:
|
|
- instance_data:/app/instance
|
|
|
|
volumes:
|
|
db_data:
|
|
instance_data:
|