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 ""