From 21210850ec57ca106c61935a66f3f0c692794578 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:16:34 +0100 Subject: [PATCH] V2 docker support react version2 (#3930) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: a --- .github/workflows/PR-Auto-Deploy-V2.yml | 310 ++++++++++++++++++ .gitignore | 1 + Dockerfile.dev | 60 ---- docker/README.md | 67 ++++ Dockerfile => docker/backend/Dockerfile | 13 +- .../backend/Dockerfile.fat | 4 +- .../backend/Dockerfile.ultra-lite | 4 +- docker/compose/docker-compose.fat.yml | 66 ++++ docker/compose/docker-compose.monolith.yml | 42 +++ docker/compose/docker-compose.ultra-lite.yml | 56 ++++ docker/compose/docker-compose.yml | 64 ++++ docker/frontend/Dockerfile | 38 +++ docker/frontend/entrypoint.sh | 10 + docker/frontend/nginx.conf | 49 +++ docker/monolith/Dockerfile | 128 ++++++++ docker/monolith/nginx-monolith.conf | 49 +++ docker/monolith/start-monolith.sh | 20 ++ 17 files changed, 910 insertions(+), 71 deletions(-) create mode 100644 .github/workflows/PR-Auto-Deploy-V2.yml delete mode 100644 Dockerfile.dev create mode 100644 docker/README.md rename Dockerfile => docker/backend/Dockerfile (90%) rename Dockerfile.fat => docker/backend/Dockerfile.fat (97%) rename Dockerfile.ultra-lite => docker/backend/Dockerfile.ultra-lite (95%) create mode 100644 docker/compose/docker-compose.fat.yml create mode 100644 docker/compose/docker-compose.monolith.yml create mode 100644 docker/compose/docker-compose.ultra-lite.yml create mode 100644 docker/compose/docker-compose.yml create mode 100644 docker/frontend/Dockerfile create mode 100644 docker/frontend/entrypoint.sh create mode 100644 docker/frontend/nginx.conf create mode 100644 docker/monolith/Dockerfile create mode 100644 docker/monolith/nginx-monolith.conf create mode 100644 docker/monolith/start-monolith.sh diff --git a/.github/workflows/PR-Auto-Deploy-V2.yml b/.github/workflows/PR-Auto-Deploy-V2.yml new file mode 100644 index 000000000..98d60c904 --- /dev/null +++ b/.github/workflows/PR-Auto-Deploy-V2.yml @@ -0,0 +1,310 @@ +name: Auto PR V2 Deployment + +on: + pull_request: + types: [opened, synchronize, reopened] + + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + check-pr: + runs-on: ubuntu-latest + outputs: + should_deploy: ${{ steps.check-conditions.outputs.should_deploy }} + pr_number: ${{ github.event.number }} + pr_repository: ${{ steps.get-pr-info.outputs.repository }} + pr_ref: ${{ steps.get-pr-info.outputs.ref }} + steps: + - name: Harden Runner + uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + with: + egress-policy: audit + + - name: Check deployment conditions + id: check-conditions + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + PR_BRANCH: ${{ github.event.pull_request.head.ref }} + run: | + echo "PR Title: $PR_TITLE" + echo "PR Author: $PR_AUTHOR" + echo "PR Branch: $PR_BRANCH" + + # Check if author is authorized + authorized_users=( + "Frooodle" + "sf298" + "Ludy87" + "LaserKaspar" + "sbplat" + "reecebrowne" + "DarioGii" + "ConnorYoh" + ) + + is_authorized=false + for user in "${authorized_users[@]}"; do + if [[ "$PR_AUTHOR" == "$user" ]]; then + is_authorized=true + break + fi + done + + # Check if title contains V2/version2 keywords (case insensitive) + has_v2_keyword=false + if [[ "$PR_TITLE" =~ [Vv]2 ]] || [[ "$PR_TITLE" =~ [Vv]ersion.?2 ]] || [[ "$PR_TITLE" =~ [Vv]ersion.?[Tt]wo ]]; then + has_v2_keyword=true + fi + + # Check if branch name contains V2 or react keywords (case insensitive) + has_branch_keyword=false + if [[ "$PR_BRANCH" =~ [Vv]2 ]] || [[ "$PR_BRANCH" =~ [Rr]eact ]]; then + has_branch_keyword=true + fi + + if [[ "$is_authorized" == "true" ]] && [[ "$has_v2_keyword" == "true" || "$has_branch_keyword" == "true" ]]; then + echo "✅ Deployment conditions met" + echo "should_deploy=true" >> $GITHUB_OUTPUT + else + echo "❌ Deployment conditions not met" + echo " - Authorized user: $is_authorized" + echo " - Has V2 keyword in title: $has_v2_keyword" + echo " - Has V2/React keyword in branch: $has_branch_keyword" + echo "should_deploy=false" >> $GITHUB_OUTPUT + fi + + - name: Get PR repository and ref + id: get-pr-info + if: steps.check-conditions.outputs.should_deploy == 'true' + run: | + # For forks, use the full repository name, for internal PRs use the current repo + if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then + repository="${{ github.event.pull_request.head.repo.full_name }}" + else + repository="${{ github.repository }}" + fi + + echo "repository=$repository" >> $GITHUB_OUTPUT + echo "ref=${{ github.event.pull_request.head.ref }}" >> $GITHUB_OUTPUT + + deploy-v2-pr: + needs: check-pr + runs-on: ubuntu-latest + if: needs.check-pr.outputs.should_deploy == 'true' + # Concurrency control - only one deployment per PR at a time + concurrency: + group: v2-deploy-pr-${{ needs.check-pr.outputs.pr_number }} + cancel-in-progress: true + permissions: + contents: read + issues: write + pull-requests: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + with: + egress-policy: audit + + - name: Generate GitHub App Token + id: generate-token + uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 + with: + app-id: ${{ secrets.GH_APP_ID }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + + - name: Add deployment started comment + id: deployment-started + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ steps.generate-token.outputs.token }} + script: | + const { owner, repo } = context.repo; + const prNumber = ${{ needs.check-pr.outputs.pr_number }}; + + // Delete previous V2 deployment comments to avoid clutter + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: prNumber, + per_page: 100 + }); + + const v2Comments = comments.filter(comment => + comment.body.includes('🚀 **Auto-deploying V2 version**') || + comment.body.includes('## 🚀 V2 Auto-Deployment Complete!') || + comment.body.includes('❌ **V2 Auto-deployment failed**') + ); + + for (const comment of v2Comments) { + console.log(`Deleting old V2 comment: ${comment.id}`); + await github.rest.issues.deleteComment({ + owner, + repo, + comment_id: comment.id + }); + } + + // Create new deployment started comment + const { data: newComment } = await github.rest.issues.createComment({ + owner, + repo, + issue_number: prNumber, + body: `🚀 **Auto-deploying V2 version** for PR #${prNumber}...\n\n_This is an automated deployment triggered by V2/version2 keywords in the PR title or V2/React keywords in the branch name._\n\n⚠️ **Note:** If new commits are pushed during deployment, this build will be cancelled and replaced with the latest version.` + }); + + return newComment.id; + + - name: Checkout PR + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: ${{ needs.check-pr.outputs.pr_repository }} + ref: ${{ needs.check-pr.outputs.pr_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up JDK + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + java-version: "17" + distribution: "temurin" + + - name: Build backend + run: | + export DISABLE_ADDITIONAL_FEATURES=true + ./gradlew clean build + env: + STIRLING_PDF_DESKTOP_UI: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + + - name: Get version number + id: versionNumber + run: | + VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}') + echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT + + - name: Login to Docker Hub + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_API }} + + - name: Build and push V2 monolith image + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + context: . + file: ./docker/monolith/Dockerfile + push: true + tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-pr-${{ needs.check-pr.outputs.pr_number }} + build-args: VERSION_TAG=v2-alpha + platforms: linux/amd64 + + - name: Set up SSH + run: | + mkdir -p ~/.ssh/ + echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key + sudo chmod 600 ../private.key + + - name: Deploy V2 to VPS + id: deploy + run: | + # Use same port strategy as regular PRs - just the PR number + V2_PORT=${{ needs.check-pr.outputs.pr_number }} + + # Create docker-compose for V2 monolith + cat > docker-compose.yml << EOF + version: '3.3' + services: + stirling-pdf-v2: + container_name: stirling-pdf-v2-pr-${{ needs.check-pr.outputs.pr_number }} + image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-pr-${{ needs.check-pr.outputs.pr_number }} + ports: + - "${V2_PORT}:80" # Frontend port (same as regular PRs) + volumes: + - /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/data:/usr/share/tessdata:rw + - /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw + - /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/logs:/logs:rw + environment: + DISABLE_ADDITIONAL_FEATURES: "true" + SECURITY_ENABLELOGIN: "false" + SYSTEM_DEFAULTLOCALE: en-GB + UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}" + UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Frontend/Backend Split Architecture" + UI_APPNAMENAVBAR: "V2 PR#${{ needs.check-pr.outputs.pr_number }}" + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + SYSTEM_GOOGLEVISIBILITY: "false" + restart: on-failure:5 + EOF + + # Deploy to VPS + scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose-v2.yml + + ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH + # Create V2 PR-specific directories + mkdir -p /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/{data,config,logs} + + # Move docker-compose file to correct location + mv /tmp/docker-compose-v2.yml /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/docker-compose.yml + + # Stop any existing container and clean up + cd /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }} + docker-compose down --remove-orphans 2>/dev/null || true + + # Start the new container + docker-compose pull + docker-compose up -d + + # Clean up unused Docker resources to save space + docker system prune -af --volumes + ENDSSH + + # Set port for output + echo "v2_port=${V2_PORT}" >> $GITHUB_OUTPUT + + - name: Post V2 deployment URL to PR + if: success() + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ steps.generate-token.outputs.token }} + script: | + const { owner, repo } = context.repo; + const prNumber = ${{ needs.check-pr.outputs.pr_number }}; + const v2Port = ${{ steps.deploy.outputs.v2_port }}; + + // Delete the "deploying..." comment since we're posting the final result + const deploymentStartedId = ${{ steps.deployment-started.outputs.result }}; + if (deploymentStartedId) { + console.log(`Deleting deployment started comment: ${deploymentStartedId}`); + try { + await github.rest.issues.deleteComment({ + owner, + repo, + comment_id: deploymentStartedId + }); + } catch (error) { + console.log(`Could not delete deployment started comment: ${error.message}`); + } + } + + const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${v2Port}`; + + const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` + + `Your V2 PR with the new frontend/backend split architecture has been deployed!\n\n` + + `🔗 **V2 Test URL:** [${deploymentUrl}](${deploymentUrl})\n\n` + + `_This deployment will be automatically cleaned up when the PR is closed._\n\n` + + `🔄 **Auto-deployed** because PR title or branch name contains V2/version2/React keywords.`; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: prNumber, + body: commentBody + }); + diff --git a/.gitignore b/.gitignore index 52e57d719..0018c3da8 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ clientWebUI/ !cucumber/exampleFiles/ !cucumber/exampleFiles/example_html.zip exampleYmlFiles/stirling/ +stirling/ /testing/file_snapshots SwaggerDoc.json diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index 15de277b9..000000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,60 +0,0 @@ -# dockerfile.dev - -# Basisimage: Gradle mit JDK 17 (Debian-basiert) -FROM gradle:8.14-jdk17 - -# Als Root-Benutzer arbeiten, um benötigte Pakete zu installieren -USER root - -# Set GRADLE_HOME und füge Gradle zum PATH hinzu -ENV GRADLE_HOME=/opt/gradle -ENV PATH="$GRADLE_HOME/bin:$PATH" - -# Update und Installation zusätzlicher Pakete (Debian/Ubuntu-basiert) -RUN apt-get update && apt-get install -y \ - sudo \ - libreoffice \ - poppler-utils \ - qpdf \ -# settings.yml | tessdataDir: /usr/share/tesseract-ocr/5/tessdata - tesseract-ocr \ - tesseract-ocr-eng \ - fonts-terminus fonts-dejavu fonts-font-awesome fonts-noto fonts-noto-core fonts-noto-cjk fonts-noto-extra fonts-liberation fonts-linuxlibertine \ - python3-uno \ - python3-venv \ -# ss -tln - iproute2 \ - && apt-get clean && rm -rf /var/lib/apt/lists/* - -# Setze die Environment Variable für setuptools -ENV SETUPTOOLS_USE_DISTUTILS=local \ - STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \ - TMPDIR=/tmp/stirling-pdf \ - TEMP=/tmp/stirling-pdf \ - TMP=/tmp/stirling-pdf - -# Installation der benötigten Python-Pakete -RUN python3 -m venv --system-site-packages /opt/venv \ - && . /opt/venv/bin/activate \ - && pip install --upgrade pip setuptools \ - && pip install --no-cache-dir WeasyPrint pdf2image pillow unoserver opencv-python-headless pre-commit - -# Füge den venv-Pfad zur globalen PATH-Variable hinzu, damit die Tools verfügbar sind -ENV PATH="/opt/venv/bin:$PATH" - -COPY . /workspace - -RUN mkdir -p /tmp/stirling-pdf \ - && adduser --disabled-password --gecos '' devuser \ - && chown -R devuser:devuser /home/devuser /workspace /tmp/stirling-pdf -RUN echo "devuser ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/devuser \ - && chmod 0440 /etc/sudoers.d/devuser - -# Setze das Arbeitsverzeichnis (wird später per Bind-Mount überschrieben) -WORKDIR /workspace - -RUN chmod +x /workspace/.devcontainer/git-init.sh -RUN sudo chmod +x /workspace/.devcontainer/init-setup.sh - -# Wechsel zum Nicht‑Root Benutzer -USER devuser diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..e73a82072 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,67 @@ +# Docker Setup for Stirling-PDF + +This directory contains the organized Docker configurations for the split frontend/backend architecture. + +## Directory Structure + +``` +docker/ +├── backend/ # Backend Docker files +│ ├── Dockerfile # Standard backend +│ ├── Dockerfile.ultra-lite # Minimal backend +│ └── Dockerfile.fat # Full-featured backend +├── frontend/ # Frontend Docker files +│ ├── Dockerfile # React/Vite frontend with nginx +│ ├── nginx.conf # Nginx configuration +│ └── entrypoint.sh # Dynamic backend URL setup +├── monolith/ # Single container setup +│ ├── Dockerfile # Combined frontend + backend +│ ├── nginx-monolith.conf # Nginx config for monolith +│ └── start-monolith.sh # Startup script +└── compose/ # Docker Compose files + ├── docker-compose.yml # Standard setup + ├── docker-compose.ultra-lite.yml # Ultra-lite setup + ├── docker-compose.fat.yml # Full-featured setup + └── docker-compose.monolith.yml # Single container setup +``` + +## Usage + +### Separate Containers (Recommended) + +From the project root directory: + +```bash +# Standard version +docker-compose -f docker/compose/docker-compose.yml up --build + +# Ultra-lite version +docker-compose -f docker/compose/docker-compose.ultra-lite.yml up --build + +# Fat version +docker-compose -f docker/compose/docker-compose.fat.yml up --build +``` + +### Single Container (Monolith) + +```bash +# Single container with both frontend and backend +docker-compose -f docker/compose/docker-compose.monolith.yml up --build +``` + +## Access Points + +- **Frontend**: http://localhost:3000 +- **Backend API (debugging)**: http://localhost:8080 (TODO: Remove in production) +- **Backend API (via frontend)**: http://localhost:3000/api/* + +## Configuration + +- **Backend URL**: Set `BACKEND_URL` environment variable for custom backend locations +- **Custom Ports**: Modify port mappings in docker-compose files +- **Memory Limits**: Adjust memory limits per variant (2G ultra-lite, 4G standard, 6G fat) + +## Development vs Production + +- **Development**: Keep backend port 8080 exposed for debugging +- **Production**: Remove backend port exposure, use only frontend proxy \ No newline at end of file diff --git a/Dockerfile b/docker/backend/Dockerfile similarity index 90% rename from Dockerfile rename to docker/backend/Dockerfile index fd02b29f7..eedbb279c 100644 --- a/Dockerfile +++ b/docker/backend/Dockerfile @@ -1,4 +1,4 @@ -# Main stage +# Backend Dockerfile - Java Spring Boot with all dependencies FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715 # Copy necessary files @@ -9,8 +9,8 @@ COPY stirling-pdf/build/libs/*.jar app.jar ARG VERSION_TAG -LABEL org.opencontainers.image.title="Stirling-PDF" -LABEL org.opencontainers.image.description="A powerful locally hosted web-based PDF manipulation tool supporting 50+ operations including merging, splitting, conversion, OCR, watermarking, and more." +LABEL org.opencontainers.image.title="Stirling-PDF Backend" +LABEL org.opencontainers.image.description="Backend service for Stirling-PDF - Java Spring Boot with PDF processing capabilities" 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" @@ -19,7 +19,7 @@ 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, merge, split, convert, OCR, watermark" +LABEL org.opencontainers.image.keywords="PDF, manipulation, backend, API, Spring Boot" # Set Environment Variables ENV DISABLE_ADDITIONAL_FEATURES=true \ @@ -39,8 +39,7 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \ TEMP=/tmp/stirling-pdf \ TMP=/tmp/stirling-pdf - -# JDK for app +# JDK for app and 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 && \ @@ -95,4 +94,4 @@ EXPOSE 8080/tcp # Set user and run command ENTRYPOINT ["tini", "--", "/scripts/init.sh"] -CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"] +CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"] \ No newline at end of file diff --git a/Dockerfile.fat b/docker/backend/Dockerfile.fat similarity index 97% rename from Dockerfile.fat rename to docker/backend/Dockerfile.fat index 666ba98be..06a4ec3cb 100644 --- a/Dockerfile.fat +++ b/docker/backend/Dockerfile.fat @@ -1,3 +1,4 @@ +# Backend fat Dockerfile - Java Spring Boot with all dependencies and build stage # Build the application FROM gradle:8.14-jdk21 AS build @@ -52,7 +53,6 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \ TEMP=/tmp/stirling-pdf \ TMP=/tmp/stirling-pdf - # JDK for app 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 && \ @@ -108,4 +108,4 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a EXPOSE 8080/tcp # Set user and run command ENTRYPOINT ["tini", "--", "/scripts/init.sh"] -CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"] +CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"] \ No newline at end of file diff --git a/Dockerfile.ultra-lite b/docker/backend/Dockerfile.ultra-lite similarity index 95% rename from Dockerfile.ultra-lite rename to docker/backend/Dockerfile.ultra-lite index c4eb4ba46..3335bee67 100644 --- a/Dockerfile.ultra-lite +++ b/docker/backend/Dockerfile.ultra-lite @@ -1,4 +1,4 @@ -# use alpine +# Backend ultra-lite Dockerfile - Java Spring Boot with minimal dependencies FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715 ARG VERSION_TAG @@ -52,4 +52,4 @@ EXPOSE 8080/tcp # Run the application ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"] -CMD ["java", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=/tmp/stirling-pdf", "-jar", "/app.jar"] +CMD ["java", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=/tmp/stirling-pdf", "-jar", "/app.jar"] \ No newline at end of file diff --git a/docker/compose/docker-compose.fat.yml b/docker/compose/docker-compose.fat.yml new file mode 100644 index 000000000..764c9604c --- /dev/null +++ b/docker/compose/docker-compose.fat.yml @@ -0,0 +1,66 @@ +version: '3.8' + +services: + backend: + build: + context: ../.. + dockerfile: docker/backend/Dockerfile.fat + container_name: stirling-pdf-backend-fat + restart: on-failure:5 + deploy: + resources: + limits: + memory: 6G + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] + interval: 5s + timeout: 10s + retries: 16 + ports: + - "8080:8080" # TODO: Remove in production - for debugging only + expose: + - "8080" + volumes: + - ../../stirling/latest/data:/usr/share/tessdata:rw + - ../../stirling/latest/config:/configs:rw + - ../../stirling/latest/logs:/logs:rw + environment: + DISABLE_ADDITIONAL_FEATURES: "false" + SECURITY_ENABLELOGIN: "false" + FAT_DOCKER: "true" + INSTALL_BOOK_AND_ADVANCED_HTML_OPS: "false" + LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID" + SYSTEM_DEFAULTLOCALE: en-US + UI_APPNAME: Stirling-PDF + UI_HOMEDESCRIPTION: Full-featured Stirling-PDF with all capabilities + UI_APPNAMENAVBAR: Stirling-PDF Fat + SYSTEM_MAXFILESIZE: "200" + METRICS_ENABLED: "true" + SYSTEM_GOOGLEVISIBILITY: "true" + SHOW_SURVEY: "true" + networks: + - stirling-network + + frontend: + build: + context: ../../frontend + dockerfile: ../docker/frontend/Dockerfile + container_name: stirling-pdf-frontend-fat + restart: on-failure:5 + ports: + - "3000:80" + environment: + BACKEND_URL: http://backend:8080 + depends_on: + - backend + networks: + - stirling-network + +networks: + stirling-network: + driver: bridge + +volumes: + stirling-data: + stirling-config: + stirling-logs: \ No newline at end of file diff --git a/docker/compose/docker-compose.monolith.yml b/docker/compose/docker-compose.monolith.yml new file mode 100644 index 000000000..06064cdf7 --- /dev/null +++ b/docker/compose/docker-compose.monolith.yml @@ -0,0 +1,42 @@ +version: '3.8' + +services: + stirling-pdf-monolith: + build: + context: ../.. + dockerfile: docker/monolith/Dockerfile + container_name: stirling-pdf-monolith + restart: on-failure:5 + deploy: + resources: + limits: + memory: 4G + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:80/ && curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] + interval: 10s + timeout: 15s + retries: 16 + ports: + - "3000:80" # Frontend access + - "8080:8080" # Direct backend access (for debugging) + volumes: + - ../../stirling/latest/data:/usr/share/tessdata:rw + - ../../stirling/latest/config:/configs:rw + - ../../stirling/latest/logs:/logs:rw + environment: + DISABLE_ADDITIONAL_FEATURES: "true" + SECURITY_ENABLELOGIN: "false" + LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID" + SYSTEM_DEFAULTLOCALE: en-US + UI_APPNAME: Stirling-PDF + UI_HOMEDESCRIPTION: Single container Stirling-PDF + UI_APPNAMENAVBAR: Stirling-PDF Monolith + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + SYSTEM_GOOGLEVISIBILITY: "true" + SHOW_SURVEY: "true" + +volumes: + stirling-data: + stirling-config: + stirling-logs: \ No newline at end of file diff --git a/docker/compose/docker-compose.ultra-lite.yml b/docker/compose/docker-compose.ultra-lite.yml new file mode 100644 index 000000000..a16484262 --- /dev/null +++ b/docker/compose/docker-compose.ultra-lite.yml @@ -0,0 +1,56 @@ +version: '3.8' + +services: + backend: + build: + context: ../.. + dockerfile: docker/backend/Dockerfile.ultra-lite + container_name: stirling-pdf-backend-ultra-lite + restart: on-failure:5 + deploy: + resources: + limits: + memory: 2G + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] + interval: 5s + timeout: 10s + retries: 16 + ports: + - "8080:8080" # TODO: Remove in production - for debugging only + expose: + - "8080" + volumes: + - ../../stirling/latest/config:/configs:rw + - ../../stirling/latest/logs:/logs:rw + environment: + DISABLE_ADDITIONAL_FEATURES: "true" + SECURITY_ENABLELOGIN: "false" + ENDPOINTS_GROUPS_TO_REMOVE: "CLI" + LANGS: "en_GB,en_US" + SYSTEM_DEFAULTLOCALE: en-US + UI_APPNAME: Stirling-PDF + UI_HOMEDESCRIPTION: Ultra-lite version of Stirling-PDF + UI_APPNAMENAVBAR: Stirling-PDF Ultra-lite + SYSTEM_MAXFILESIZE: "50" + networks: + - stirling-network + + frontend: + build: + context: ../../frontend + dockerfile: ../docker/frontend/Dockerfile + container_name: stirling-pdf-frontend-ultra-lite + restart: on-failure:5 + ports: + - "3000:80" + environment: + BACKEND_URL: http://backend:8080 + depends_on: + - backend + networks: + - stirling-network + +networks: + stirling-network: + driver: bridge \ No newline at end of file diff --git a/docker/compose/docker-compose.yml b/docker/compose/docker-compose.yml new file mode 100644 index 000000000..3bcdca423 --- /dev/null +++ b/docker/compose/docker-compose.yml @@ -0,0 +1,64 @@ +version: '3.8' + +services: + backend: + build: + context: ../.. + dockerfile: docker/backend/Dockerfile + container_name: stirling-pdf-backend + restart: on-failure:5 + deploy: + resources: + limits: + memory: 4G + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] + interval: 5s + timeout: 10s + retries: 16 + ports: + - "8080:8080" # TODO: Remove in production - for debugging only + expose: + - "8080" + volumes: + - ../../stirling/latest/data:/usr/share/tessdata:rw + - ../../stirling/latest/config:/configs:rw + - ../../stirling/latest/logs:/logs:rw + environment: + DISABLE_ADDITIONAL_FEATURES: "true" + SECURITY_ENABLELOGIN: "false" + LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID" + SYSTEM_DEFAULTLOCALE: en-US + UI_APPNAME: Stirling-PDF + UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest + UI_APPNAMENAVBAR: Stirling-PDF Latest + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + SYSTEM_GOOGLEVISIBILITY: "true" + SHOW_SURVEY: "true" + networks: + - stirling-network + + frontend: + build: + context: ../../frontend + dockerfile: ../docker/frontend/Dockerfile + container_name: stirling-pdf-frontend + restart: on-failure:5 + ports: + - "3000:80" + environment: + BACKEND_URL: http://backend:8080 + depends_on: + - backend + networks: + - stirling-network + +networks: + stirling-network: + driver: bridge + +volumes: + stirling-data: + stirling-config: + stirling-logs: \ No newline at end of file diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile new file mode 100644 index 000000000..af570b6bb --- /dev/null +++ b/docker/frontend/Dockerfile @@ -0,0 +1,38 @@ +# Frontend Dockerfile - React/Vite application +FROM node:20-alpine AS build + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy built files from build stage +COPY --from=build /app/dist /usr/share/nginx/html + +# Copy nginx configuration and entrypoint +COPY nginx.conf /etc/nginx/nginx.conf +COPY entrypoint.sh /entrypoint.sh + +# Make entrypoint executable +RUN chmod +x /entrypoint.sh + +# Expose port 80 (standard HTTP port) +EXPOSE 80 + +# Environment variables for flexibility +ENV BACKEND_URL=http://backend:8080 + +# Use custom entrypoint +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/docker/frontend/entrypoint.sh b/docker/frontend/entrypoint.sh new file mode 100644 index 000000000..ca1d6cba7 --- /dev/null +++ b/docker/frontend/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# Set default backend URL if not provided +BACKEND_URL=${BACKEND_URL:-"http://backend:8080"} + +# Replace the placeholder in nginx.conf with the actual backend URL +sed -i "s|\${BACKEND_URL}|${BACKEND_URL}|g" /etc/nginx/nginx.conf + +# Start nginx +exec nginx -g "daemon off;" \ No newline at end of file diff --git a/docker/frontend/nginx.conf b/docker/frontend/nginx.conf new file mode 100644 index 000000000..f45f5784b --- /dev/null +++ b/docker/frontend/nginx.conf @@ -0,0 +1,49 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # 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 80; + server_name _; + root /usr/share/nginx/html; + index index.html index.htm; + + # 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; + } + + # 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; + } +} \ No newline at end of file diff --git a/docker/monolith/Dockerfile b/docker/monolith/Dockerfile new file mode 100644 index 000000000..1957fb8f5 --- /dev/null +++ b/docker/monolith/Dockerfile @@ -0,0 +1,128 @@ +# Monolith Dockerfile - Frontend + Backend in same container +# Build frontend +FROM node:20-alpine AS frontend-build + +WORKDIR /app/frontend + +# Copy frontend package files +COPY frontend/package*.json ./ + +# Install frontend dependencies +RUN npm ci + +# Copy frontend source +COPY frontend/ ./ + +# Build frontend +RUN npm run build + +# Main stage - Backend with frontend files +FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715 + +# Copy necessary files +COPY scripts /scripts +COPY pipeline /pipeline +COPY stirling-pdf/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ +COPY stirling-pdf/build/libs/*.jar app.jar + +# Copy built frontend files +COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html + +ARG VERSION_TAG + +LABEL org.opencontainers.image.title="Stirling-PDF Monolith" +LABEL org.opencontainers.image.description="Single container with both frontend and backend for Stirling-PDF" +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, monolith, single-container" + +# Set Environment Variables +ENV DISABLE_ADDITIONAL_FEATURES=true \ + 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 + +# Install nginx and 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 \ + qpdf \ + shadow \ + su-exec \ + openssl \ + openssl-dev \ + openjdk21-jre \ + nginx \ + # Doc conversion + gcompat \ + libc6-compat \ + libreoffice \ + # pdftohtml + poppler-utils \ + # OCR MY PDF (unpaper for descew and other advanced features) + tesseract-ocr-data-eng \ + tesseract-ocr-data-chi_sim \ + tesseract-ocr-data-deu \ + tesseract-ocr-data-fra \ + tesseract-ocr-data-por \ + # 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 && \ + fc-cache -f -v && \ + chmod +x /scripts/* && \ + chmod +x /scripts/init.sh && \ + # User permissions + addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ + chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \ + chown stirlingpdfuser:stirlingpdfgroup /app.jar && \ + chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/nginx/html + +# Copy nginx configuration for monolith +COPY docker/monolith/nginx-monolith.conf /etc/nginx/nginx.conf + +# Copy startup script +COPY docker/monolith/start-monolith.sh /start-monolith.sh +RUN chmod +x /start-monolith.sh + +# Expose both ports +EXPOSE 80 8080 + +# Set user and run command +ENTRYPOINT ["tini", "--"] +CMD ["/start-monolith.sh"] \ No newline at end of file diff --git a/docker/monolith/nginx-monolith.conf b/docker/monolith/nginx-monolith.conf new file mode 100644 index 000000000..05fa72b7d --- /dev/null +++ b/docker/monolith/nginx-monolith.conf @@ -0,0 +1,49 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # 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 80; + server_name _; + root /usr/share/nginx/html; + index index.html index.htm; + + # Handle client-side routing - support subpaths + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API calls to backend running on same container + location /api/ { + proxy_pass http://localhost:8080/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; + } + + # 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; + } +} \ No newline at end of file diff --git a/docker/monolith/start-monolith.sh b/docker/monolith/start-monolith.sh new file mode 100644 index 000000000..b75f15d2d --- /dev/null +++ b/docker/monolith/start-monolith.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Start the Java backend in the background +echo "Starting Java backend..." +su-exec stirlingpdfuser:stirlingpdfgroup bash -c " + cd /home/stirlingpdfuser && \ + java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & + /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1 & +" + +# Wait for backend to start +echo "Waiting for backend to start..." +until curl -f http://localhost:8080/api/v1/info/status >/dev/null 2>&1; do + sleep 2 +done + +echo "Backend started, starting nginx..." + +# Start nginx in the foreground +nginx -g "daemon off;" \ No newline at end of file