# 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-.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 # 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.0 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: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-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 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" volumes: db_data: