unoserver docker (#6328)

# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## 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/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)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### 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 run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
This commit is contained in:
Anthony Stirling
2026-05-12 13:22:15 +01:00
committed by GitHub
parent f60a075443
commit d62f2ad3ed
13 changed files with 614 additions and 30 deletions
+1 -1
View File
@@ -97,7 +97,7 @@ To add a 3rd UNO server:
```yaml
unoserver3:
container_name: UNO-Server-3
image: ghcr.io/unoconv/unoserver-docker:0.4.4
image: ghcr.io/stirling-tools/stirling-unoserver:latest
# ... same config as unoserver1/2
```
@@ -57,38 +57,34 @@ services:
unoserver1:
container_name: UNO-Server-1
# NOTE: This image needs to be updated to unoserver 3.6 to match Stirling-PDF's client version
# Current :latest uses 3.4 which causes API mismatch errors
image: ghcr.io/unoconv/unoserver-docker:latest
# See docker/unoserver/VERSION for available tags.
image: ghcr.io/stirling-tools/stirling-unoserver:latest
environment:
UNOSERVER_PORT: "2003"
UNOSERVER_UNO_PORT: "2002"
UNOSERVER_CONVERSION_TIMEOUT: "1800"
# Recycle hourly to bound LibreOffice memory; 0 disables, floor 60s.
UNOSERVER_RECYCLE_INTERVAL_SECONDS: "3600"
volumes:
- stirling-tmp:/tmp/stirling-pdf:rw
expose:
- "2003"
healthcheck:
test: ["CMD-SHELL", "timeout 2 bash -c 'cat < /dev/null > /dev/tcp/localhost/2003' || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- stirling-network
restart: on-failure:5
unoserver2:
container_name: UNO-Server-2
# NOTE: This image needs to be updated to unoserver 3.6 to match Stirling-PDF's client version
# Current :latest uses 3.4 which causes API mismatch errors
image: ghcr.io/unoconv/unoserver-docker:latest
image: ghcr.io/stirling-tools/stirling-unoserver:latest
environment:
UNOSERVER_PORT: "2003"
UNOSERVER_UNO_PORT: "2002"
UNOSERVER_CONVERSION_TIMEOUT: "1800"
UNOSERVER_RECYCLE_INTERVAL_SECONDS: "3600"
volumes:
- stirling-tmp:/tmp/stirling-pdf:rw
expose:
- "2003"
healthcheck:
test: ["CMD-SHELL", "timeout 2 bash -c 'cat < /dev/null > /dev/tcp/localhost/2003' || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- stirling-network
restart: on-failure:5
+106
View File
@@ -0,0 +1,106 @@
# Standalone unoserver image for Stirling-PDF remote UNO mode.
# Pinned to unoserver 3.6 to match Stirling-PDF's client (avoids 3.4/3.6 wire mismatch).
FROM ubuntu:noble@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b
ARG UNOSERVER_VERSION=3.6
# ~120 MB of CJK fonts — opt-in.
ARG INSTALL_CJK_FONTS=false
ENV DEBIAN_FRONTEND=noninteractive \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
UNOSERVER_PORT=2003 \
UNOSERVER_UNO_PORT=2002 \
UNOSERVER_INTERFACE=0.0.0.0 \
UNOSERVER_CONVERSION_TIMEOUT=1800 \
UNOSERVER_RECYCLE_INTERVAL_SECONDS=0 \
UNOSERVER_PROFILE_DIR=/var/lib/unoserver/profile
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
software-properties-common ca-certificates curl gnupg tini; \
add-apt-repository -y ppa:libreoffice/ppa; \
apt-get update; \
apt-get install -y --no-install-recommends \
python3 python3-venv python3-uno python3-packaging \
libreoffice-writer-nogui libreoffice-calc-nogui \
libreoffice-impress-nogui libreoffice-draw-nogui \
libreoffice-base-nogui libreoffice-java-common \
xvfb x11-utils coreutils \
fonts-dejavu fonts-liberation fonts-liberation2 \
fonts-crosextra-caladea fonts-crosextra-carlito \
fonts-noto-core fonts-noto-mono fonts-noto-color-emoji \
hyphen-en-gb hyphen-en-us hyphen-fr hyphen-de hyphen-es hyphen-it \
hyphen-pt-pt hyphen-pt-br hyphen-nl hyphen-pl hyphen-ru \
fontconfig \
libcairo2 libpango-1.0-0 libpangoft2-1.0-0 libgdk-pixbuf-2.0-0 \
libfreetype6 libfontconfig1 libx11-6 libxt6 libxext6 libxrender1 \
libxtst6 libxi6 libxinerama1 libxkbcommon0 libsm6 libice6 \
libglib2.0-0; \
if [ "${INSTALL_CJK_FONTS}" = "true" ]; then \
apt-get install -y --no-install-recommends fonts-noto-cjk; \
fi; \
apt-get remove --purge -y software-properties-common; \
apt-get autoremove --purge -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
# Noble's hyphen-* packages skip LibreOffice registration; wire it up by hand
# or the dicts never load.
RUN set -eu; \
mkdir -p /usr/share/myspell/dicts /usr/share/myspell/infos/ooo; \
for f in /usr/share/hyphen/hyph_*_*.dic; do \
base=$(basename "$f" .dic); \
locale=${base#hyph_}; \
lang=${locale%_*}; \
country=${locale#*_}; \
ln -sfn "../../hyphen/${base}.dic" "/usr/share/myspell/dicts/${base}.dic"; \
printf "HYPH %s %s %s\n" "$lang" "$country" "$base" \
> "/usr/share/myspell/infos/ooo/hyphen-${lang}-${country}"; \
done
COPY --chmod=0644 docker/unoserver/fonts.conf /etc/fonts/conf.d/100-stirling.conf
RUN fc-cache -f
# venv with --system-site-packages so it picks up distro python3-uno.
RUN python3 -m venv /opt/unoserver-venv --system-site-packages && \
/opt/unoserver-venv/bin/pip install --no-cache-dir "unoserver==${UNOSERVER_VERSION}" && \
ln -sf /opt/unoserver-venv/bin/unoserver /usr/local/bin/unoserver && \
ln -sf /opt/unoserver-venv/bin/unoconvert /usr/local/bin/unoconvert && \
ln -sf /opt/unoserver-venv/bin/unoping /usr/local/bin/unoping && \
find /opt/unoserver-venv -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
RUN rm -rf /usr/lib/libreoffice/share/gallery \
/usr/lib/libreoffice/share/template \
/usr/lib/libreoffice/share/wizards \
/usr/lib/libreoffice/help \
/usr/lib/libreoffice/share/extensions/wiki-publisher \
/usr/lib/libreoffice/share/extensions/nlpsolver \
/usr/share/doc/* /usr/share/man/* /usr/share/info/*
ARG PUID=1001
ARG PGID=1001
RUN groupadd -g "${PGID}" unoserver && \
useradd -u "${PUID}" -g unoserver -m -d /home/unoserver -s /bin/bash unoserver && \
mkdir -p "${UNOSERVER_PROFILE_DIR}" && \
chown -R unoserver:unoserver "${UNOSERVER_PROFILE_DIR}" /home/unoserver
COPY --chmod=0755 docker/unoserver/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY --chmod=0755 docker/unoserver/healthcheck.sh /usr/local/bin/healthcheck.sh
USER unoserver
WORKDIR /home/unoserver
EXPOSE 2003
HEALTHCHECK --interval=15s --timeout=5s --retries=4 --start-period=45s \
CMD /usr/local/bin/healthcheck.sh
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"]
LABEL org.opencontainers.image.title="Stirling-PDF Unoserver" \
org.opencontainers.image.description="Pinned unoserver build for Stirling-PDF remote UNO mode" \
org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.vendor="Stirling-Tools"
+1
View File
@@ -0,0 +1 @@
1.0.0
+104
View File
@@ -0,0 +1,104 @@
#!/bin/bash
set -eu
PORT="${UNOSERVER_PORT:-2003}"
UNO_PORT="${UNOSERVER_UNO_PORT:-2002}"
INTERFACE="${UNOSERVER_INTERFACE:-0.0.0.0}"
CONVERSION_TIMEOUT="${UNOSERVER_CONVERSION_TIMEOUT:-1800}"
RECYCLE_INTERVAL_SECONDS="${UNOSERVER_RECYCLE_INTERVAL_SECONDS:-0}"
RECYCLE_INTERVAL_FLOOR=60
PROFILE_DIR="${UNOSERVER_PROFILE_DIR:-/var/lib/unoserver/profile}"
log() { printf '%s %s\n' "[unoserver-entrypoint]" "$*" >&2; }
case "$PORT" in ''|*[!0-9]*) log "Invalid UNOSERVER_PORT='$PORT'"; exit 64 ;; esac
case "$UNO_PORT" in ''|*[!0-9]*) log "Invalid UNOSERVER_UNO_PORT='$UNO_PORT'"; exit 64 ;; esac
case "$CONVERSION_TIMEOUT" in ''|*[!0-9]*) log "Invalid UNOSERVER_CONVERSION_TIMEOUT='$CONVERSION_TIMEOUT'"; exit 64 ;; esac
case "$RECYCLE_INTERVAL_SECONDS" in ''|*[!0-9]*) log "Invalid UNOSERVER_RECYCLE_INTERVAL_SECONDS='$RECYCLE_INTERVAL_SECONDS'"; exit 64 ;; esac
mkdir -p "$PROFILE_DIR"
start_xvfb() {
if command -v Xvfb >/dev/null 2>&1 && [ -z "${DISPLAY:-}" ]; then
Xvfb :99 -screen 0 1024x768x24 -ac +extension GLX +render -noreset >/dev/null 2>&1 &
XVFB_PID=$!
export DISPLAY=:99
sleep 1
log "Xvfb started (pid $XVFB_PID, DISPLAY=$DISPLAY)"
fi
}
cleanup() {
trap '' TERM INT EXIT
if [ -n "${UNOSERVER_PID:-}" ] && kill -0 "$UNOSERVER_PID" 2>/dev/null; then
log "Stopping unoserver (pid $UNOSERVER_PID)"
pkill -TERM -P "$UNOSERVER_PID" 2>/dev/null || true
kill -TERM "$UNOSERVER_PID" 2>/dev/null || true
wait "$UNOSERVER_PID" 2>/dev/null || true
fi
if [ -n "${XVFB_PID:-}" ] && kill -0 "$XVFB_PID" 2>/dev/null; then
kill -TERM "$XVFB_PID" 2>/dev/null || true
fi
}
trap cleanup TERM INT EXIT
start_unoserver() {
log "Starting unoserver on ${INTERFACE}:${PORT} (uno-port ${UNO_PORT}, timeout ${CONVERSION_TIMEOUT}s, profile ${PROFILE_DIR})"
# Pass --user-installation as a plain path; unoserver 3.6 wraps it itself
# and crashes if pre-wrapped as a file:// URI.
unoserver \
--interface "$INTERFACE" \
--port "$PORT" \
--uno-port "$UNO_PORT" \
--user-installation "${PROFILE_DIR}" \
--conversion-timeout "$CONVERSION_TIMEOUT" \
2> >(grep --line-buffered -v "POST /RPC2" >&2) \
&
UNOSERVER_PID=$!
}
# Wall-clock recycle to bound LibreOffice memory growth. wait -n is unreliable
# here because the unoserver job is wrapped in a process substitution.
recycle_supervisor() {
if [ "$RECYCLE_INTERVAL_SECONDS" -le 0 ]; then
log "Recycle disabled (UNOSERVER_RECYCLE_INTERVAL_SECONDS=0)"
wait "$UNOSERVER_PID"
return $?
fi
local interval="$RECYCLE_INTERVAL_SECONDS"
if [ "$interval" -lt "$RECYCLE_INTERVAL_FLOOR" ]; then
log "Clamping recycle interval ${interval}s up to floor ${RECYCLE_INTERVAL_FLOOR}s"
interval="$RECYCLE_INTERVAL_FLOOR"
fi
log "Recycle enabled: restart every ${interval}s"
while true; do
local elapsed=0
while [ "$elapsed" -lt "$interval" ]; do
if ! kill -0 "$UNOSERVER_PID" 2>/dev/null; then
wait "$UNOSERVER_PID"
local rc=$?
log "unoserver exited on its own (rc=$rc); not recycling"
return "$rc"
fi
sleep 1
elapsed=$((elapsed + 1))
done
log "Recycling unoserver (pid ${UNOSERVER_PID})"
pkill -TERM -P "$UNOSERVER_PID" 2>/dev/null || true
kill -TERM "$UNOSERVER_PID" 2>/dev/null || true
for _ in 1 2 3 4 5; do
kill -0 "$UNOSERVER_PID" 2>/dev/null || break
sleep 1
done
pkill -KILL -P "$UNOSERVER_PID" 2>/dev/null || true
kill -KILL "$UNOSERVER_PID" 2>/dev/null || true
wait "$UNOSERVER_PID" 2>/dev/null || true
rm -rf "${PROFILE_DIR:?}"/* 2>/dev/null || true
start_unoserver
log "unoserver restarted (pid ${UNOSERVER_PID})"
done
}
start_xvfb
start_unoserver
recycle_supervisor
+30
View File
@@ -0,0 +1,30 @@
<?xml version='1.0'?>
<!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
<!-- Subpixel hinting + slight hint + LCD filter for headless LibreOffice. -->
<fontconfig>
<match target="font">
<edit mode="assign" name="rgba">
<const>rgb</const>
</edit>
</match>
<match target="font">
<edit mode="assign" name="hinting">
<bool>true</bool>
</edit>
</match>
<match target="font">
<edit mode="assign" name="hintstyle">
<const>hintslight</const>
</edit>
</match>
<match target="font">
<edit mode="assign" name="antialias">
<bool>true</bool>
</edit>
</match>
<match target="font">
<edit mode="assign" name="lcdfilter">
<const>lcddefault</const>
</edit>
</match>
</fontconfig>
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# Real RPC ping — a TCP socket can be bound while LibreOffice is wedged.
set -eu
PORT="${UNOSERVER_PORT:-2003}"
if command -v unoping >/dev/null 2>&1; then
unoping --host 127.0.0.1 --port "$PORT" >/dev/null 2>&1
exit $?
fi
exec timeout 2 bash -c "cat < /dev/null > /dev/tcp/127.0.0.1/${PORT}"