2026/10/21 23:55:33

JDK8+FFmpeg+LibreOffice三合一Docker镜像构建实战与踩坑记录

JDK8+FFmpeg+LibreOffice三合一Docker镜像构建实战与踩坑记录 做过在线文档预览、音视频处理的兄弟应该都懂一个 Java 服务要同时干“Office 转 PDF”和“视频截帧、转码”的活儿基础镜像就得把 JDK8、FFmpeg、LibreOffice 三样全带上。可真正动手做镜像才会发现这三样东西的依赖互相缠绕装完体积动不动就几个 G更别提字体乱码、进程冲突、首次启动慢这些破事。这篇文章我会把 JDK8 FFmpeg LibreOffice 三合一镜像的制作过程、选型逻辑和踩坑记录完整写出来适合负责基础镜像维护、正在搭文档转换服务或视频处理容器的开发者参考。全文不绕弯子直接上结论和可复现的 Dockerfile。1. 为什么需要三合一镜像以及设计思路怎么定1.1 业务场景决定了镜像内容先想清楚一个基础镜像为什么要同时塞三样东西。在很多系统里Java 是业务主语言文档预览模块依赖 LibreOffice 做格式转换音视频模块依赖 FFmpeg 做抽帧、压缩、转格式。如果这三个组件分散在不同容器要么通过微服务拆分要么在运维上做一堆额外协调。对于中小团队来说最省事的方式就是把它们打进同一个镜像让业务容器直接拥有完整能力。这个选择不是单纯为了省事而是为了“环境一致性”。你本地装好了 JDK8、FFmpeg、LibreOffice换一台机器可能就少个编码器或者字体缺失。镜像把操作系统层、运行库、命令工具全部固化成不可变单元无论部署到哪台机器行为都是一致的。JDK8 到现在还是很多老项目的基线很多代码依赖 Java 8 的运行时行为升级 JDK 的风险远大于收益。所以基础镜像直接锁定 JDK8是许多团队的务实选择。LibreOffice 提供 headless 模式可以在没有图形界面的容器里把 doc、docx、ppt、xls 转成 PDFFFmpeg 则负责视频抽帧和格式互转。这三者组合起来能做出一套完整的文件处理能力。1.2 基础镜像选型Ubuntu 20.04 是我最终的选择基础镜像的选择直接影响整个安装过程。最初规划时我评估了三个方向CentOS 7、Alpine、Ubuntu 20.04。CentOS 7 看似稳定但软件源里的 OpenJDK8 版本老旧LibreOffice 和 FFmpeg 都偏旧装新版本又得引入第三方源后期维护成本高。Alpine 体积最小可它用 musl libcLibreOffice 和 FFmpeg 这类重组件在 Alpine 上经常出现 ABI 兼容问题很多预编译包也不直接支持硬装反而浪费时间。Ubuntu 20.04 的官方源里同时有 openjdk-8、ffmpeg、libreoffice不需要额外配置第三方仓库apt 一条命令就能把核心依赖串起来。最终我选FROM ubuntu:20.04不是因为它有多新而是因为它是官方源里最后一个默认带 OpenJDK8 的 LTS 版本。Ubuntu 22.04 的源里已经默认切换到 OpenJDK11/17想装 JDK8 还得额外加仓库维护复杂度一下就上来了。还有一个容易被忽略的点Ubuntu 20.04 的 LTS 支持周期长安全更新跟得上。作为基础镜像稳定性和可维护性比新版本特性更重要。后面如果想升级只需改基础镜像标签但前提是应用和组件都要做好回归测试。1.3 分层设计与构建缓存策略Dockerfile 写得好不好不只影响可读性还直接影响构建速度和镜像体积。Docker 镜像是由一层层只读层叠加的每一行 RUN 命令都会生成一个新层。构建时如果某个 RUN 没变化Docker 可以直接复用缓存层不需要重新执行。所以我倾向于把“不常变的系统依赖安装”和“频繁变动的业务代码”分开。在这个基础镜像里所有系统包安装都放在前面JDK8、FFmpeg、LibreOffice 的版本固定不变这样每次重复构建时大部分层都能命中缓存。同时要注意apt 安装会留下/var/lib/apt/lists下的索引文件和下载缓存这些都会写进镜像层。很多镜像做完特别大不全是软件本身的问题而是清理没做干净。我会在同一个 RUN 里安装完包紧接着执行apt-get clean和删除 lists 文件。把安装和清理放在同一个 RUN 是刻意的如果分成两个 RUN清理只会清掉最后一层前面 apt 层的缓存已经固化进镜像体积照样大。2. 三个核心组件的安装细节与取舍2.1 JDK8用 headless 版本别装完整 JDKJDK8 在 Ubuntu 20.04 官方源里的包名是openjdk-8-jdk-headless。很多 Dockerfile 习惯直接装openjdk-8-jdk这个包会带图形化相关工具比如appletviewer、policytool等。容器环境根本用不到这些装进来只会白白增加体积和攻击面。openjdk-8-jdk-headless去掉了图形界面相关部分保留javac、java、jar等核心命令足够跑 Java 业务服务。安装后要手动设置JAVA_HOME环境变量。在 Ubuntu 20.04 上OpenJDK8 的安装路径通常是/usr/lib/jvm/java-8-openjdk-amd64。没有JAVA_HOME很多 Java 应用启动脚本会直接挂掉尤其是用 Maven、Tomcat 或自定义启动命令的项目。路径可以通过以下命令确认docker run --rm 镜像 readlink -f $(which java)readlink结果会指向真实安装路径但规范路径还是/usr/lib/jvm/java-8-openjdk-amd64。如果以后换成其他 JDK 发行版这行环境变量就要跟着改。我一般在 Dockerfile 里把JAVA_HOME和PATH一起写上确保命令行能直接执行java。2.2 FFmpeg默认用 apt特殊场景再用静态构建FFmpeg 的安装方式有几种apt 安装、官方静态构建包、源码编译。坦白说最佳选择取决于你要做什么。apt 安装最省心Ubuntu 20.04 自带的 FFmpeg 版本是 4.2.x支持常见的 H.264、H.265、VP8、VP9、AAC、MP3 等编解码器。对于在线预览场景里的视频抽帧、缩略图生成、格式转换这个版本足够稳定。如果你只是需要ffmpeg -i input.mp4 -vf thumbnail1 -frames:v 1 output.jpg这种操作apt 版完全够用没必要折腾源码。但如果你需要最新的编码器特性比如 av1、svt-av1、或者修改过的 libx264 分支apt 源里的版本就不够用了。此时有两个选择下载 FFmpeg 官方发布的静态构建包或者自己编译。静态构建包的好处是单个二进制文件拷贝到/usr/local/bin并加上执行权限就能用不污染系统库。风险在于它可能没有链接系统的 libx264 等库所有功能都编译在二进制里体积会大一些且需要根据 glibc 版本选对构建。源码编译则能精确控制功能但编译时间动辄一两个小时还容易遇到缺失依赖的编译错误不适合做成基础镜像的常备步骤。我的建议是先用 apt 版本跑通业务等真遇到编码器缺失时再考虑静态构建。不要一开始就陷入源码编译的泥潭。在 Dockerfile 里安装 FFmpeg 可以简单写一行ffmpeg但我建议后续验证编码器情况ffmpeg -encoders | grep libx264如果有输出说明 H.264 编码可用。如果没有需要安装 libavcodec 的扩展包或者换静态构建。2.3 LibreOffice最小化安装但别省到缺功能LibreOffice 是三件套里体积最大的也是最容易出乱子的。Ubuntu 20.04 官方源里 LibreOffice 版本是 6.4.x虽然不算新但处理常规 Office 文档没有任何问题。安装时我会明确安装这几个包libreoffice-core、libreoffice-writer、libreoffice-calc、libreoffice-impress。writer负责文档calc负责表格impress负责演示文稿。只装libreoffice-writer可能省体积但业务侧如果收到 xls 文件转 PDF 时就会因缺少组件而失败。宁可多装几百 MB也不要让转换任务在运行时出错。--no-install-recommends是必须加的选项。apt 默认会安装推荐包这些推荐包会带进一堆打印机驱动、图形界面组件、邮件通知工具甚至办公辅助组件。在容器里全都没用。加上这个参数后只安装依赖库和核心功能包体积能省出一大截。字体是 LibreOffice 转换流程里最关键的依赖。中文字体缺失最常见的现象是 PDF 里到处是方块或乱码。Ubuntu 源里的fonts-noto-cjk提供了完整的中日韩字体基本覆盖日常文档需求。字体安装后还需要更新字体缓存让 fontconfig 能识别新字体。如果业务对字体有特殊要求比如必须用宋体、黑体或仿宋那就得把对应字体文件 COPY 进镜像放到/usr/share/fonts下再执行fc-cache -f。不要小看这一步很多“用容器转出来的 PDF 中文乱码”问题本质上不是 LibreOffice 出了问题而是容器里压根没有中文字体。3. Dockerfile 完整实现与逐行拆解3.1 最终可复现的 Dockerfile上面讲了一堆设计思路现在给出一个我实际用下来最稳的 Dockerfile。这个方案基于 Ubuntu 20.04apt 安装全部组件同时包含字体、locale、时区、非 root 用户和 LibreOffice 预热步骤。FROM ubuntu:20.04 ARG DEBIAN_FRONTENDnoninteractive ENV TZAsia/Shanghai \ LANGzh_CN.UTF-8 \ LANGUAGEzh_CN:zh \ JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 \ PATH/usr/lib/jvm/java-8-openjdk-amd64/bin:$PATH RUN apt-get update \ apt-get install -y --no-install-recommends \ tzdata \ locales \ ca-certificates \ curl \ fontconfig \ openjdk-8-jdk-headless \ ffmpeg \ libreoffice-core \ libreoffice-writer \ libreoffice-calc \ libreoffice-impress \ fonts-noto-cjk \ apt-get clean \ rm -rf /var/lib/apt/lists/* \ localedef -i zh_CN -f UTF-8 zh_CN.UTF-8 \ ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ echo $TZ /etc/timezone RUN useradd -m -u 1000 appuser \ mkdir -p /app \ chown -R appuser:appuser /app USER appuser WORKDIR /app RUN echo prewarm /tmp/blank.txt \ soffice --headless \ -env:UserInstallationfile:///tmp/lo_profile \ --convert-to pdf --outdir /tmp /tmp/blank.txt /dev/null 21 \ ; rm -f /tmp/blank.txt /tmp/blank.pdf CMD [/bin/bash]3.2 逐项解释关键参数ARG DEBIAN_FRONTENDnoninteractive放在FROM后面只在构建阶段生效不会写进最终镜像的环境变量。这个参数解决了 tzdata 安装时的交互弹窗问题。如果不设置安装 tzdata 时 apt 会停下来问你时区导致构建卡死。用环境变量ENV DEBIAN_FRONTENDnoninteractive也能达到同样效果但它会保留在最终容器环境里没必要。tzdata和locales是容易被忽略的包。设置时区TZAsia/Shanghai不只是为了日志时间还影响 LibreOffice 在转换文档时生成的日期字段。如果容器保持默认 UTC用户文档里的时间戳可能平白差 8 小时这种问题排查起来非常隐蔽。locales配合localedef -i zh_CN -f UTF-8 zh_CN.UTF-8生成中文 locale保证 Java 和系统层面的中文字符正确处理。openjdk-8-jdk-headless和ffmpeg两个包名很关键。我没有装完整 JDK也没有装 FFmpeg 的静态版。前者的理由前面讲过后者是为了保证编码器和系统库的一致性。libreoffice-core加libreoffice-writer、libreoffice-calc、libreoffice-impress这个组合覆盖了最常见的 Word、Excel、PowerPoint 转换需求。如果只装libreoffice-writer遇到 xls 文件就会报错或转出的 PDF 格式怪异。多装两个组件包比后续再往已有镜像里补包要省事得多。fonts-noto-cjk是中文渲染的基础保障安装后必须执行fc-cache -f让字体缓存生效。虽然我这里没有显式写fc-cache -f但 LibreOffice 首次运行时通常会自动扫描字体目录。为了保险你可以在同一个 RUN 里加上这一行。useradd -m -u 1000 appuser创建了一个普通用户。LibreOffice 在 root 用户下运行虽然也能工作但会有安全隐患而且生成的 profile 文件对后续非 root 运行产生权限干扰。提前切换到普通用户容器运行时的默认行为和云原生安全基线更贴合。WORKDIR /app也是配合非 root 用户使用避免应用启动后没有写权限。3.3 构建时预热 LibreOffice那个RUN echo prewarm /tmp/blank.txt的预热步骤可能有人看不懂但它是我解决“首次启动慢”的关键操作。LibreOffice 第一次以 headless 模式启动时需要执行初始化、扫描字体、建立 profile这个过程通常会耗时几十秒甚至更久。在构建阶段先让它跑一次转换把 profile 生成到/tmp/lo_profile并固化进镜像层后续容器启动时就能跳过初始化过程明显缩短首次转换时间。这里一定要加-env:UserInstallationfile:///tmp/lo_profile。这个参数把 LibreOffice 的用户配置目录指向/tmp避免多个 soffice 进程争抢同一个 profile 导致锁冲突。在频繁调用转换的应用里这句话能省下一大堆“卡死”“超时”的问题。4. 构建、验证与镜像瘦身实操4.1 构建命令与缓存利用Dockerfile 准备好之后构建命令很简单docker build -t devtools:jdk8-ffmpeg-lo .第一次构建会比较慢因为要下载 Ubuntu 基础镜像和所有软件包。网络状况好的话大约需要几分钟到十几分钟。如果你只是加了最后一层 RUN 里的业务代码重新构建时前面所有层都会命中缓存速度会快很多。这就是为什么我强调把安装步骤固定在前面的层把可变内容留到最后。构建过程中如果因为网络原因导致某个包下载失败重新执行docker build时已经成功的层会被复用不会从头再来。但也正因如此如果 apt 源的软件包版本有更新而 Dockerfile 里没有写具体版本号缓存可能导致你构建出和上次不同的镜像。想避免这种情况可以在 apt 包名后固定版本号或者在需要时用--no-cache强制重新构建。4.2 容器内验证三件套是否可用镜像构建完成后先别急着接业务先在容器里跑一轮冒烟测试确认三件套都正常工作。先验证基础版本docker run --rm devtools:jdk8-ffmpeg-lo java -version docker run --rm devtools:jdk8-ffmpeg-lo ffmpeg -version docker run --rm devtools:jdk8-ffmpeg-lo soffice --version再验证实际转换能力。准备一个测试用的 docx 文件挂载到容器里转 PDFdocker run --rm -v $(pwd)/testdocs:/data devtools:jdk8-ffmpeg-lo \ soffice --headless -env:UserInstallationfile:///tmp/lo_profile \ --convert-to pdf --outdir /data /data/sample.docxFFmpeg 的抽帧测试docker run --rm -v $(pwd)/testdocs:/data devtools:jdk8-ffmpeg-lo \ ffmpeg -i /data/input.mp4 -vf thumbnail1 -frames:v 1 /data/thumb.jpg这里的坑在于挂载目录权限。宿主机当前用户的 UID 如果和容器内 appuser 的 UID 不一致输出目录可能没有写权限。最简单的办法是在宿主机上把测试目录权限放开比如chmod 777 testdocs。生产环境则要交给编排平台去处理比如 K8s 里的 PVC 挂载或者使用 entrypoint 动态调整用户 UID。不要直接用-u root绕过非 root 设计否则前面创建的 appuser 就白做了。4.3 镜像体积控制在什么程度所有组件装完镜像体积会相当可观。我实测过的镜像从 1.5G 到 2.5G 都有主要看基础镜像和是否清理缓存。这个体积在基础镜像层面是正常的不用太焦虑。真正的优化空间在于减少不必要的包和清理缓存。体积占比大致是这样组件/内容体积影响说明Ubuntu 基础镜像约 70MB底层系统无法省略OpenJDK8 headless约 300MB完整 JDK 会更大约 400MBFFmpeg约 200MB和 libavcodec 等库耦合LibreOffice 核心Writer/Calc/Impress约 800MB体积大头依赖极多中文字体约 100MBNoto CJK 字体包如果你对体积特别敏感可以尝试去掉libreoffice-calc或libreoffice-impress但这取决于业务需求也可以只保留libreoffice-core和libreoffice-writer体积会明显下降但代价是表格和演示文稿转换不可用。还可以用docker history 镜像名 --no-trunc查看每一层的大小找出哪些层异常膨胀再针对性优化。5. 常见问题与排查技巧实录5.1 中文乱码和方块字的真正原因中文转出来全是方块绝大多数情况不是 LibreOffice 的问题而是容器里没有中文字体。noto-cjk 装完之后先用这个命令确认字体是否生效docker run --rm devtools:jdk8-ffmpeg-lo fc-list :langzh如果输出里能看到Noto Sans CJK或Noto Serif CJK说明字体已注册。如果输出为空说明字体缓存没更新需要执行fc-cache -f。如果业务方要求特定字体比如仿宋、宋体必须把字体文件 COPY 到/usr/share/fonts/custom/下然后执行fc-cache -f再重新转换验证。字体版权要留意不要随意把不知来源的字体打包进生产镜像。5.2 LibreOffice 转换卡死或超时处理大量文档时最容易遇到的问题就是 soffice 进程卡死。罪魁祸首通常是多进程并发争抢同一个 LibreOffice profile。解决思路有两个每次转换命令都指定独立的UserInstallation目录或者用单例模式统一排队。使用独立目录是最直接的方式soffice --headless -env:UserInstallationfile:///tmp/lo_profile_$$ \ --convert-to pdf --outdir /data /data/input.docx$$是当前 shell 的 PID每个进程都会用自己的临时目录避免锁冲突。但这样做的代价是每次转换都要重新初始化速度会慢一些。如果你追求吞吐量更推荐在应用层起一个长期驻留的 LibreOffice 服务通过 UNO 接口或 JODConverter 提交任务。基础镜像只需要保证soffice命令存在具体架构由业务层决定。5.3 FFmpeg 缺少编码器怎么办用ffmpeg -encoders检查列表如果发现没有libx264或libx265说明当前 FFmpeg 功能不完整。发生这种情况大概率是你用了静态构建包或者基础系统的 FFmpeg 裁剪过。先用 apt 源里的官方包重装一遍apt-get install -y --no-install-recommends ffmpeg libavcodec-extralibavcodec-extra会带更多私有编码器比如某些 H.264 变体。如果业务必须用最新版本而且功能要求更高再考虑下载官方静态构建包。替换静态包时记得先移除原有 ffmpegapt-get remove -y ffmpeg COPY ffmpeg /usr/local/bin/ffmpeg RUN chmod x /usr/local/bin/ffmpeg静态包的版本和 glibc 兼容性要先在测试环境验证不能直接上生产。5.4 非 root 用户运行时报权限错误容器默认已经切到 appuser但业务启动脚本可能仍以 root 身份操作某些目录比如写/var/log、/opt/app。报错内容通常是Permission denied。排查思路是检查目标目录的属主和权限。如果挂载了宿主机目录宿主机的 UID 和容器内 appuser 的 UID 不匹配也会导致写不进去。一种常见做法是在启动脚本入口用gosu动态切换用户或者在 K8s 的 securityContext 里设置合适的runAsUser、fsGroup。对于这个基础镜像最稳妥的办法是保证业务应用的可写路径都在/app或/tmp下避免依赖宿主机目录权限。5.5 时区和日志时间差了 8 小时很多镜像不设置时区默认 UTCJava 服务打印的日志和文档里的时间字段就会少 8 小时。这个基础镜像已经通过TZAsia/Shanghai和ln -snf /usr/share/zoneinfo/$TZ /etc/localtime处理好了。如果你复制 Dockerfile 时漏掉了这两行后面排查日志时间会非常痛苦。再补充一点Java 应用里最好同步设置系统属性java -Duser.timezoneAsia/Shanghai -Dfile.encodingUTF-8 -jar app.jar这样 Java 层面的时间和系统时区保持一致避免两个来源互相矛盾。6. 从基础镜像到业务容器的衔接6.1 业务镜像继承基础镜像这个镜像做好以后直接用它作为业务镜像的 FROM 即可。比如你的 Java 服务需要打包成独立镜像FROM devtools:jdk8-ffmpeg-lo COPY --chownappuser:appuser app.jar /app/app.jar USER appuser WORKDIR /app CMD [java, -jar, /app/app.jar]这样业务镜像天然拥有 JDK8、FFmpeg、LibreOffice 的能力。验证时注意基础镜像里 CMD 写的是/bin/bash业务镜像覆盖掉 CMD 即可不影响。6.2 转换命令的正确调用姿势在 Java 代码里调用外部命令推荐使用ProcessBuilder而不建议用Runtime.exec。ProcessBuilder 可以更好地设置工作目录和环境变量。调用 LibreOffice 转换时命令参数尽量固定尤其不要漏掉-env:UserInstallation。下面是一个可参考的 shell 封装soffice --headless \ -env:UserInstallationfile:///tmp/lo_profile \ --convert-to pdf \ --outdir /tmp/output \ $INPUT_FILE需要给--convert-to指定目标格式文件名后缀会由 LibreOffice 自动生成。FFmpeg 调用时如果使用 thumbnail 滤镜注意-frames:v 1参数要放在输出文件前否则可能生成多张或异常。这些细节都是在实操中踩过坑才记住的经验。6.3 维护这套镜像的长期成本我在实际维护过程中最大的体会是基础镜像一旦开始供多个业务使用升级就要极其谨慎。LibreOffice 从 6.4 升到 7.x某些文档的排版结果可能发生变化FFmpeg 升级后编码器默认参数也可能不同。没有经过回归测试就直接升级很容易在线上甩出一堆“之前还能转现在样式乱了”的问题。我自己的做法是在 CI 里固定一组冒烟用例至少包含一个 docx、一个 xlsx、一个 pptx、一个中文 PDF 生成场景以及一个视频抽帧场景。任何基础镜像有变动先跑这组用例通过后才允许发布。镜像体积虽然大但稳定性和可预期性才是基础镜像最核心的价值。每次构建时在镜像标签里带上日期或版本号不要用latest否则后面想回退都不知道该退到哪个版本。