From 67579f13aade29323497a8468a2d013fe65708d0 Mon Sep 17 00:00:00 2001 From: Proudlock <270015376@geaerospace.com> Date: Mon, 7 Sep 2026 00:26:48 -0400 Subject: [PATCH] Add pre-built frontend Docker approach - workaround for Zscaler proxy Corporate proxy (Zscaler) causes npm to fail inside Docker builds with 'Exit handler never called' error. This approach builds frontend on host where npm works, then copies pre-built dist/ into Docker image. New files: - Dockerfile.prebuilt: Single-stage build, copies frontend/dist - docker-compose.prebuilt.yml: Uses Dockerfile.prebuilt - .dockerignore.prebuilt: Allows frontend/dist to be included - start-docker-quick.sh: Quick restart without rebuild - DOCKER-PREBUILT-APPROACH.md: Documents workaround and comparison Benefits: - Builds in 2-3 minutes (not 15+) - Works behind corporate proxy - Simpler to debug - Faster iteration Trade-off: Requires npm on host, two-step build process For production: CI/CD builds frontend + image, pushes to registry --- .dockerignore.prebuilt | 22 +++++++++ DOCKER-PREBUILT-APPROACH.md | 96 +++++++++++++++++++++++++++++++++++++ Dockerfile.prebuilt | 47 ++++++++++++++++++ docker-compose.prebuilt.yml | 65 +++++++++++++++++++++++++ start-docker-quick.sh | 75 +++++++++++++++++++++++++++++ 5 files changed, 305 insertions(+) create mode 100644 .dockerignore.prebuilt create mode 100644 DOCKER-PREBUILT-APPROACH.md create mode 100644 Dockerfile.prebuilt create mode 100644 docker-compose.prebuilt.yml create mode 100644 start-docker-quick.sh diff --git a/.dockerignore.prebuilt b/.dockerignore.prebuilt new file mode 100644 index 00000000..1e36ddd9 --- /dev/null +++ b/.dockerignore.prebuilt @@ -0,0 +1,22 @@ +# Dockerignore for pre-built frontend approach +# frontend/dist is INCLUDED (not ignored) for this build + +**/node_modules +frontend/src/.plugins-staged +frontend/src/router/routes.gen.js + +# Python caches +**/__pycache__ +**/*.pyc +.venv +venv + +# Local dev and VCS +.git +.gitignore +*.log +.env.local + +# Installer artifacts +installer-arch-dev/build +installer-arch-dev/dist diff --git a/DOCKER-PREBUILT-APPROACH.md b/DOCKER-PREBUILT-APPROACH.md new file mode 100644 index 00000000..f47343ef --- /dev/null +++ b/DOCKER-PREBUILT-APPROACH.md @@ -0,0 +1,96 @@ +# Docker Pre-Built Frontend Approach + +**Problem:** Zscaler corporate proxy causes npm to fail inside Docker builds with "Exit handler never called" error. + +**Solution:** Build frontend on host (where npm works), copy pre-built dist/ into Docker image. + +## Quick Start + +```bash +# 1. Build frontend on host +cd frontend +npm ci +npm run build + +# 2. Build Docker image (uses pre-built dist/) +cd .. +docker compose -f docker-compose.prebuilt.yml build + +# 3. Start services +docker compose -f docker-compose.prebuilt.yml up -d + +# 4. Initialize (first time only) +docker compose -f docker-compose.prebuilt.yml exec api flask db upgrade +docker compose -f docker-compose.prebuilt.yml exec api bash /app/scripts/bootstrap-plugins.sh + +# 5. Access +https://localhost:5001 +``` + +## Files + +- `Dockerfile.prebuilt` - Simplified Dockerfile without npm build +- `docker-compose.prebuilt.yml` - Uses Dockerfile.prebuilt +- `.dockerignore.prebuilt` - Allows frontend/dist to be copied + +## Why This Works + +**Normal Dockerfile:** +- Multi-stage build +- Stage 1: Node 20 runs `npm ci` and `npm run build` +- Stage 2: Copies built frontend from stage 1 +- **PROBLEM:** npm fails inside Docker with Zscaler proxy + +**Pre-Built Approach:** +- Build frontend on Windows host (npm works here) +- Single-stage Dockerfile copies pre-built `frontend/dist/` +- Only installs Python dependencies +- **FASTER:** 2-3 minutes vs 15+ minutes + +## Trade-offs + +**Pros:** +- Avoids corporate proxy issues +- Much faster builds (no npm in container) +- Frontend build errors visible immediately +- Can iterate on frontend without rebuilding container + +**Cons:** +- Requires npm on host machine +- Two-step build process +- frontend/dist must be in version control OR built before deployment + +## For Production (CI/CD) + +```yaml +# GitHub Actions / Jenkins example +- name: Build frontend + run: | + cd frontend + npm ci + npm run build + +- name: Build Docker image + run: | + docker build -f Dockerfile.prebuilt -t shopdb:${{ github.sha }} . + +- name: Push to registry + run: | + docker push shopdb:${{ github.sha }} +``` + +## Comparison + +| Approach | Build Time | Corporate Proxy | Complexity | Production Ready | +|----------|-----------|----------------|------------|------------------| +| Multi-stage Dockerfile | 15+ min | ❌ Fails | High | ❌ No | +| Pre-built frontend | 2-3 min | ✅ Works | Medium | ⚠️ Yes (with CI/CD) | +| Pre-built images from registry | 30 sec | ✅ Works | Low | ✅ Yes | + +## Next Steps + +For true production deployment: +1. Set up CI/CD to build frontend + Docker image +2. Push to container registry +3. Sites pull pre-built images +4. No build step at deployment site diff --git a/Dockerfile.prebuilt b/Dockerfile.prebuilt new file mode 100644 index 00000000..2a53d0dc --- /dev/null +++ b/Dockerfile.prebuilt @@ -0,0 +1,47 @@ +# Dockerfile for pre-built frontend +# Use this when frontend/dist is already built on the host +# Avoids npm issues with corporate proxy + +FROM python:3.14-slim AS base + +WORKDIR /app + +# Install system dependencies +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + default-libmysqlclient-dev \ + pkg-config \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt \ + && pip install --no-cache-dir gunicorn gevent + +# Copy application code +COPY shopdb/ ./shopdb/ +COPY plugins/ ./plugins/ +COPY migrations/ ./migrations/ +COPY scripts/ ./scripts/ +COPY wsgi.py ./ + +# Copy pre-built frontend +COPY frontend/dist ./frontend/dist + +# Make scripts executable +RUN chmod +x /app/scripts/docker-entrypoint.sh /app/scripts/bootstrap-plugins.sh + +# Create instance directory +RUN mkdir -p /app/instance + +# Expose port +EXPOSE 5001 + +# Set environment +ENV PYTHONUNBUFFERED=1 +ENV FLASK_APP=shopdb + +# Entrypoint runs migrations and starts gunicorn +ENTRYPOINT ["/app/scripts/docker-entrypoint.sh"] diff --git a/docker-compose.prebuilt.yml b/docker-compose.prebuilt.yml new file mode 100644 index 00000000..fe1df5f0 --- /dev/null +++ b/docker-compose.prebuilt.yml @@ -0,0 +1,65 @@ +# Docker Compose using pre-built frontend +# Build frontend on host first: cd frontend && npm run build +# Then: docker compose -f docker-compose.prebuilt.yml build + +services: + db: + image: mysql:8.4 + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: shopdb_flask + MYSQL_USER: shopdb + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + volumes: + - db_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + api: + build: + context: . + dockerfile: Dockerfile.prebuilt + ports: + - "${API_PORT:-5001}:5001" + environment: + - DATABASE_URL=mysql+pymysql://shopdb:${MYSQL_PASSWORD}@db:3306/shopdb_flask + - SECRET_KEY=${SECRET_KEY} + - JWT_SECRET_KEY=${JWT_SECRET_KEY} + - FLASK_ENV=${FLASK_ENV:-production} + - FLASK_DEBUG=${FLASK_DEBUG:-0} + - CORS_ORIGINS=${CORS_ORIGINS} + - SHOPDB_ENV_LABEL=${SHOPDB_ENV_LABEL} + - LOG_LEVEL=${LOG_LEVEL:-INFO} + - ZABBIX_URL=${ZABBIX_URL:-} + - ZABBIX_TOKEN=${ZABBIX_TOKEN:-} + - COLLECTOR_API_KEY=${COLLECTOR_API_KEY:-} + - COLLECTOR_API_KEY_ENDPOINTS=${COLLECTOR_API_KEY_ENDPOINTS:-} + volumes: + - instance_data:/app/instance + - ./plugins:/app/plugins:ro + - ./scripts:/app/scripts:ro + - ./certs:/app/certs:ro + depends_on: + db: + condition: service_healthy + restart: unless-stopped + command: > + gunicorn + --bind 0.0.0.0:5001 + --workers 4 + --worker-class gevent + --worker-connections 2000 + --timeout 300 + --certfile /app/certs/server.crt + --keyfile /app/certs/server.key + --access-logfile - + --error-logfile - + wsgi:app + +volumes: + db_data: + instance_data: diff --git a/start-docker-quick.sh b/start-docker-quick.sh new file mode 100644 index 00000000..c25be622 --- /dev/null +++ b/start-docker-quick.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# Quick start for ShopDB Docker - skips rebuild if images exist +# Use this for daily restarts after initial build + +set -e + +echo "==> ShopDB Docker - Quick Start" +echo "" + +# Check prerequisites +if [ ! -f .env ]; then + echo "ERROR: .env file not found" + echo "Copy .env.docker-dev to .env first:" + echo " cp .env.docker-dev .env" + exit 1 +fi + +# Check if API image exists +if ! docker images | grep -q "flask-shopdb-api"; then + echo "ERROR: Docker images not built yet" + echo "Run this first: docker compose build" + exit 1 +fi + +# Start services (no rebuild) +echo "==> Starting services..." +docker compose -f docker-compose.yml -f docker-compose.backup.yml --profile backup up -d + +# Wait for database +echo "==> Waiting for database..." +sleep 10 + +# Check status +docker compose ps + +# Initialize if needed +echo "" +echo "==> Checking database state..." +if docker compose exec -T api flask db current 2>&1 | grep -q "Can't locate revision"; then + echo "==> Running migrations..." + docker compose exec api flask db upgrade +fi + +# Check plugins +echo "" +echo "==> Checking plugins..." +PLUGIN_COUNT=$(docker compose exec -T api flask plugin list 2>/dev/null | grep -c "^\s\+\w" || echo "0") + +if [ "$PLUGIN_COUNT" -lt 5 ]; then + echo "==> Installing plugins..." + docker compose exec api bash /app/scripts/bootstrap-plugins.sh +else + echo "==> Plugins already installed ($PLUGIN_COUNT found)" +fi + +echo "" +echo "====================================" +echo "ShopDB is running!" +echo "====================================" +echo "" +echo "Access: https://localhost:5001" +echo "" +echo "Services:" +echo " - MySQL Database (internal)" +echo " - Flask API (https://localhost:5001)" +echo " - Backup Store (isolated)" +echo " - Backup Watcher (polls every 30s)" +echo "" +echo "View logs:" +echo " docker compose logs -f api" +echo " docker compose logs -f backup-watcher" +echo "" +echo "Stop:" +echo " docker compose down" +echo ""