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.
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:
|