GraphicsMagick 图像处理完整教程 / 第11章 Docker 与服务化
第11章 Docker 与服务化
11.1 Docker 基础镜像
11.1.1 使用官方镜像
# 拉取官方镜像
docker pull graphicsmagick/graphicsmagick
# 基本使用
docker run --rm -v $(pwd):/work graphicsmagick/graphicsmagick \
gm convert /work/input.jpg -resize 800x600 /work/output.jpg
# 查看版本
docker run --rm graphicsmagick/graphicsmagick gm version
# 查看支持格式
docker run --rm graphicsmagick/graphicsmagick gm convert -list format
11.1.2 自定义 Dockerfile
# Dockerfile — 生产环境 GraphicsMagick 镜像
FROM debian:bookworm-slim
LABEL maintainer="[email protected]"
LABEL description="GraphicsMagick 生产环境镜像"
# 安装依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
graphicsmagick \
ghostscript \
librsvg2-common \
libjpeg62-turbo \
libpng16-16 \
libtiff6 \
libwebp7 \
libfreetype6 \
liblcms2-2 \
fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/*
# 创建工作目录
WORKDIR /work
# 设置环境变量
ENV MAGICK_TMPDIR=/tmp \
MAGICK_MEMORY_LIMIT=2GiB \
MAGICK_MAP_LIMIT=4GiB \
MAGICK_DISK_LIMIT=8GiB \
OMP_NUM_THREADS=4
# 验证安装
RUN gm version && gm convert -list format | wc -l
ENTRYPOINT ["gm"]
CMD ["version"]
构建:
docker build -t gm-prod .
docker run --rm gm-prod convert -list format
11.1.3 带编程语言的镜像
# Dockerfile.python — Python + GraphicsMagick
FROM python:3.11-slim
# 安装 GraphicsMagick
RUN apt-get update && apt-get install -y --no-install-recommends \
graphicsmagick \
libgraphicsmagick++1-dev \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 绑定
RUN pip install --no-cache-dir Wand
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
11.2 批量处理容器
11.2.1 一次性批量处理
# 批量缩放目录中的图片
docker run --rm \
-v $(pwd)/input:/input \
-v $(pwd)/output:/output \
graphicsmagick/graphicsmagick \
sh -c 'for f in /input/*.jpg; do
gm convert "$f" -resize "800x600>" -quality 85 \
"/output/$(basename $f)"
done'
11.2.2 使用 Docker Compose
# docker-compose.yml
version: '3.8'
services:
image-processor:
build: .
volumes:
- ./input:/input:ro
- ./output:/output
environment:
- OMP_NUM_THREADS=4
- MAGICK_MEMORY_LIMIT=2GiB
command: >
sh -c '
for f in /input/*.jpg; do
gm convert "$$f" \
-auto-orient \
-resize "1200x1200>" \
-quality 85 \
-strip \
"/output/$$(basename $$f)"
done
echo "处理完成: $$(ls /output/*.jpg | wc -l) 张图片"
'
# 定时任务
scheduler:
build: .
volumes:
- ./input:/input:ro
- ./output:/output
entrypoint: /bin/sh
command: -c 'echo "0 */6 * * * /app/process.sh" | crontab - && crond -f'
11.2.3 并行批量处理
# Dockerfile.parallel
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
graphicsmagick \
parallel \
&& rm -rf /var/lib/apt/lists/*
COPY process.sh /app/process.sh
RUN chmod +x /app/process.sh
ENTRYPOINT ["/app/process.sh"]
#!/bin/bash
# process.sh
INPUT_DIR="${1:-/input}"
OUTPUT_DIR="${2:-/output}"
JOBS="${3:-$(nproc)}"
mkdir -p "$OUTPUT_DIR"
export -f process_one
process_one() {
local f="$1"
local out="$OUTPUT_DIR/$(basename "$f")"
gm convert "$f" -resize "1200x1200>" -quality 85 -strip "$out" && \
echo "✅ $(basename "$f")" || echo "❌ $(basename "$f")"
}
find "$INPUT_DIR" -name "*.jpg" | parallel -j "$JOBS" process_one {}
echo "处理完成"
11.3 REST API 服务
11.3.1 Flask API 服务
# Dockerfile.api
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
graphicsmagick \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir flask wand gunicorn
WORKDIR /app
COPY api.py .
EXPOSE 5000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "api:app"]
# api.py
import os
import uuid
import tempfile
from flask import Flask, request, jsonify, send_file
from wand.image import Image
app = Flask(__name__)
UPLOAD_DIR = tempfile.mkdtemp()
@app.route('/resize', methods=['POST'])
def resize():
"""缩放图像
参数:
- file: 图像文件
- width: 目标宽度
- height: 目标高度
- quality: JPEG 质量 (默认 85)
"""
if 'file' not in request.files:
return jsonify({'error': 'No file uploaded'}), 400
file = request.files['file']
width = request.form.get('width', 800, type=int)
height = request.form.get('height', 600, type=int)
quality = request.form.get('quality', 85, type=int)
# 保存上传文件
input_path = os.path.join(UPLOAD_DIR, f"{uuid.uuid4()}.jpg")
output_path = os.path.join(UPLOAD_DIR, f"{uuid.uuid4()}.jpg")
file.save(input_path)
try:
with Image(filename=input_path) as img:
img.auto_orient()
img.resize(width, height)
img.compression_quality = quality
img.strip()
img.save(filename=output_path)
return send_file(output_path, mimetype='image/jpeg')
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
os.unlink(input_path)
@app.route('/thumbnail', methods=['POST'])
def thumbnail():
"""生成缩略图(正方形裁剪)"""
if 'file' not in request.files:
return jsonify({'error': 'No file uploaded'}), 400
file = request.files['file']
size = request.form.get('size', 200, type=int)
quality = request.form.get('quality', 85, type=int)
input_path = os.path.join(UPLOAD_DIR, f"{uuid.uuid4()}.jpg")
output_path = os.path.join(UPLOAD_DIR, f"{uuid.uuid4()}.jpg")
file.save(input_path)
try:
with Image(filename=input_path) as img:
img.auto_orient()
# 按短边缩放
w, h = img.width, img.height
if w > h:
img.resize(size, int(h * size / w))
else:
img.resize(int(w * size / h), size)
# 居中裁剪
img.crop(width=size, height=size, gravity='center')
img.compression_quality = quality
img.strip()
img.save(filename=output_path)
return send_file(output_path, mimetype='image/jpeg')
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
os.unlink(input_path)
@app.route('/info', methods=['POST'])
def info():
"""获取图像信息"""
if 'file' not in request.files:
return jsonify({'error': 'No file uploaded'}), 400
file = request.files['file']
input_path = os.path.join(UPLOAD_DIR, f"{uuid.uuid4()}.jpg")
file.save(input_path)
try:
with Image(filename=input_path) as img:
return jsonify({
'width': img.width,
'height': img.height,
'format': img.format,
'depth': img.depth,
'colorspace': str(img.colorspace),
'size_bytes': os.path.getsize(input_path)
})
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
os.unlink(input_path)
@app.route('/health')
def health():
return jsonify({'status': 'ok'})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
11.3.2 使用示例
# 构建并运行
docker build -t gm-api -f Dockerfile.api .
docker run -d -p 5000:5000 --name gm-api gm-api
# 缩放图像
curl -X POST http://localhost:5000/resize \
-F "[email protected]" \
-F "width=800" \
-F "height=600" \
-o resized.jpg
# 生成缩略图
curl -X POST http://localhost:5000/thumbnail \
-F "[email protected]" \
-F "size=200" \
-o thumb.jpg
# 获取图像信息
curl -X POST http://localhost:5000/info \
-F "[email protected]"
11.3.3 Node.js Express API
// server.js
const express = require('express');
const multer = require('multer');
const gm = require('gm').subClass({ imageMagick: false });
const path = require('path');
const fs = require('fs');
const app = express();
const upload = multer({ dest: '/tmp/uploads/' });
app.post('/resize', upload.single('file'), (req, res) => {
const { width = 800, height = 600, quality = 85 } = req.body;
gm(req.file.path)
.autoOrient()
.resize(parseInt(width), parseInt(height))
.quality(parseInt(quality))
.strip()
.stream('jpeg')
.pipe(res);
// 清理上传文件
res.on('finish', () => fs.unlinkSync(req.file.path));
});
app.post('/info', upload.single('file'), (req, res) => {
gm(req.file.path).identify((err, info) => {
fs.unlinkSync(req.file.path);
if (err) return res.status(500).json({ error: err.message });
res.json({
width: info.size.width,
height: info.size.height,
format: info.format,
filesize: info.Filesize
});
});
});
app.listen(3000, () => console.log('Image API running on :3000'));
11.4 微服务架构
11.4.1 完整 Docker Compose 微服务
# docker-compose.microservices.yml
version: '3.8'
services:
# 图像处理 API
api:
build:
context: .
dockerfile: Dockerfile.api
ports:
- "5000:5000"
environment:
- OMP_NUM_THREADS=2
- MAGICK_MEMORY_LIMIT=1GiB
- REDIS_URL=redis://redis:6379
depends_on:
- redis
deploy:
replicas: 3
resources:
limits:
memory: 2G
cpus: '2'
# 后台批量处理 worker
worker:
build:
context: .
dockerfile: Dockerfile.worker
environment:
- OMP_NUM_THREADS=4
- MAGICK_MEMORY_LIMIT=4GiB
- REDIS_URL=redis://redis:6379
depends_on:
- redis
deploy:
replicas: 2
# Redis 队列
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
# 监控面板
dashboard:
image: adminer
ports:
- "8080:8080"
volumes:
redis_data:
11.4.2 Worker 进程
# worker.py
import redis
import json
import os
from wand.image import Image
r = redis.from_url(os.environ.get('REDIS_URL', 'redis://localhost:6379'))
def process_task(task):
task_type = task.get('type')
input_path = task.get('input')
output_path = task.get('output')
if task_type == 'resize':
with Image(filename=input_path) as img:
img.resize(task['width'], task['height'])
img.compression_quality = task.get('quality', 85)
img.strip()
img.save(filename=output_path)
elif task_type == 'thumbnail':
with Image(filename=input_path) as img:
size = task.get('size', 200)
w, h = img.width, img.height
if w > h:
img.resize(size, int(h * size / w))
else:
img.resize(int(w * size / h), size)
img.crop(width=size, height=size, gravity='center')
img.compression_quality = task.get('quality', 85)
img.strip()
img.save(filename=output_path)
elif task_type == 'watermark':
with Image(filename=input_path) as img:
with Image(filename=task['watermark']) as wm:
img.watermark(wm, transparency=0.5, left=20, top=20)
img.save(filename=output_path)
def main():
print("Worker 启动,等待任务...")
while True:
_, task_json = r.brpop('image_tasks')
task = json.loads(task_json)
try:
process_task(task)
r.lpush('completed_tasks', json.dumps({'status': 'ok', **task}))
print(f"✅ 完成: {task['type']} - {task['input']}")
except Exception as e:
r.lpush('completed_tasks', json.dumps({'status': 'error', 'error': str(e), **task}))
print(f"❌ 失败: {task['type']} - {e}")
if __name__ == '__main__':
main()
11.5 CI/CD 集成
11.5.1 GitHub Actions
# .github/workflows/optimize-images.yml
name: Optimize Images
on:
push:
paths:
- 'static/image/**'
jobs:
optimize:
runs-on: ubuntu-latest
container: graphicsmagick/graphicsmagick
steps:
- uses: actions/checkout@v4
- name: Optimize images
run: |
for img in static/image/*.{jpg,jpeg,png}; do
[ -f "$img" ] || continue
echo "优化: $img"
gm mogrify \
-strip \
-quality 85 \
-resize "2000x2000>" \
"$img"
done
- name: Commit optimized images
run: |
git config user.name "Image Optimizer"
git config user.email "[email protected]"
git add -A
git diff --cached --quiet || \
git commit -m "chore: optimize images [skip ci]" && git push
11.5.2 GitLab CI
# .gitlab-ci.yml
image: graphicsmagick/graphicsmagick
optimize-images:
stage: build
script:
- |
for img in public/image/*.{jpg,png}; do
[ -f "$img" ] || continue
gm mogrify -strip -quality 85 -resize "1200x1200>" "$img"
done
only:
changes:
- "public/image/**"
11.6 Kubernetes 部署
11.6.1 部署配置
# k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: gm-api
spec:
replicas: 3
selector:
matchLabels:
app: gm-api
template:
metadata:
labels:
app: gm-api
spec:
containers:
- name: gm-api
image: gm-api:latest
ports:
- containerPort: 5000
env:
- name: OMP_NUM_THREADS
value: "2"
- name: MAGICK_MEMORY_LIMIT
value: "1GiB"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2"
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: gm-api-service
spec:
selector:
app: gm-api
ports:
- port: 80
targetPort: 5000
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: gm-api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: gm-api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
11.7 无服务器 (Serverless)
11.7.1 AWS Lambda
# lambda_function.py
import json
import base64
import tempfile
from wand.image import Image
def lambda_handler(event, context):
# 解码 base64 图像
image_data = base64.b64decode(event['body'])
width = event.get('queryStringParameters', {}).get('width', 800)
height = event.get('queryStringParameters', {}).get('height', 600)
with tempfile.NamedTemporaryFile(suffix='.jpg') as tmp_in, \
tempfile.NamedTemporaryFile(suffix='.jpg') as tmp_out:
tmp_in.write(image_data)
tmp_in.flush()
with Image(filename=tmp_in.name) as img:
img.resize(int(width), int(height))
img.compression_quality = 85
img.strip()
img.save(filename=tmp_out.name)
with open(tmp_out.name, 'rb') as f:
result = f.read()
return {
'statusCode': 200,
'headers': {'Content-Type': 'image/jpeg'},
'body': base64.b64encode(result).decode('utf-8'),
'isBase64Encoded': True
}
11.7.2 Cloudflare Workers(通过容器)
// 使用 container-based workers
export default {
async fetch(request) {
const formData = await request.formData();
const file = formData.get('file');
const width = formData.get('width') || 800;
// 调用容器化 API
const response = await fetch('https://gm-api.internal/resize', {
method: 'POST',
body: formData
});
return new Response(response.body, {
headers: { 'Content-Type': 'image/jpeg' }
});
}
};
11.8 生产环境最佳实践
11.8.1 安全建议
| 要点 | 推荐措施 |
|---|---|
| 文件大小限制 | 最大 10MB-50MB |
| 尺寸限制 | 最大 8000x8000 |
| 格式白名单 | 仅允许 jpg, png, webp, gif |
| 文件名清理 | 重命名为 UUID |
| 临时文件清理 | 处理完立即删除 |
| 超时设置 | 30 秒-60 秒 |
| 内存限制 | 每实例 1-2GB |
| 并发限制 | 根据内存计算 |
11.8.2 监控指标
关键指标:
- 请求处理时间 (P50, P95, P99)
- 内存使用量
- CPU 使用率
- 错误率
- 队列深度 (worker 模式)
- 临时文件空间
11.9 本章小结
| 要点 | 说明 |
|---|---|
| 官方 Docker 镜像可用 | graphicsmagick/graphicsmagick |
| 自定义镜像更灵活 | 选择需要的格式库 |
| REST API 封装是主流 | Flask/Express + Wand/gm |
| 微服务架构适合大规模 | API + Worker + Redis 队列 |
| K8s HPA 自动扩缩 | 基于 CPU 利用率 |
| 安全限制必不可少 | 文件大小、格式、超时 |
扩展阅读
上一章:第10章 编程接口 (API) 下一章:第12章 最佳实践