强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 中引用:

![架构图](/image/diagrams/architecture.svg)

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 - 最佳实践 — 工程实践与综合建议。