diff --git a/docker/Dockerfile.unified b/docker/Dockerfile.unified new file mode 100644 index 000000000..0f1c31cf8 --- /dev/null +++ b/docker/Dockerfile.unified @@ -0,0 +1,141 @@ +# Unified Dockerfile - Frontend + Backend in single container +# Supports MODE parameter: BOTH (default), FRONTEND, BACKEND + +# Stage 1: Build Frontend +FROM node:20-alpine AS frontend-build + +WORKDIR /app + +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci + +COPY frontend . +RUN npm run build + +# Stage 2: Build Backend +FROM gradle:8.14-jdk21 AS backend-build + +COPY build.gradle . +COPY settings.gradle . +COPY gradlew . +COPY gradle gradle/ +COPY app/core/build.gradle core/. +COPY app/common/build.gradle common/. +COPY app/proprietary/build.gradle proprietary/. +RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0 + +WORKDIR /app +COPY . . + +RUN DISABLE_ADDITIONAL_FEATURES=false \ + STIRLING_PDF_DESKTOP_UI=false \ + ./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube + +# Stage 3: Final unified image +FROM alpine:3.22.1 + +ARG VERSION_TAG + +# Labels +LABEL org.opencontainers.image.title="Stirling-PDF Unified" +LABEL org.opencontainers.image.description="Unified container for Stirling-PDF - Frontend + Backend with MODE parameter" +LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF" +LABEL org.opencontainers.image.licenses="MIT" +LABEL org.opencontainers.image.vendor="Stirling-Tools" +LABEL org.opencontainers.image.url="https://www.stirlingpdf.com" +LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com" +LABEL maintainer="Stirling-Tools" +LABEL org.opencontainers.image.authors="Stirling-Tools" +LABEL org.opencontainers.image.version="${VERSION_TAG}" +LABEL org.opencontainers.image.keywords="PDF, manipulation, unified, API, Spring Boot, React" + +# Copy backend files +COPY scripts /scripts +COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ +COPY --from=backend-build /app/app/core/build/libs/*.jar app.jar + +# Copy frontend files +COPY --from=frontend-build /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY docker/unified/nginx.conf /etc/nginx/nginx.conf +COPY docker/unified/entrypoint.sh /entrypoint.sh + +# Environment Variables +ENV DISABLE_ADDITIONAL_FEATURES=false \ + VERSION_TAG=$VERSION_TAG \ + JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \ + JAVA_CUSTOM_OPTS="" \ + HOME=/home/stirlingpdfuser \ + PUID=1000 \ + PGID=1000 \ + UMASK=022 \ + PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \ + UNO_PATH=/usr/lib/libreoffice/program \ + URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \ + PATH=$PATH:/opt/venv/bin \ + STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \ + TMPDIR=/tmp/stirling-pdf \ + TEMP=/tmp/stirling-pdf \ + TMP=/tmp/stirling-pdf \ + MODE=BOTH \ + BACKEND_INTERNAL_PORT=8081 \ + VITE_API_BASE_URL=http://localhost:8080 + +# Install all dependencies +RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ + echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ + echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \ + apk upgrade --no-cache -a && \ + apk add --no-cache \ + ca-certificates \ + tzdata \ + tini \ + bash \ + curl \ + shadow \ + su-exec \ + openssl \ + openssl-dev \ + openjdk21-jre \ + nginx \ + # Doc conversion + gcompat \ + libc6-compat \ + libreoffice \ + # pdftohtml + poppler-utils \ + # OCR MY PDF + unpaper \ + tesseract-ocr-data-eng \ + tesseract-ocr-data-chi_sim \ + tesseract-ocr-data-deu \ + tesseract-ocr-data-fra \ + tesseract-ocr-data-por \ + ocrmypdf \ + # CV + py3-opencv \ + python3 \ + py3-pip \ + py3-pillow@testing \ + py3-pdf2image@testing && \ + python3 -m venv /opt/venv && \ + /opt/venv/bin/pip install --upgrade pip setuptools && \ + /opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \ + ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \ + ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \ + ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \ + mv /usr/share/tessdata /usr/share/tessdata-original && \ + mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf /pipeline/watchedFolders /pipeline/finishedFolders && \ + mkdir -p /var/lib/nginx/tmp /var/log/nginx && \ + fc-cache -f -v && \ + chmod +x /scripts/* && \ + chmod +x /entrypoint.sh && \ + # User permissions + addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ + chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf /var/lib/nginx /var/log/nginx /usr/share/nginx && \ + chown stirlingpdfuser:stirlingpdfgroup /app.jar + +EXPOSE 8080/tcp + +ENTRYPOINT ["tini", "--", "/entrypoint.sh"] diff --git a/docker/compose/docker-compose-unified-backend.yml b/docker/compose/docker-compose-unified-backend.yml new file mode 100644 index 000000000..b8ebfd42b --- /dev/null +++ b/docker/compose/docker-compose-unified-backend.yml @@ -0,0 +1,58 @@ +# Example Docker Compose for Unified Stirling-PDF Container +# MODE=BACKEND: Backend API only (no frontend) + +services: + stirling-pdf-backend-only: + container_name: Stirling-PDF-Backend-Only + build: + context: ../.. + dockerfile: docker/Dockerfile.unified + ports: + - "8080:8080" + volumes: + - ./stirling/data:/usr/share/tessdata:rw + - ./stirling/config:/configs:rw + - ./stirling/logs:/logs:rw + - ./stirling/customFiles:/customFiles:rw + - ./stirling/pipeline:/pipeline:rw + environment: + # MODE parameter: BACKEND only + MODE: BACKEND + + # Standard Stirling-PDF configuration + DISABLE_ADDITIONAL_FEATURES: "false" + DOCKER_ENABLE_SECURITY: "false" + PUID: 1000 + PGID: 1000 + UMASK: "022" + + # Application settings + SYSTEM_DEFAULTLOCALE: en-US + UI_APPNAME: Stirling-PDF + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + + # Optional: Add OCR languages (comma-separated) + # TESSERACT_LANGS: "deu,fra,spa" + + # Optional: Java memory settings + # JAVA_CUSTOM_OPTS: "-Xmx4g" + + restart: unless-stopped + + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + deploy: + resources: + limits: + memory: 4G + reservations: + memory: 2G + +# Access the API at: http://localhost:8080/api +# Swagger UI at: http://localhost:8080/swagger-ui/index.html diff --git a/docker/compose/docker-compose-unified-both.yml b/docker/compose/docker-compose-unified-both.yml new file mode 100644 index 000000000..92e08e4aa --- /dev/null +++ b/docker/compose/docker-compose-unified-both.yml @@ -0,0 +1,59 @@ +# Example Docker Compose for Unified Stirling-PDF Container +# MODE=BOTH (default): Frontend + Backend in single container on port 8080 + +services: + stirling-pdf-unified: + container_name: Stirling-PDF-Unified-Both + build: + context: ../.. + dockerfile: docker/Dockerfile.unified + ports: + - "8080:8080" + volumes: + - ./stirling/data:/usr/share/tessdata:rw + - ./stirling/config:/configs:rw + - ./stirling/logs:/logs:rw + - ./stirling/customFiles:/customFiles:rw + - ./stirling/pipeline:/pipeline:rw + environment: + # MODE parameter: BOTH (default), FRONTEND, or BACKEND + MODE: BOTH + + # Backend runs internally on this port when MODE=BOTH + BACKEND_INTERNAL_PORT: 8081 + + # Standard Stirling-PDF configuration + DISABLE_ADDITIONAL_FEATURES: "false" + DOCKER_ENABLE_SECURITY: "false" + PUID: 1000 + PGID: 1000 + UMASK: "022" + + # Application settings + SYSTEM_DEFAULTLOCALE: en-US + UI_APPNAME: Stirling-PDF + UI_HOMEDESCRIPTION: Your locally hosted one-stop-shop for all your PDF needs + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + + # Optional: Add OCR languages (comma-separated) + # TESSERACT_LANGS: "deu,fra,spa" + + # Optional: Java memory settings + # JAVA_CUSTOM_OPTS: "-Xmx4g" + + restart: unless-stopped + + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + deploy: + resources: + limits: + memory: 4G + reservations: + memory: 2G diff --git a/docker/compose/docker-compose-unified-frontend.yml b/docker/compose/docker-compose-unified-frontend.yml new file mode 100644 index 000000000..c7d217b34 --- /dev/null +++ b/docker/compose/docker-compose-unified-frontend.yml @@ -0,0 +1,63 @@ +# Example Docker Compose for Unified Stirling-PDF Container +# MODE=FRONTEND: Frontend only, connects to separate backend + +services: + stirling-pdf-backend: + container_name: Stirling-PDF-Backend + build: + context: ../.. + dockerfile: docker/Dockerfile.unified + ports: + - "8081:8080" + volumes: + - ./stirling/data:/usr/share/tessdata:rw + - ./stirling/config:/configs:rw + - ./stirling/logs:/logs:rw + - ./stirling/customFiles:/customFiles:rw + - ./stirling/pipeline:/pipeline:rw + environment: + MODE: BACKEND + DISABLE_ADDITIONAL_FEATURES: "false" + DOCKER_ENABLE_SECURITY: "false" + PUID: 1000 + PGID: 1000 + UMASK: "022" + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + deploy: + resources: + limits: + memory: 4G + + stirling-pdf-frontend: + container_name: Stirling-PDF-Frontend + build: + context: ../.. + dockerfile: docker/Dockerfile.unified + ports: + - "8080:8080" + environment: + MODE: FRONTEND + + # Point to the backend service + VITE_API_BASE_URL: http://stirling-pdf-backend:8080 + + # Minimal config needed for frontend + PUID: 1000 + PGID: 1000 + depends_on: + - stirling-pdf-backend + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/ || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + deploy: + resources: + limits: + memory: 512M diff --git a/docker/unified/README.md b/docker/unified/README.md new file mode 100644 index 000000000..6f0488aa2 --- /dev/null +++ b/docker/unified/README.md @@ -0,0 +1,458 @@ +# Stirling-PDF Unified Container + +Single Docker container that can run as **frontend + backend**, **frontend only**, or **backend only** using the `MODE` environment variable. + +## Quick Start + +### MODE=BOTH (Default) +Single container with both frontend and backend on port 8080: + +```bash +docker run -p 8080:8080 \ + -e MODE=BOTH \ + stirlingtools/stirling-pdf:unified +``` + +Access at: `http://localhost:8080` + +### MODE=FRONTEND +Frontend only, connecting to separate backend: + +```bash +docker run -p 8080:8080 \ + -e MODE=FRONTEND \ + -e VITE_API_BASE_URL=http://backend:8080 \ + stirlingtools/stirling-pdf:unified +``` + +### MODE=BACKEND +Backend API only: + +```bash +docker run -p 8080:8080 \ + -e MODE=BACKEND \ + stirlingtools/stirling-pdf:unified +``` + +Access API at: `http://localhost:8080/api` +Swagger UI at: `http://localhost:8080/swagger-ui/index.html` + +--- + +## Architecture + +### MODE=BOTH (Default) +``` +┌─────────────────────────────────────┐ +│ Port 8080 (External) │ +│ ┌───────────────────────────────┐ │ +│ │ Nginx │ │ +│ │ • Serves frontend (/) │ │ +│ │ • Proxies /api/* → backend │ │ +│ └───────────┬───────────────────┘ │ +│ │ │ +│ ┌───────────▼───────────────────┐ │ +│ │ Backend (Internal 8081) │ │ +│ │ • Spring Boot │ │ +│ │ • PDF Processing │ │ +│ │ • UnoServer │ │ +│ └───────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +### MODE=FRONTEND +``` +┌─────────────────────────────┐ ┌──────────────────┐ +│ Frontend Container │ │ Backend │ +│ Port 8080 │ │ (External) │ +│ ┌───────────────────────┐ │ │ │ +│ │ Nginx │ │──────▶ :8080/api │ +│ │ • Serves frontend │ │ │ │ +│ │ • Proxies to backend │ │ │ │ +│ └───────────────────────┘ │ └──────────────────┘ +└─────────────────────────────┘ +``` + +### MODE=BACKEND +``` +┌─────────────────────────────┐ +│ Backend Container │ +│ Port 8080 │ +│ ┌───────────────────────┐ │ +│ │ Spring Boot │ │ +│ │ • API Endpoints │ │ +│ │ • PDF Processing │ │ +│ │ • UnoServer │ │ +│ └───────────────────────┘ │ +└─────────────────────────────┘ +``` + +--- + +## Environment Variables + +### MODE Configuration + +| Variable | Values | Default | Description | +|----------|--------|---------|-------------| +| `MODE` | `BOTH`, `FRONTEND`, `BACKEND` | `BOTH` | Container operation mode | + +### MODE=BOTH Specific + +| Variable | Default | Description | +|----------|---------|-------------| +| `BACKEND_INTERNAL_PORT` | `8081` | Internal port for backend when MODE=BOTH | + +### MODE=FRONTEND Specific + +| Variable | Default | Description | +|----------|---------|-------------| +| `VITE_API_BASE_URL` | `http://backend:8080` | Backend URL for API proxying | + +### Standard Configuration + +All modes support standard Stirling-PDF environment variables: + +- `DISABLE_ADDITIONAL_FEATURES` - Enable/disable OCR and LibreOffice features +- `DOCKER_ENABLE_SECURITY` - Enable authentication +- `PUID` / `PGID` - User/Group IDs +- `SYSTEM_MAXFILESIZE` - Max upload size (MB) +- `TESSERACT_LANGS` - Comma-separated OCR language codes +- `JAVA_CUSTOM_OPTS` - Additional JVM options + +See full configuration docs at: https://docs.stirlingpdf.com + +--- + +## Docker Compose Examples + +### Example 1: All-in-One (MODE=BOTH) + +**File:** `docker/compose/docker-compose-unified-both.yml` + +```yaml +services: + stirling-pdf: + image: stirlingtools/stirling-pdf:unified + ports: + - "8080:8080" + volumes: + - ./data:/usr/share/tessdata:rw + - ./config:/configs:rw + environment: + MODE: BOTH + restart: unless-stopped +``` + +### Example 2: Separate Frontend & Backend + +**File:** `docker/compose/docker-compose-unified-frontend.yml` + +```yaml +services: + backend: + image: stirlingtools/stirling-pdf:unified + ports: + - "8081:8080" + environment: + MODE: BACKEND + volumes: + - ./data:/usr/share/tessdata:rw + - ./config:/configs:rw + + frontend: + image: stirlingtools/stirling-pdf:unified + ports: + - "8080:8080" + environment: + MODE: FRONTEND + VITE_API_BASE_URL: http://backend:8080 + depends_on: + - backend +``` + +### Example 3: Backend API Only + +**File:** `docker/compose/docker-compose-unified-backend.yml` + +```yaml +services: + stirling-pdf-api: + image: stirlingtools/stirling-pdf:unified + ports: + - "8080:8080" + environment: + MODE: BACKEND + volumes: + - ./data:/usr/share/tessdata:rw + - ./config:/configs:rw + restart: unless-stopped +``` + +--- + +## Building the Image + +```bash +# From repository root +docker build -t stirlingtools/stirling-pdf:unified -f docker/Dockerfile.unified . +``` + +### Build Arguments + +| Argument | Description | +|----------|-------------| +| `VERSION_TAG` | Version tag for the image | + +Example: +```bash +docker build \ + --build-arg VERSION_TAG=v1.0.0 \ + -t stirlingtools/stirling-pdf:unified \ + -f docker/Dockerfile.unified . +``` + +--- + +## Use Cases + +### 1. Simple Deployment (MODE=BOTH) +- **Best for:** Personal use, small teams, simple deployments +- **Pros:** Single container, easy setup, minimal configuration +- **Cons:** Frontend and backend scale together + +### 2. Scaled Frontend (MODE=FRONTEND + BACKEND) +- **Best for:** High traffic, need to scale frontend independently +- **Pros:** Scale frontend containers separately, CDN-friendly +- **Example:** + ```yaml + services: + backend: + image: stirlingtools/stirling-pdf:unified + environment: + MODE: BACKEND + deploy: + replicas: 1 + + frontend: + image: stirlingtools/stirling-pdf:unified + environment: + MODE: FRONTEND + VITE_API_BASE_URL: http://backend:8080 + deploy: + replicas: 5 # Scale frontend independently + ``` + +### 3. API-Only (MODE=BACKEND) +- **Best for:** Headless deployments, custom frontends, API integrations +- **Pros:** Minimal resources, no nginx overhead +- **Example:** Use with external frontend or API consumers + +### 4. Multi-Backend Setup +- **Best for:** Load balancing, high availability +- **Example:** + ```yaml + services: + backend-1: + image: stirlingtools/stirling-pdf:unified + environment: + MODE: BACKEND + + backend-2: + image: stirlingtools/stirling-pdf:unified + environment: + MODE: BACKEND + + frontend: + image: stirlingtools/stirling-pdf:unified + environment: + MODE: FRONTEND + VITE_API_BASE_URL: http://load-balancer:8080 + ``` + +--- + +## Port Configuration + +All modes use **port 8080** by default: + +- **MODE=BOTH**: Nginx listens on 8080, proxies to backend on internal 8081 +- **MODE=FRONTEND**: Nginx listens on 8080 +- **MODE=BACKEND**: Spring Boot listens on 8080 + +**Expose port 8080** in all configurations: +```yaml +ports: + - "8080:8080" +``` + +--- + +## Health Checks + +### MODE=BOTH and MODE=BACKEND +```yaml +healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"] + interval: 30s + timeout: 10s + retries: 3 +``` + +### MODE=FRONTEND +```yaml +healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/ || exit 1"] + interval: 30s + timeout: 10s + retries: 3 +``` + +--- + +## Troubleshooting + +### Check logs +```bash +docker logs stirling-pdf-container +``` + +Look for the startup banner: +``` +=================================== +Stirling-PDF Unified Container +MODE: BOTH +=================================== +``` + +### Invalid MODE error +``` +ERROR: Invalid MODE 'XYZ'. Must be BOTH, FRONTEND, or BACKEND +``` +**Fix:** Set `MODE` to one of the three valid values. + +### Frontend can't connect to backend (MODE=FRONTEND) +**Check:** +1. `VITE_API_BASE_URL` points to correct backend URL +2. Backend container is running and accessible +3. Network connectivity between containers + +### Backend not starting (MODE=BOTH or BACKEND) +**Check:** +1. Sufficient memory allocated (4GB recommended) +2. Java heap size (`JAVA_CUSTOM_OPTS`) +3. Volume permissions for `/tmp/stirling-pdf` + +--- + +## Migration Guide + +### From Separate Containers → MODE=BOTH + +**Before:** +```yaml +services: + frontend: + image: stirlingtools/stirling-pdf:frontend + ports: ["80:80"] + + backend: + image: stirlingtools/stirling-pdf:backend + ports: ["8080:8080"] +``` + +**After:** +```yaml +services: + stirling-pdf: + image: stirlingtools/stirling-pdf:unified + ports: ["8080:8080"] + environment: + MODE: BOTH +``` + +### From Legacy → MODE=BACKEND +```yaml +services: + stirling-pdf: + image: stirlingtools/stirling-pdf:latest + ports: ["8080:8080"] +``` + +**Becomes:** +```yaml +services: + stirling-pdf: + image: stirlingtools/stirling-pdf:unified + ports: ["8080:8080"] + environment: + MODE: BACKEND +``` + +--- + +## Performance Tuning + +### MODE=BOTH +```yaml +environment: + JAVA_CUSTOM_OPTS: "-Xmx4g -XX:MaxRAMPercentage=75" + BACKEND_INTERNAL_PORT: 8081 +deploy: + resources: + limits: + memory: 4G + reservations: + memory: 2G +``` + +### MODE=FRONTEND (Lightweight) +```yaml +deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M +``` + +### MODE=BACKEND (Heavy Processing) +```yaml +environment: + JAVA_CUSTOM_OPTS: "-Xmx8g" +deploy: + resources: + limits: + memory: 10G + reservations: + memory: 4G +``` + +--- + +## Security Considerations + +1. **MODE=BOTH**: Backend not exposed externally (runs on internal port) +2. **MODE=BACKEND**: API exposed directly - consider API authentication +3. **MODE=FRONTEND**: Only serves static files - minimal attack surface + +Enable security features: +```yaml +environment: + DOCKER_ENABLE_SECURITY: "true" + SECURITY_ENABLELOGIN: "true" +``` + +--- + +## Support + +- Documentation: https://docs.stirlingpdf.com +- GitHub Issues: https://github.com/Stirling-Tools/Stirling-PDF/issues +- Docker Hub: https://hub.docker.com/r/stirlingtools/stirling-pdf + +--- + +## License + +MIT License - See repository for full details diff --git a/docker/unified/build.sh b/docker/unified/build.sh new file mode 100644 index 000000000..5dd59d668 --- /dev/null +++ b/docker/unified/build.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Build script for Stirling-PDF Unified Container +# Usage: ./build.sh [version-tag] + +set -e + +VERSION_TAG=${1:-latest} +IMAGE_NAME="stirlingtools/stirling-pdf:unified-${VERSION_TAG}" + +echo "===================================" +echo "Building Stirling-PDF Unified Container" +echo "Version: $VERSION_TAG" +echo "Image: $IMAGE_NAME" +echo "===================================" + +# Navigate to repository root (assuming script is in docker/unified/) +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +REPO_ROOT="$SCRIPT_DIR/../.." + +cd "$REPO_ROOT" + +# Build the image +docker build \ + --build-arg VERSION_TAG="$VERSION_TAG" \ + -t "$IMAGE_NAME" \ + -f docker/Dockerfile.unified \ + . + +echo "===================================" +echo "✓ Build complete!" +echo "Image: $IMAGE_NAME" +echo "" +echo "Test the image:" +echo " MODE=BOTH: docker run -p 8080:8080 -e MODE=BOTH $IMAGE_NAME" +echo " MODE=FRONTEND: docker run -p 8080:8080 -e MODE=FRONTEND $IMAGE_NAME" +echo " MODE=BACKEND: docker run -p 8080:8080 -e MODE=BACKEND $IMAGE_NAME" +echo "===================================" diff --git a/docker/unified/entrypoint.sh b/docker/unified/entrypoint.sh new file mode 100644 index 000000000..92075ff3a --- /dev/null +++ b/docker/unified/entrypoint.sh @@ -0,0 +1,176 @@ +#!/bin/bash + +set -e + +# Default MODE to BOTH if not set +MODE=${MODE:-BOTH} + +echo "===================================" +echo "Stirling-PDF Unified Container" +echo "MODE: $MODE" +echo "===================================" + +# Function to setup OCR (from init.sh) +setup_ocr() { + echo "Setting up OCR languages..." + + # Copy tessdata + mkdir -p /usr/share/tessdata + cp -rn /usr/share/tessdata-original/* /usr/share/tessdata 2>/dev/null || true + + if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then + cp -r /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tessdata 2>/dev/null || true + fi + + if [ -d /usr/share/tesseract-ocr/5/tessdata ]; then + cp -r /usr/share/tesseract-ocr/5/tessdata/* /usr/share/tessdata 2>/dev/null || true + fi + + # Install additional languages if specified + if [[ -n "$TESSERACT_LANGS" ]]; then + SPACE_SEPARATED_LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ') + pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$' + for LANG in $SPACE_SEPARATED_LANGS; do + if [[ $LANG =~ $pattern ]]; then + apk add --no-cache "tesseract-ocr-data-$LANG" 2>/dev/null || true + fi + done + fi +} + +# Function to setup user permissions (from init-without-ocr.sh) +setup_permissions() { + echo "Setting up user permissions..." + + export JAVA_TOOL_OPTIONS="${JAVA_BASE_OPTS} ${JAVA_CUSTOM_OPTS}" + + # Update user and group IDs + if [ ! -z "$PUID" ] && [ "$PUID" != "$(id -u stirlingpdfuser)" ]; then + usermod -o -u "$PUID" stirlingpdfuser || true + fi + + if [ ! -z "$PGID" ] && [ "$PGID" != "$(getent group stirlingpdfgroup | cut -d: -f3)" ]; then + groupmod -o -g "$PGID" stirlingpdfgroup || true + fi + + umask "$UMASK" || true + + # Install fonts if needed + if [[ -n "$LANGS" ]]; then + /scripts/installFonts.sh $LANGS + fi + + # Ensure directories exist with correct permissions + mkdir -p /tmp/stirling-pdf || true + + # Set ownership and permissions + chown -R stirlingpdfuser:stirlingpdfgroup \ + $HOME /logs /scripts /usr/share/fonts/opentype/noto \ + /configs /customFiles /pipeline /tmp/stirling-pdf \ + /var/lib/nginx /var/log/nginx /usr/share/nginx \ + /app.jar 2>/dev/null || echo "[WARN] Some chown operations failed, may run as host user" + + chmod -R 755 /logs /scripts /usr/share/fonts/opentype/noto \ + /configs /customFiles /pipeline /tmp/stirling-pdf 2>/dev/null || true +} + +# Function to configure nginx +configure_nginx() { + local backend_url=$1 + echo "Configuring nginx with backend URL: $backend_url" + sed -i "s|\${BACKEND_URL}|${backend_url}|g" /etc/nginx/nginx.conf +} + +# Function to run as user or root depending on permissions +run_as_user() { + if [ "$(id -u)" = "0" ]; then + # Running as root, use su-exec + su-exec stirlingpdfuser "$@" + else + # Already running as non-root + exec "$@" + fi +} + +# Setup OCR and permissions +setup_ocr +setup_permissions + +# Handle different modes +case "$MODE" in + BOTH) + echo "Starting in BOTH mode: Frontend + Backend on port 8080" + + # Configure nginx to proxy to internal backend + configure_nginx "http://localhost:${BACKEND_INTERNAL_PORT:-8081}" + + # Start backend on internal port + echo "Starting backend on port ${BACKEND_INTERNAL_PORT:-8081}..." + run_as_user sh -c "java -Dfile.encoding=UTF-8 \ + -Djava.io.tmpdir=/tmp/stirling-pdf \ + -Dserver.port=${BACKEND_INTERNAL_PORT:-8081} \ + -jar /app.jar" & + BACKEND_PID=$! + + # Start unoserver for document conversion + run_as_user /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1 & + UNO_PID=$! + + # Wait for backend to start + sleep 3 + + # Start nginx on port 8080 + echo "Starting nginx on port 8080..." + run_as_user nginx -g "daemon off;" & + NGINX_PID=$! + + echo "===================================" + echo "✓ Frontend available at: http://localhost:8080" + echo "✓ Backend API at: http://localhost:8080/api" + echo "✓ Backend running internally on port ${BACKEND_INTERNAL_PORT:-8081}" + echo "===================================" + ;; + + FRONTEND) + echo "Starting in FRONTEND mode: Frontend only on port 8080" + + # Configure nginx with external backend URL + BACKEND_URL=${VITE_API_BASE_URL:-http://backend:8080} + configure_nginx "$BACKEND_URL" + + # Start nginx on port 8080 + echo "Starting nginx on port 8080..." + run_as_user nginx -g "daemon off;" & + NGINX_PID=$! + + echo "===================================" + echo "✓ Frontend available at: http://localhost:8080" + echo "✓ Proxying API calls to: $BACKEND_URL" + echo "===================================" + ;; + + BACKEND) + echo "Starting in BACKEND mode: Backend only on port 8080" + + # Start backend on port 8080 + echo "Starting backend on port 8080..." + run_as_user sh -c "java -Dfile.encoding=UTF-8 \ + -Djava.io.tmpdir=/tmp/stirling-pdf \ + -Dserver.port=8080 \ + -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1" & + BACKEND_PID=$! + + echo "===================================" + echo "✓ Backend API available at: http://localhost:8080/api" + echo "✓ Swagger UI at: http://localhost:8080/swagger-ui/index.html" + echo "===================================" + ;; + + *) + echo "ERROR: Invalid MODE '$MODE'. Must be BOTH, FRONTEND, or BACKEND" + exit 1 + ;; +esac + +# Wait for all background processes +wait diff --git a/docker/unified/nginx.conf b/docker/unified/nginx.conf new file mode 100644 index 000000000..77ee17f89 --- /dev/null +++ b/docker/unified/nginx.conf @@ -0,0 +1,118 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Add .mjs MIME type mapping + types { + text/javascript mjs; + } + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; + + server { + listen 8080; + server_name _; + root /usr/share/nginx/html; + index index.html index.htm; + + # Global settings for file uploads + client_max_body_size 100m; + + # Handle client-side routing - support subpaths + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API calls to backend + location /api/ { + proxy_pass ${BACKEND_URL}/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # Additional headers for proper API proxying + proxy_set_header Connection ''; + proxy_http_version 1.1; + proxy_buffering off; + proxy_cache off; + + # Timeout settings for large file uploads + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # Request size limits for file uploads + client_max_body_size 100m; + proxy_request_buffering off; + } + + # Proxy Swagger UI to backend (including versioned paths) + location ~ ^/swagger-ui(.*)$ { + proxy_pass ${BACKEND_URL}/swagger-ui$1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + proxy_set_header Connection ''; + proxy_http_version 1.1; + proxy_buffering off; + proxy_cache off; + } + + # Proxy API docs to backend (with query parameters and sub-paths) + location ~ ^/v3/api-docs(.*)$ { + proxy_pass ${BACKEND_URL}/v3/api-docs$1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + } + + # Proxy v1 API docs to backend (with query parameters and sub-paths) + location ~ ^/v1/api-docs(.*)$ { + proxy_pass ${BACKEND_URL}/v1/api-docs$1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + } + + # Serve .mjs files with correct MIME type (must come before general static assets) + location ~* \.mjs$ { + try_files $uri =404; + add_header Content-Type "text/javascript; charset=utf-8" always; + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + } +}