Build Overleaf Image #150
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Build overleaf runtime image from upstream overleaf/overleaf | |
| # This workflow builds the runtime image with full TeX Live scheme-full | |
| # Daily scheduled for EDGE (main branch) builds only | |
| # Note: Stable releases are handled via mirroring from Docker Hub | |
| name: Build Overleaf Image | |
| on: | |
| schedule: | |
| # Daily at 02:00 UTC (after base image weekly build) | |
| - cron: '0 2 * * *' | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - '.github/workflows/build-overleaf.yml' | |
| - '.github/scripts/**' | |
| workflow_dispatch: | |
| inputs: | |
| force_rebuild: | |
| description: 'Force rebuild even if image exists' | |
| required: false | |
| default: 'false' | |
| type: boolean | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: btreemap/overleaf | |
| BASE_IMAGE_NAME: btreemap/overleaf-base | |
| permissions: | |
| contents: read | |
| packages: write | |
| jobs: | |
| detect-upstream: | |
| name: Detect Upstream State | |
| runs-on: ubuntu-latest | |
| outputs: | |
| edge_sha: ${{ steps.detect.outputs.edge_sha }} | |
| edge_sha_short: ${{ steps.detect.outputs.edge_sha_short }} | |
| build_date: ${{ steps.detect.outputs.build_date }} | |
| build_datetime: ${{ steps.detect.outputs.build_datetime }} | |
| should_build_edge: ${{ steps.check.outputs.should_build_edge }} | |
| steps: | |
| - name: Detect upstream state | |
| id: detect | |
| run: | | |
| # Get the tip of main branch from upstream (EDGE) | |
| EDGE_SHA=$(git ls-remote https://github.com/overleaf/overleaf.git refs/heads/main | cut -f1) | |
| echo "edge_sha=${EDGE_SHA}" >> "$GITHUB_OUTPUT" | |
| echo "edge_sha_short=${EDGE_SHA:0:7}" >> "$GITHUB_OUTPUT" | |
| echo "Detected upstream edge SHA: ${EDGE_SHA}" | |
| # Generate build timestamps | |
| BUILD_DATE=$(date -u '+%Y-%m-%d') | |
| BUILD_DATETIME=$(date -u '+%Y-%m-%d.%H-%M-%S') | |
| echo "build_date=${BUILD_DATE}" >> "$GITHUB_OUTPUT" | |
| echo "build_datetime=${BUILD_DATETIME}" >> "$GITHUB_OUTPUT" | |
| echo "Build date: ${BUILD_DATE}" | |
| echo "Build datetime: ${BUILD_DATETIME}" | |
| - name: Check if images exist in GHCR | |
| id: check | |
| run: | | |
| EDGE_SHA="${{ steps.detect.outputs.edge_sha }}" | |
| FORCE_REBUILD="${{ inputs.force_rebuild || 'false' }}" | |
| check_image_exists() { | |
| local tag="$1" | |
| if docker buildx imagetools inspect "ghcr.io/${{ env.IMAGE_NAME }}:${tag}" > /dev/null 2>&1; then | |
| return 0 # exists | |
| else | |
| return 1 # does not exist | |
| fi | |
| } | |
| # Determine if we should build edge | |
| SHOULD_BUILD_EDGE="false" | |
| if [[ "$FORCE_REBUILD" == "true" ]]; then | |
| SHOULD_BUILD_EDGE="true" | |
| echo "Force rebuild requested for edge" | |
| elif ! check_image_exists "edge-sha-${EDGE_SHA}"; then | |
| SHOULD_BUILD_EDGE="true" | |
| echo "Edge image not found, will build" | |
| else | |
| echo "Edge image already exists for SHA ${EDGE_SHA}, skipping" | |
| fi | |
| echo "should_build_edge=${SHOULD_BUILD_EDGE}" >> "$GITHUB_OUTPUT" | |
| # =========================================== | |
| # EDGE BUILD | |
| # =========================================== | |
| build-edge: | |
| name: Build Edge (${{ matrix.arch }}) | |
| needs: detect-upstream | |
| if: needs.detect-upstream.outputs.should_build_edge == 'true' | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runner: ubuntu-latest | |
| arch: amd64 | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| arch: arm64 | |
| steps: | |
| - name: Checkout this repo for scripts | |
| uses: actions/checkout@v6 | |
| - name: Checkout upstream overleaf | |
| uses: actions/checkout@v6 | |
| with: | |
| repository: overleaf/overleaf | |
| path: upstream | |
| ref: ${{ needs.detect-upstream.outputs.edge_sha }} | |
| - name: Generate CI Dockerfile with full TeX Live | |
| run: | | |
| cat > upstream/Dockerfile.ci << 'EOF' | |
| # CI-generated Dockerfile for full TeX Live installation | |
| # Built from upstream overleaf/overleaf with scheme-full TeX Live | |
| ARG OVERLEAF_BASE_TAG=ghcr.io/btreemap/overleaf-base:edge | |
| FROM $OVERLEAF_BASE_TAG AS base | |
| WORKDIR /overleaf | |
| # Add required source files | |
| ADD server-ce/genScript.js /overleaf/genScript.js | |
| ADD server-ce/services.js /overleaf/services.js | |
| ADD package.json package-lock.json /overleaf/ | |
| ADD libraries/ /overleaf/libraries/ | |
| ADD services/ /overleaf/services/ | |
| ADD tools/migrations/ /overleaf/tools/migrations/ | |
| # Add npm patches | |
| ADD patches/ /overleaf/patches | |
| # Install npm dependencies and build webpack assets | |
| RUN --mount=type=cache,target=/root/.cache \ | |
| --mount=type=cache,target=/root/.npm \ | |
| --mount=type=cache,target=/overleaf/services/web/node_modules/.cache,id=server-ce-webpack-cache \ | |
| --mount=type=tmpfs,target=/tmp true \ | |
| && node genScript install | bash \ | |
| && node genScript compile | bash | |
| # Copy runit service startup scripts to its location | |
| ADD server-ce/runit /etc/service | |
| # Copy runit global settings to its location | |
| ADD server-ce/config/env.sh /etc/overleaf/env.sh | |
| # Configure nginx | |
| ADD server-ce/nginx/nginx.conf.template /etc/nginx/templates/nginx.conf.template | |
| ADD server-ce/nginx/overleaf.conf /etc/nginx/sites-enabled/overleaf.conf | |
| ADD server-ce/nginx/clsi-nginx.conf /etc/nginx/sites-enabled/clsi-nginx.conf | |
| # Configure log rotation | |
| ADD server-ce/logrotate/overleaf /etc/logrotate.d/overleaf | |
| RUN chmod 644 /etc/logrotate.d/overleaf | |
| # Configure cron tasks | |
| ADD server-ce/cron /overleaf/cron | |
| ADD server-ce/config/crontab-history /etc/cron.d/crontab-history | |
| RUN chmod 600 /etc/cron.d/crontab-history | |
| ADD server-ce/config/crontab-deletion /etc/cron.d/crontab-deletion | |
| RUN chmod 600 /etc/cron.d/crontab-deletion | |
| # Copy Phusion Image startup and shutdown scripts to their locations | |
| COPY server-ce/init_scripts/ /etc/my_init.d/ | |
| COPY server-ce/init_preshutdown_scripts/ /etc/my_init.pre_shutdown.d/ | |
| # Copy app settings files | |
| COPY server-ce/config/settings.js /etc/overleaf/settings.js | |
| # Copy history-v1 files | |
| COPY server-ce/config/production.json /overleaf/services/history-v1/config/production.json | |
| COPY server-ce/config/custom-environment-variables.json /overleaf/services/history-v1/config/custom-environment-variables.json | |
| # Copy grunt thin wrapper | |
| ADD server-ce/bin/grunt /usr/local/bin/grunt | |
| RUN chmod +x /usr/local/bin/grunt | |
| # Copy history helper scripts | |
| ADD server-ce/bin/flush-history-queues /overleaf/bin/flush-history-queues | |
| RUN chmod +x /overleaf/bin/flush-history-queues | |
| ADD server-ce/bin/force-history-resyncs /overleaf/bin/force-history-resyncs | |
| RUN chmod +x /overleaf/bin/force-history-resyncs | |
| # Copy Latexmkrc | |
| COPY server-ce/config/latexmkrc /usr/local/share/latexmk/LatexMk | |
| # ============================================ | |
| # FULL TEX LIVE INSTALLATION (AIR-GAPPED) | |
| # ============================================ | |
| # Install scheme-full for complete LaTeX package availability | |
| # This ensures the image works offline without needing to download packages | |
| RUN tlmgr install scheme-full \ | |
| && tlmgr path add \ | |
| # Clean up tlmgr caches to reduce image size | |
| && rm -rf /usr/local/texlive/*/tlpkg/backups/* \ | |
| && rm -rf /tmp/* | |
| # File that controls open|closed status of the site | |
| ENV SITE_MAINTENANCE_FILE="/etc/overleaf/site_status" | |
| RUN touch $SITE_MAINTENANCE_FILE | |
| # Set Environment Variables | |
| ENV OVERLEAF_CONFIG=/etc/overleaf/settings.js | |
| ENV WEB_API_USER="overleaf" | |
| ENV ADMIN_PRIVILEGE_AVAILABLE="true" | |
| ENV OVERLEAF_APP_NAME="Overleaf Community Edition" | |
| ENV OPTIMISE_PDF="true" | |
| # Phusion Image timeouts before sending SIGKILL to processes | |
| ENV KILL_PROCESS_TIMEOUT=55 | |
| ENV KILL_ALL_PROCESSES_TIMEOUT=55 | |
| ENV GRACEFUL_SHUTDOWN_DELAY_SECONDS=1 | |
| ENV NODE_ENV="production" | |
| ENV LOG_LEVEL="info" | |
| EXPOSE 80 | |
| ENTRYPOINT ["/sbin/my_init"] | |
| EOF | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate Docker metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| labels: | | |
| org.opencontainers.image.title=Overleaf (Edge) | |
| org.opencontainers.image.description=Overleaf Community Edition with full TeX Live - Edge build from main | |
| org.opencontainers.image.vendor=BTreeMap | |
| org.opencontainers.image.source=https://github.com/overleaf/overleaf | |
| org.opencontainers.image.revision=${{ needs.detect-upstream.outputs.edge_sha }} | |
| - name: Build and push by digest | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: upstream | |
| file: upstream/Dockerfile.ci | |
| platforms: ${{ matrix.platform }} | |
| push: true | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| OVERLEAF_BASE_TAG=ghcr.io/${{ env.BASE_IMAGE_NAME }}:edge | |
| outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | |
| cache-from: type=gha,scope=overleaf-edge-${{ matrix.arch }} | |
| cache-to: type=gha,mode=max,scope=overleaf-edge-${{ matrix.arch }} | |
| - name: Export digest | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.build.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| - name: Upload digest | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: digests-edge-${{ matrix.arch }} | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| merge-edge: | |
| name: Create Edge Multi-Arch Manifest | |
| needs: [detect-upstream, build-edge] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download digests | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: /tmp/digests | |
| pattern: digests-edge-* | |
| merge-multiple: true | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate Docker metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| # Immutable SHA tag | |
| type=raw,value=edge-sha-${{ needs.detect-upstream.outputs.edge_sha }} | |
| # Floating edge tags | |
| type=raw,value=edge | |
| type=raw,value=edge-${{ needs.detect-upstream.outputs.build_date }} | |
| type=raw,value=edge-${{ needs.detect-upstream.outputs.build_datetime }} | |
| - name: Create manifest list and push | |
| working-directory: /tmp/digests | |
| run: | | |
| TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") | |
| DIGESTS=$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) | |
| # shellcheck disable=SC2086 | |
| docker buildx imagetools create $TAGS $DIGESTS | |
| - name: Inspect image | |
| run: | | |
| docker buildx imagetools inspect "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:edge" |