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
This commit is contained in:
Proudlock
2026-09-07 00:26:48 -04:00
parent 128bdabcb4
commit 67579f13aa
5 changed files with 305 additions and 0 deletions

22
.dockerignore.prebuilt Normal file
View File

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

View File

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

47
Dockerfile.prebuilt Normal file
View File

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

View File

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

75
start-docker-quick.sh Normal file
View File

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