Skip to content

Build Overleaf Full Image #161

Build Overleaf Full Image

Build Overleaf Full Image #161

# Build our own "full" Overleaf image with TeX Live scheme-full
# Daily scheduled build that fetches the latest version from upstream
# and creates an optimized image with minimal layers
#
# Tags: latest, yyyy-mm-dd
name: Build Overleaf Full Image
on:
schedule:
# Daily at 04:00 UTC (after mirroring workflow)
- cron: '0 4 * * *'
push:
branches:
- main
paths:
- '.github/workflows/build-overleaf-full.yml'
- '.github/scripts/mirror_images.py'
- '.github/scripts/check_platform_support.py'
workflow_dispatch:
inputs:
force_rebuild:
description: 'Force rebuild even if image exists for today'
required: false
default: 'false'
type: boolean
base_version:
description: 'Override base image version (leave empty for latest)'
required: false
default: ''
type: string
env:
REGISTRY: ghcr.io
IMAGE_NAME: btreemap/overleaf
permissions:
contents: read
packages: write
jobs:
detect-version:
name: Detect Latest Version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.detect.outputs.version }}
build_date: ${{ steps.detect.outputs.build_date }}
should_build: ${{ steps.check.outputs.should_build }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Detect latest upstream version
id: detect
run: |
BUILD_DATE=$(date -u '+%Y-%m-%d')
echo "build_date=${BUILD_DATE}" >> "$GITHUB_OUTPUT"
OVERRIDE_VERSION="${{ inputs.base_version }}"
if [[ -n "$OVERRIDE_VERSION" ]]; then
echo "version=${OVERRIDE_VERSION}" >> "$GITHUB_OUTPUT"
echo "Using override version: ${OVERRIDE_VERSION}"
else
VERSION=$(python3 .github/scripts/mirror_images.py latest --source sharelatex/sharelatex)
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Detected version: ${VERSION}"
fi
echo "Build date: ${BUILD_DATE}"
- name: Check if today's image exists
id: check
run: |
BUILD_DATE="${{ steps.detect.outputs.build_date }}"
FORCE_REBUILD="${{ inputs.force_rebuild || 'false' }}"
IMAGE="ghcr.io/${{ env.IMAGE_NAME }}"
if [[ "$FORCE_REBUILD" == "true" ]]; then
echo "Force rebuild requested"
echo "should_build=true" >> "$GITHUB_OUTPUT"
elif docker buildx imagetools inspect "${IMAGE}:${BUILD_DATE}" > /dev/null 2>&1; then
echo "Image for ${BUILD_DATE} already exists, skipping build"
echo "should_build=false" >> "$GITHUB_OUTPUT"
else
echo "Image for ${BUILD_DATE} not found, will build"
echo "should_build=true" >> "$GITHUB_OUTPUT"
fi
build:
name: Build Full Image (${{ matrix.arch }})
needs: detect-version
if: needs.detect-version.outputs.should_build == 'true'
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
arch: amd64
optional: false
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
optional: true
steps:
- name: Checkout repository for scripts
uses: actions/checkout@v6
- name: Generate optimized Dockerfile
run: |
cat > Dockerfile << 'DOCKERFILE'
# Optimized Overleaf Full Image
# Built from upstream sharelatex/sharelatex with full TeX Live and extras
# Commands structured to balance caching and regular apt upgrades
ARG BASE_VERSION=latest
FROM sharelatex/sharelatex:${BASE_VERSION}
SHELL ["/bin/bash", "-c"]
# TeX Live setup and configuration
RUN set -eux; \
# Update tlmgr
wget -q "https://mirror.ctan.org/systems/texlive/tlnet/update-tlmgr-latest.sh" -O /tmp/update-tlmgr.sh; \
sh /tmp/update-tlmgr.sh; \
tlmgr --version; \
# Update texlive-scripts for ctex support
tlmgr update texlive-scripts; \
# Update all packages
tlmgr update --all; \
# Install scheme-full
tlmgr install scheme-full; \
# Recreate symlinks
tlmgr path add; \
# Enable shell-escape by default
TEXLIVE_FOLDER=$(find /usr/local/texlive/ -maxdepth 1 -type d -name '20*' | head -1); \
if [ -n "$TEXLIVE_FOLDER" ]; then \
echo "% enable shell-escape by default" >> "$TEXLIVE_FOLDER/texmf.cnf"; \
echo "shell_escape = t" >> "$TEXLIVE_FOLDER/texmf.cnf"; \
fi; \
# Clean up TeX-related caches and temp files
rm -rf /tmp/*; \
rm -rf /usr/local/texlive/*/tlpkg/backups/*; \
rm -rf /root/.cache/*
# Daily-refreshing OS packages and extra tools
# APT_REFRESH is set per-build (daily) in the workflow to bust only this layer's cache.
ARG APT_REFRESH=0
RUN set -eux; \
echo "APT_REFRESH=${APT_REFRESH}"; \
apt-get update; \
apt-get upgrade -y; \
apt-get install -y --no-install-recommends inkscape lilypond; \
apt-get clean; \
rm -rf /var/lib/apt/lists/*
DOCKERFILE
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Check upstream platform availability
id: platform
run: |
set -euo pipefail
IMAGE="docker.io/sharelatex/sharelatex:${{ needs.detect-version.outputs.version }}"
export PLATFORM="${{ matrix.platform }}"
if docker buildx imagetools inspect --format '{{json .}}' "$IMAGE" > /tmp/inspect.json 2> /tmp/inspect.err; then
SUPPORTED=$(python3 .github/scripts/check_platform_support.py \
--platform "${PLATFORM}" \
--inspect-json /tmp/inspect.json)
else
echo "Unable to inspect ${IMAGE}; treating ${PLATFORM} as unsupported."
if [ -s /tmp/inspect.err ]; then
echo "Inspect error output:"
cat /tmp/inspect.err
fi
SUPPORTED="false"
fi
if [[ "$SUPPORTED" == "true" ]]; then
echo "Upstream image supports ${PLATFORM}"
echo "should_build=true" >> "$GITHUB_OUTPUT"
elif [[ "${{ matrix.optional }}" == "true" ]]; then
echo "Upstream image does not provide ${PLATFORM}; skipping build."
echo "should_build=false" >> "$GITHUB_OUTPUT"
else
echo "Upstream image does not provide ${PLATFORM}." >&2
exit 1
fi
- 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 Full
org.opencontainers.image.description=Overleaf with full TeX Live scheme-full, inkscape, and lilypond
org.opencontainers.image.vendor=BTreeMap
org.opencontainers.image.version=${{ needs.detect-version.outputs.version }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v7
if: steps.platform.outputs.should_build == 'true'
with:
context: .
file: ./Dockerfile
platforms: ${{ matrix.platform }}
push: true
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BASE_VERSION=${{ needs.detect-version.outputs.version }}
APT_REFRESH=${{ needs.detect-version.outputs.build_date }}
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=overleaf-full-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=overleaf-full-${{ matrix.arch }}
- name: Export digest
if: steps.platform.outputs.should_build == 'true'
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v7
if: steps.platform.outputs.should_build == 'true'
with:
name: digests-full-${{ matrix.arch }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
name: Create Multi-Arch Manifest
needs: [detect-version, build]
runs-on: ubuntu-latest
steps:
- name: Download digests
uses: actions/download-artifact@v8
continue-on-error: true
with:
path: /tmp/digests
pattern: digests-full-*
merge-multiple: true
- name: Check for digest files
id: digests
run: |
mkdir -p /tmp/digests
digest_probe=$(find /tmp/digests -maxdepth 1 -type f -print -quit)
if [ -n "$digest_probe" ]; then
digests=(/tmp/digests/*)
echo "Found ${#digests[@]} digest(s)."
echo "has_digests=true" >> "$GITHUB_OUTPUT"
else
echo "No digests found; skipping manifest creation."
echo "has_digests=false" >> "$GITHUB_OUTPUT"
fi
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
if: steps.digests.outputs.has_digests == 'true'
- name: Login to GHCR
uses: docker/login-action@v3
if: steps.digests.outputs.has_digests == 'true'
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v5
if: steps.digests.outputs.has_digests == 'true'
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
# Latest tag
type=raw,value=latest
# Date-based tag
type=raw,value=${{ needs.detect-version.outputs.build_date }}
# Version-based tag
type=raw,value=${{ needs.detect-version.outputs.version }}-full
- name: Create manifest list and push
if: steps.digests.outputs.has_digests == 'true'
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
if: steps.digests.outputs.has_digests == 'true'
run: |
docker buildx imagetools inspect "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"