11 - Docker 与自动化
第 11 章 · Docker 与自动化
11.1 为什么使用 Docker
| 优势 | 说明 |
|---|---|
| 环境一致性 | 避免字体、版本差异 |
| CI/CD 集成 | 流水线中无缝使用 |
| 无需本地安装 | 不污染系统环境 |
| 批量处理 | 容器中批量渲染 |
| 可复现 | Dockerfile 锁定所有依赖 |
11.2 基础 Dockerfile
最小镜像
FROM ubuntu:22.04
# 安装 Graphviz 和中文字体
RUN apt-get update && apt-get install -y \
graphviz \
fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/*
# 工作目录
WORKDIR /data
# 默认入口
ENTRYPOINT ["dot"]
构建和使用
# 构建镜像
docker build -t graphviz .
# 渲染单个文件
docker run --rm -v $(pwd):/data graphviz -Tpng input.dot -o output.png
# 渲染 SVG
docker run --rm -v $(pwd):/data graphviz -Tsvg input.dot -o output.svg
# 渲染 PDF
docker run --rm -v $(pwd):/data graphviz -Tpdf input.dot -o output.pdf
11.3 完整 Dockerfile(含多种工具)
FROM ubuntu:22.04
# 避免交互式安装
ENV DEBIAN_FRONTEND=noninteractive
# 安装 Graphviz 及依赖
RUN apt-get update && apt-get install -y \
graphviz \
graphviz-dev \
fonts-noto-cjk \
fonts-noto-cjk-extra \
fonts-wqy-zenhei \
librsvg2-dev \
libcairo2-dev \
libpango1.0-dev \
poppler-utils \
python3 \
python3-pip \
ghostscript \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 包
RUN pip3 install --no-cache-dir graphviz pydot
# 字体缓存更新
RUN fc-cache -fv
# 验证安装
RUN dot -V && fc-list :lang=zh
WORKDIR /data
# 多种工具入口
ENTRYPOINT ["dot"]
CMD ["-Tsvg"]
11.4 批量渲染脚本
Shell 批量渲染
#!/bin/bash
# render_all.sh — 批量渲染 DOT 文件
INPUT_DIR="${1:-.}"
OUTPUT_DIR="${2:-output}"
FORMAT="${3:-svg}"
mkdir -p "$OUTPUT_DIR"
echo "输入目录: $INPUT_DIR"
echo "输出目录: $OUTPUT_DIR"
echo "输出格式: $FORMAT"
echo "---"
count=0
errors=0
for dotfile in "$INPUT_DIR"/*.dot; do
[ -f "$dotfile" ] || continue
basename=$(basename "$dotfile" .dot)
output="$OUTPUT_DIR/${basename}.${FORMAT}"
echo -n "渲染 $dotfile -> $output ... "
if dot -T"$FORMAT" "$dotfile" -o "$output" 2>/dev/null; then
echo "✓"
((count++))
else
echo "✗"
((errors++))
fi
done
echo "---"
echo "成功: $count 个, 失败: $errors 个"
Docker 中批量渲染
# 挂载目录并批量渲染
docker run --rm \
-v $(pwd)/diagrams:/data/input:ro \
-v $(pwd)/output:/data/output \
graphviz bash -c '
mkdir -p /data/output
for f in /data/input/*.dot; do
name=$(basename "$f" .dot)
echo "Rendering: $name"
dot -Tsvg "$f" -o "/data/output/${name}.svg"
done
echo "Done!"
'
Python 批量渲染
#!/usr/bin/env python3
"""batch_render.py — 批量渲染 DOT 文件"""
import os
import sys
import subprocess
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
def render_dot(dot_path: Path, output_dir: Path, fmt: str = 'svg') -> bool:
"""渲染单个 DOT 文件"""
output_path = output_dir / f"{dot_path.stem}.{fmt}"
try:
subprocess.run(
['dot', f'-T{fmt}', str(dot_path), '-o', str(output_path)],
check=True,
capture_output=True,
text=True
)
print(f" ✓ {dot_path.name} -> {output_path.name}")
return True
except subprocess.CalledProcessError as e:
print(f" ✗ {dot_path.name}: {e.stderr.strip()}")
return False
def batch_render(input_dir: str, output_dir: str, fmt: str = 'svg', workers: int = 4):
"""批量渲染"""
input_path = Path(input_dir)
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
dot_files = list(input_path.glob('*.dot'))
print(f"找到 {len(dot_files)} 个 DOT 文件")
print(f"输出格式: {fmt}")
print(f"并行数: {workers}")
print("---")
success = 0
failed = 0
with ThreadPoolExecutor(max_workers=workers) as executor:
futures = {
executor.submit(render_dot, f, output_path, fmt): f
for f in dot_files
}
for future in futures:
if future.result():
success += 1
else:
failed += 1
print("---")
print(f"成功: {success}, 失败: {failed}")
if __name__ == '__main__':
input_dir = sys.argv[1] if len(sys.argv) > 1 else '.'
output_dir = sys.argv[2] if len(sys.argv) > 2 else 'output'
fmt = sys.argv[3] if len(sys.argv) > 3 else 'svg'
batch_render(input_dir, output_dir, fmt)
11.5 PDF 输出
直接 PDF 输出
# 需要 Cairo 和 Pango 支持
dot -Tpdf input.dot -o output.pdf
多页 PDF
# 使用 ps2pdf 或 ghostscript 合并
dot -Tps input1.dot -o page1.ps
dot -Tps input2.dot -o page2.ps
ps2pdf page1.ps page2.ps output.pdf
LaTeX 集成
# 生成 EPS(LaTeX 传统格式)
dot -Teps diagram.dot -o diagram.eps
# 生成 PDF(推荐 pdflatex)
dot -Tpdf diagram.dot -o diagram.pdf
% LaTeX 中嵌入 Graphviz 输出
\documentclass{article}
\usepackage{graphicx}
\begin{document}
\begin{figure}[htbp]
\centering
\includegraphics[width=0.8\textwidth]{diagram.pdf}
\caption{系统架构图}
\label{fig:architecture}
\end{figure}
\end{document}
11.6 CI/CD 集成
GitHub Actions
# .github/workflows/graphviz.yml
name: Build Graphviz Diagrams
on:
push:
paths:
- 'docs/diagrams/**'
jobs:
render:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Graphviz
run: |
sudo apt-get update
sudo apt-get install -y graphviz fonts-noto-cjk
- name: Render diagrams
run: |
mkdir -p docs/images
for f in docs/diagrams/*.dot; do
name=$(basename "$f" .dot)
dot -Tsvg "$f" -o "docs/images/${name}.svg"
dot -Tpng "$f" -Gdpi=150 -o "docs/images/${name}.png"
done
- name: Commit rendered images
run: |
git config user.name "GitHub Actions"
git config user.email "[email protected]"
git add docs/images/
git diff --cached --quiet || git commit -m "chore: update rendered diagrams"
git push
GitLab CI
# .gitlab-ci.yml
render-diagrams:
image: ubuntu:22.04
before_script:
- apt-get update && apt-get install -y graphviz fonts-noto-cjk
script:
- mkdir -p public/diagrams
- for f in diagrams/*.dot; do
name=$(basename "$f" .dot);
dot -Tsvg "$f" -o "public/diagrams/${name}.svg";
done
artifacts:
paths:
- public/diagrams/
Jenkins Pipeline
pipeline {
agent any
stages {
stage('Render Graphviz') {
steps {
sh '''
docker run --rm \
-v $WORKSPACE/diagrams:/data/input:ro \
-v $WORKSPACE/output:/data/output \
graphviz bash -c '
for f in /data/input/*.dot; do
name=$(basename "$f" .dot)
dot -Tsvg "$f" -o "/data/output/${name}.svg"
done
'
'''
}
}
}
post {
always {
archiveArtifacts artifacts: 'output/*.svg'
}
}
}
11.7 服务化部署
Web API 服务
#!/usr/bin/env python3
"""graphviz_server.py — Graphviz Web 渲染服务"""
from flask import Flask, request, jsonify, send_file
import subprocess
import tempfile
import os
app = Flask(__name__)
@app.route('/render', methods=['POST'])
def render():
"""POST DOT 代码,返回渲染结果"""
data = request.json
dot_code = data.get('dot', '')
fmt = data.get('format', 'svg')
if not dot_code:
return jsonify({'error': 'Missing dot code'}), 400
supported_formats = ['svg', 'png', 'pdf', 'jpg']
if fmt not in supported_formats:
return jsonify({'error': f'Unsupported format: {fmt}'}), 400
with tempfile.NamedTemporaryFile(mode='w', suffix='.dot', delete=False) as f:
f.write(dot_code)
dot_path = f.name
output_path = dot_path.replace('.dot', f'.{fmt}')
try:
result = subprocess.run(
['dot', f'-T{fmt}', dot_path, '-o', output_path],
capture_output=True, text=True, timeout=30
)
if result.returncode != 0:
return jsonify({'error': result.stderr}), 400
return send_file(output_path, mimetype=f'image/{fmt}')
except subprocess.TimeoutExpired:
return jsonify({'error': 'Rendering timeout'}), 504
finally:
os.unlink(dot_path)
if os.path.exists(output_path):
os.unlink(output_path)
@app.route('/health')
def health():
return jsonify({'status': 'ok'})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Docker 化 Web 服务
FROM python:3.11-slim
RUN apt-get update && apt-get install -y \
graphviz fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/*
RUN pip install flask gunicorn
WORKDIR /app
COPY graphviz_server.py .
EXPOSE 5000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "graphviz_server:app"]
# 构建和运行
docker build -t graphviz-api .
docker run -p 5000:5000 graphviz-api
# 调用 API
curl -X POST http://localhost:5000/render \
-H "Content-Type: application/json" \
-d '{"dot": "digraph { A -> B -> C }", "format": "svg"}' \
-o output.svg
11.8 Hugo 集成
自动渲染 Hugo 中的 DOT 文件
#!/bin/bash
# render_hugo_dot.sh — Hugo 构建前自动渲染 DOT 文件
CONTENT_DIR="content"
STATIC_DIR="static/image/diagrams"
mkdir -p "$STATIC_DIR"
find "$CONTENT_DIR" -name "*.dot" | while read dotfile; do
name=$(basename "$dotfile" .dot)
relpath=$(dirname "$dotfile" | sed "s|$CONTENT_DIR/||")
outdir="$STATIC_DIR/$relpath"
mkdir -p "$outdir"
echo "Rendering: $dotfile"
dot -Tsvg "$dotfile" -o "$outdir/${name}.svg"
dot -Tpng "$dotfile" -Gdpi=150 -o "$outdir/${name}.png"
done
echo "Done! Diagrams rendered to $STATIC_DIR"
在 Markdown 中引用:

11.9 Makefile 自动化
# Makefile — Graphviz 自动化构建
DIAGRAMS_DIR = diagrams
OUTPUT_DIR = output
FORMATS = svg png pdf
DOT_FILES = $(wildcard $(DIAGRAMS_DIR)/*.dot)
SVG_FILES = $(patsubst $(DIAGRAMS_DIR)/%.dot,$(OUTPUT_DIR)/%.svg,$(DOT_FILES))
PNG_FILES = $(patsubst $(DIAGRAMS_DIR)/%.dot,$(OUTPUT_DIR)/%.png,$(DOT_FILES))
.PHONY: all clean svg png pdf docker
all: svg png
svg: $(SVG_FILES)
png: $(PNG_FILES)
$(OUTPUT_DIR)/%.svg: $(DIAGRAMS_DIR)/%.dot | $(OUTPUT_DIR)
dot -Tsvg $< -o $@
$(OUTPUT_DIR)/%.png: $(DIAGRAMS_DIR)/%.dot | $(OUTPUT_DIR)
dot -Tpng -Gdpi=150 $< -o $@
pdf: $(patsubst $(DIAGRAMS_DIR)/%.dot,$(OUTPUT_DIR)/%.pdf,$(DOT_FILES))
$(OUTPUT_DIR)/%.pdf: $(DIAGRAMS_DIR)/%.dot | $(OUTPUT_DIR)
dot -Tpdf $< -o $@
$(OUTPUT_DIR):
mkdir -p $(OUTPUT_DIR)
docker:
docker run --rm \
-v $(CURDIR)/$(DIAGRAMS_DIR):/data/input:ro \
-v $(CURDIR)/$(OUTPUT_DIR):/data/output \
graphviz bash -c 'for f in /data/input/*.dot; do \
name=$$(basename "$$f" .dot); \
dot -Tsvg "$$f" -o "/data/output/$${name}.svg"; \
dot -Tpng -Gdpi=150 "$$f" -o "/data/output/$${name}.png"; \
done'
clean:
rm -rf $(OUTPUT_DIR)
注意事项
⚠️ 中文字体:Docker 镜像中必须安装中文字体,否则中文标签显示为方块。
⚠️ 字体缓存:安装字体后运行
fc-cache -fv更新缓存。
⚠️ 文件权限:Docker 挂载目录时注意文件权限(
-u $(id -u):$(id -g))。
⚠️ PDF 依赖:PDF 输出需要 Cairo 和 Pango 库。
⚠️ 超时处理:Web 服务中需设置渲染超时,防止恶意输入导致挂起。
⚠️ 安全:Web 服务不应直接执行用户输入的 DOT 代码,需进行沙箱隔离。
扩展阅读
下一章:12 - 最佳实践 — 工程实践与综合建议。