docker: air-gapped deploy kit (image bundle + offline compose + runbook)
Air-gapped sites cannot pip install / npm ci / docker pull, so a build-at-site compose (build: .) fails and reports 'service api is not running'. Add a build-once-ship-image path: - scripts/build-offline-bundle.ps1: on a connected box, build shopdb-flask + pull mysql:8.0, docker save both into one gzipped tarball with a sha256. - docker-compose.airgap.yml: runs pre-loaded images (image:, never build:), drops the ./plugins bind mount (which would mask the image's baked-in plugins with an empty host dir and load zero plugins at an image-only site), and adds a one-shot migrate service (db upgrade + plugin upgrade-all + seed) that api waits on via service_completed_successfully, so 'up -d' brings a working site. - docs/DEPLOY-AIRGAP.md: full runbook (build, transfer+verify, load+run, admin, verify, upgrade, troubleshooting), incl the Zscaler in-build cert caveat. - .env.example: IMAGE_TAG for the air-gap compose to pin the loaded image tag.
This commit is contained in:
90
docker-compose.airgap.yml
Normal file
90
docker-compose.airgap.yml
Normal file
@@ -0,0 +1,90 @@
|
||||
# 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.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:
|
||||
Reference in New Issue
Block a user