Libravatar 2.0 配置参数详解
讲透 ivatar 2.0 配置体系:三层配置加载顺序、config.py 全参数默认值与含义,WebP 内容协商、Gravatar 代理、ALLOWED_HOSTS、尺寸钳制逐项生效实测与排障实录。
Libravatar 2.0 配置参数详解
目标:把 Libravatar 服务端(ivatar 2.0)的配置体系讲透——三层配置文件的加载与覆盖顺序、config.py 中全部业务参数的默认值与含义,以及关键开关(Gravatar 代理、WebP 内容协商、ALLOWED_HOSTS、尺寸钳制)的逐项生效实测。本文所有"实测"均来自真实 Ubuntu 22.04 服务器上部署的 ivatar 2.0 实例(部署过程见《Ubuntu 22.04 安装 Libravatar 2.0 指南》,协议与联邦机制背景见《Libravatar 2.0 介绍与选型指南》),命令与回显逐行照抄。
一、环境信息
| 项目 | 取值 |
|---|---|
| 操作系统 | Ubuntu 22.04.5 LTS(x86_64) |
| 服务端实现 | ivatar 2.0(源码 commit 6f4b7e0,2026-09-25,master 分支) |
| 部署路径 | /opt/ivatar(虚拟环境 /opt/ivatar/.virtualenv) |
| Python | 3.10.12(系统) |
| Django | 5.2.18(requirements 声明 >= 4.2.16,实测安装 5.2.18) |
| 数据库 | PostgreSQL 14.24(psycopg 3.3.6) |
| 应用服务 | gunicorn 26.2.0,WSGI,监听 127.0.0.1:8000,systemd 单元 ivatar.service |
| 反向代理 | nginx 1.18.0(Ubuntu),监听 8080 |
| 缓存 | memcached 1.6.14(127.0.0.1:11211) |
| 验证方法 | manage.py diffsettings / curl 实测响应头与响应体 |
二、配置体系总览
2.1 三层配置链:settings.py → config.py → config_local.py
ivatar 是标准 Django 工程外加一层"可覆盖配置",加载顺序固定为三段:
ivatar/settings.py:Django 工程原生设置,随源码安装,不要修改;/opt/ivatar/config.py:随源码分发的"官方推荐默认值",settings.py 在定义完自身变量后执行from config import *导入它(注释原文:「These are modified here because settings.py does 'from config import *' AFTER defining them」);/opt/ivatar/config_local.py:运维自定义覆盖层,config.py 的最后一行按需加载它:
# This MUST BE THE LAST!
if os.path.isfile(os.path.join(BASE_DIR, "config_local.py")):
from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover
由此得出两条铁律:
- 只改 config_local.py,永远不改 config.py——config.py 会被升级覆盖,config_local.py 不会;
- config_local.py 中显式赋值的优先级高于 config.py 中
os.environ.get(...)读到的环境变量(因为 config_local 在 import 时最后执行)。
仓库中自带参考模板 config_local.py.example,起步方式即复制改名:
# cd /opt/ivatar
# cp config_local.py.example config_local.py
2.2 如何查看"当前生效值":diffsettings
Django 内置的 manage.py diffsettings 打印所有与 Django 全局默认值不同的配置项,行尾 ### 是它自己的标记(表示"这一行与默认不同"),不是配置内容。本部署实例的输出(节选,重点行):
# cd /opt/ivatar
# .virtualenv/bin/python manage.py diffsettings
ALLOWED_HOSTS = ['*']
BASE_DIR = '/opt/ivatar' ###
BASE_URL = 'http://avatars.example.com/avatar/' ###
CACHES = {'default': {'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', 'LOCATION': ['127.0.0.1:11211']}, 'filesystem': {'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 'LOCATION': '/var/tmp/ivatar_cache', 'TIMEOUT': 86400, 'OPTIONS': {'MAX_ENTRIES': 1000000}}}
DATABASES = {'default': {'ENGINE': 'django.db.backends.postgresql', 'NAME': 'ivatar', 'USER': 'ivatar_user', 'PASSWORD': 'q1w2e3r4', 'HOST': '127.0.0.1', 'PORT': '5432', ...}}
DEFAULT_GRAVATARPROXY = False ###
ENABLE_WEBP_CONTENT_NEGOTIATION = True ###
IVATAR_VERSION = '2.0' ###
LOGS_DIR = '/var/log/ivatar' ###
PREGENERATE_PHOTO_SIZES = (28, 48, 80, 128, 256) ###
SECRET_KEY = 'fZ7_O6oQrGiCVw-oABdO_HnIDajX1FpUP9uQ80O6Z9CniAMMkA1U0ZgGkni5lCrIuGA'
SECURE_BASE_URL = 'https://avatars.example.com/avatar/' ###
SITE_NAME = 'libravatar' ###
STATIC_ROOT = '/opt/ivatar/static'
USE_X_FORWARDED_HOST = True
(其余输出略,diffsettings 全量约 90 行)
说明:
ALLOWED_HOSTS/SECRET_KEY/DATABASES/USE_X_FORWARDED_HOST/STATIC_ROOT等行没有###标记,是因为它们虽然被本项目赋值,但值恰好与 Django 全局默认形态一致或由 settings.py 原生定义;带###的行是 ivatar 特有或与默认不同的项。判断"我的覆盖是否生效",直接在输出里找你的参数名即可,不必纠结标记。
三、配置参数详解
以下参数全部取自 /opt/ivatar/config.py(ivatar 2.0,commit 6f4b7e0),"本部署值"列为 /opt/ivatar/config_local.py 的实际覆盖值;"生效实测"统一见第四章。
3.1 站点标识与对外地址
| 参数 | config.py 默认 | 本部署值 | 说明 |
|---|---|---|---|
SITE_NAME | os.environ.get("SITE_NAME", "libravatar") | 未覆盖(libravatar) | 站点名,用于页面标题等展示 |
IVATAR_VERSION | "2.0" | 未覆盖 | 版本字符串,/version/ 端点的 application_version 字段即来源于此 |
SCHEMAROOT | "https://www.libravatar.org/schemas/export/0.2" | 未覆盖 | 导出 XML 的 schema 地址 |
SECURE_BASE_URL | os.environ.get("SECURE_BASE_URL", "https://avatars.linux-kernel.at/avatar/") | https://avatars.example.com/avatar/ | HTTPS 对外头像基础 URL,页面/邮件里拼头像链接用 |
BASE_URL | os.environ.get("BASE_URL", "http://avatars.linux-kernel.at/avatar/") | http://avatars.example.com/avatar/ | HTTP 对外头像基础 URL |
LOGIN_REDIRECT_URL | reverse_lazy("profile") | 未覆盖 | 登录后跳转到个人资料页 |
注意 SECURE_BASE_URL/BASE_URL 是"展示与委派用"的声明值,不改变本机路由——对外服务的实际入口由 nginx 监听端口决定。另有一个容易困惑的现象:/stats/ 端点里 top_viewed_avatars 的 avatar_url 是请求发生时落库的历史值,改了 BASE_URL 后旧记录仍显示旧地址(实测确认,重启与清缓存都不回写), diffsettings 才是验证配置生效的可靠手段。
3.2 数据库(DATABASES)
config.py 的数据库选择逻辑(按优先级从上到下,先命中先生效):
try:
from ivatar.settings import DATABASES
except ImportError:
DATABASES = []
if "default" not in DATABASES:
DATABASES["default"] = {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
if "MYSQL_DATABASE" in os.environ: # MySQL(编排部署风格变量)
DATABASES["default"] = { ... }
if "POSTGRESQL_DATABASE" in os.environ: # PostgreSQL(编排部署风格变量)
DATABASES["default"] = { ... }
if "POSTGRES_DB" in os.environ: # PostgreSQL(CI/CD 风格变量)
DATABASES["default"] = { ... }
即:什么变量都不给,默认落到 BASE_DIR 下的 SQLite(db.sqlite3);裸机生产建议显式在 config_local.py 里写死 PostgreSQL:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "ivatar",
"USER": "ivatar_user",
"PASSWORD": "q1w2e3r4",
"HOST": "127.0.0.1",
"PORT": "5432",
}
}
diffsettings 实测确认生效(见 2.2,ENGINE 已是 postgresql)。官方支持矩阵:SQLite(仅开发)/ MySQL-MariaDB / PostgreSQL;PostgreSQL 走 psycopg 3(实测 3.3.6)。
3.3 缓存体系
| 参数 | 默认值 | 说明 |
|---|---|---|
CACHES["default"] | memcached PyMemcacheCache,127.0.0.1:11211 | 主缓存(头像响应 page cache 等)。未启动 memcached 时头像接口会降级报错,安装篇 FAQ 有实测 |
CACHES["filesystem"] | 文件缓存 /var/tmp/ivatar_cache,TTL 86400,MAX_ENTRIES 1000000 | 第二层文件缓存 |
CACHE_RESPONSE | True | 开启对头像响应的整页缓存 |
PHOTO_CACHE_TIMEOUT | 604800(7 天) | 用户照片响应缓存 TTL |
MAX_USER_PHOTO_CACHE_SIZE | 500 | 用户照片内存缓存条目上限 |
STATS_CACHE_TIMEOUT | 3600 | /stats/ 端点缓存 TTL(该端点未命中时要全表扫描照片表,务必保留缓存) |
BLUESKY_CACHE_TIMEOUT | 3600 | Bluesky 代理载荷缓存 TTL |
实测提醒:PHOTO_CACHE_TIMEOUT 意味着改完配置切换开关后,同一个哈希的旧响应可能仍从缓存返回——排障时先用 manage.py clear_cache 清缓存,或换一个全新邮箱哈希验证(本文所有两态实测均换用新哈希,见第四章)。
3.4 头像业务参数
| 参数 | 默认值 | 说明 |
|---|---|---|
DEFAULT_AVATAR_SIZE | 80 | 未带 s= 参数时的默认边长(px) |
AVATAR_MAX_SIZE | 512 | s= 参数的上限,超限被钳制到 512 |
MAX_PHOTO_SIZE | 10485760(10 MB,注释原文 "in bytes") | 用户上传照片体积上限 |
MAX_PIXELS | 7000 | 单边最大像素(7000 px,防止解压炸弹) |
JPEG_QUALITY | 85 | JPEG 重编码质量 |
MAX_NUM_PHOTOS | 5 | 每用户最多照片数 |
MAX_NUM_UNCONFIRMED_EMAILS | 5 | 待确认邮箱上限 |
MAX_LENGTH_EMAIL / MIN_LENGTH_EMAIL | 254 / 6 | 邮箱长度校验 |
MAX_LENGTH_URL / MIN_LENGTH_URL | 255 / 11 | OpenID URL 长度校验 |
PREGENERATE_PHOTO_SIZES | (28, 48, 80, 128, 256) | 保存照片时预生成的缩略图档位 |
PREGENERATE_THUMBNAILS_ON_SAVE | True | 保存即同步预生成(关掉则首次请求时现算,慢) |
PREGENERATE_THUMBNAILS_ASYNC | False | 预生成同步执行 |
PREGENERATE_WEBP_THUMBNAILS | True | 预生成 WebP 版本缩略图 |
尺寸钳制实测(管理员账号头像,md5 = e64c7d89f26bd1972efa854d13d7dd61):
$ curl -s -o /dev/null -w "s=512: %{size_download}B\n" "http://avatars.example.com:8080/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=512"
s=512: 1880B
$ curl -s -o /dev/null -w "s=600: %{size_download}B\n" "http://avatars.example.com:8080/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=600"
s=600: 1880B
$ curl -s -o /dev/null -w "s=1: %{size_download}B\n" "http://avatars.example.com:8080/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=1"
s=1: 69B
s=600 与 s=512 字节数完全一致(1880B),证实 AVATAR_MAX_SIZE = 512 把超限请求钳制到了 512px;s=1 正常返回 1px 图(69B)。
3.5 WebP 内容协商
| 参数 | 默认值 | 说明 |
|---|---|---|
ENABLE_WEBP_CONTENT_NEGOTIATION | True | 开启后,客户端 Accept 头带 image/webp 且请求的是用户上传照片时,返回 WebP |
WEBP_QUALITY | 85 | WebP 编码质量 |
两态实测(alice@example.com 的照片,md5 = c160f8cc69a4f0bf2b0362752353d060;bob@example.com,md5 = 4b9bb80620f03eb3719e0a061c14283d):
开启(默认态)时——
$ curl -s -o /dev/null -w "%{http_code} %{content_type} %{size_download}B" -H "Accept: image/webp,image/*" "http://avatars.example.com:8080/avatar/c160f8cc69a4f0bf2b0362752353d060"
200 image/webp 96B
关闭(config_local.py 设 ENABLE_WEBP_CONTENT_NEGOTIATION = False 并重启服务后,用全新哈希验证)——同样请求只返回 image/png。
两个边界结论(实测):
- 默认头像路径不受该开关影响:
?d=retro等生成型默认头像即便带Accept: image/webp也恒为 PNG——200 image/png 1267B; - PNG 用户照片(96B WebP vs 177B PNG)在该样例上 WebP 更小,但对已有小 PNG 收益有限,属正常现象。
3.6 Gravatar 联动开关
| 参数 | 默认值 | 说明 |
|---|---|---|
DEFAULT_GRAVATARPROXY | True | 本库未命中头像时,代理 Gravatar 的结果(用户感知不到 Gravatar 存在) |
DEFAULT_GRAVATARREDIRECT | False | 本库未命中时 302 重定向到 Gravatar(暴露上游) |
FORCEDEFAULT | False | 全站强制走默认头像(等同所有请求带 f=y) |
两态实测(全新哈希 00112233445566778899aabbccddeeff,本库无此邮箱头像):
关闭时(DEFAULT_GRAVATARPROXY = False)——直接走本站默认头像,302 到 nobody 静态图:
$ curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}" "http://avatars.example.com:8080/avatar/00112233445566778899aabbccddeeff"
302 -> http://avatars.example.com:8080/static/img/nobody/80.png
开启时(DEFAULT_GRAVATARPROXY = True,重启 ivatar 服务后)——先 302 到站内 gravatarproxy 视图:
$ curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}" "http://avatars.example.com:8080/avatar/00112233445566778899aabbccddeeff"
302 -> http://avatars.example.com:8080/gravatarproxy/00112233445566778899aabbccddeeff?s=80
gravatarproxy 视图再去抓 Gravatar——服务器没有公网出口时这一步失败,实测返回 500:
$ curl -s -o /dev/null -w "%{http_code} %{content_type} %{size_download}B" "http://avatars.example.com:8080/gravatarproxy/00112233445566778899aabbccddeeff"
500 text/html; charset=utf-8 145B
journalctl 中的真实异常(排障实录,见第五章):
# journalctl -u ivatar --no-pager -n 4
Oct 28 13:52:07 ivatar-server gunicorn[8794]: File "/usr/lib/python3.10/urllib/request.py", line 1351, in do_open
Oct 28 13:52:07 ivatar-server gunicorn[8794]: raise URLError(err)
Oct 28 13:52:07 ivatar-server gunicorn[8794]: urllib.error.URLError: <urlopen error [Errno 101] Network is unreachable>
内部网络部署务必保持 DEFAULT_GRAVATARPROXY = False(或给服务器配好公网出口),否则所有未命中头像都是 500。
3.7 安全与上传
| 参数 | 默认值 | 说明 |
|---|---|---|
DEBUG | settings.py 开发默认 True,生产必须显式 False | 本部署 False(实测:出错页只给 "Server Error (500)" 空白页,不带堆栈) |
SECRET_KEY | 无默认,必须生成 | 会话/CSRF 签名密钥,用 manage.py generate_secret_key 生成后写入 config_local.py |
ALLOWED_HOSTS | ["*"] | 允许的 Host 头列表。config.py 默认 * 便于起步,生产建议收紧(实测见第四章) |
USE_X_FORWARDED_HOST | True | 从 X-Forwarded-Host 取 Host(nginx 反代场景必须) |
TRUSTED_DEFAULT_URLS | ui-avatars.com、gravatar.com、dicebear.com 等 11 条 | d= 指向站外 URL 时的防开放重定向白名单(CWE-601 防护),支持 host_equals/host_suffix/path_prefix 组合 |
URL_TIMEOUT | 10 | 抓取外部 URL(Gravatar/Bluesky 等)的超时秒数 |
FILE_UPLOAD_MAX_MEMORY_SIZE | 5 * 1024 * 1024(5 MB) | 超过则落盘临时文件 |
DATA_UPLOAD_MAX_MEMORY_SIZE | 5 * 1024 * 1024 | 请求体上限 |
FILE_UPLOAD_PERMISSIONS | 0o644 | 上传文件权限 |
ENABLE_FILE_SECURITY_VALIDATION | True | 上传文件安全校验(魔数验证,依赖系统 libmagic) |
ENABLE_EXIF_SANITIZATION | True | 上传时清洗 EXIF(防照片泄露 GPS 等元数据) |
ENABLE_MALICIOUS_CONTENT_SCAN | True | 恶意内容扫描 |
3.8 认证相关
| 参数 | 默认值 | 说明 |
|---|---|---|
AUTHENTICATION_BACKENDS | OpenID 后端 + Fedora OIDC 后端 + ModelBackend | 三后端叠加;config.py 注释保留 django_auth_ldap.backend.LDAPBackend 开关(启用需装 LDAP 依赖,见安装篇) |
OPENID_CREATE_USERS | True | OpenID 首次登录自动建用户 |
OPENID_UPDATE_DETAILS_FROM_SREG | True | 从 OpenID sreg 拉取昵称邮箱 |
SOCIAL_AUTH_FEDORA_KEY / SOCIAL_AUTH_FEDORA_SECRET | None | Fedora OIDC 客户端凭据(不用 Fedora 登录可不管) |
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS | ["avatars.linux-kernel.at", "localhost"] | OpenID 回跳域名白名单,自部署要加上自己的域名 |
ADMIN_USERS | [] | 特权用户名列表 |
TRUST_EMAIL_FROM_SOCIAL_AUTH_BACKENDS | ["fedora"] | 信任哪些社交后端声明的邮箱 |
SOCIAL_AUTH_JSONFIELD_ENABLED | True | social_django JSON 字段 |
3.9 Bluesky 代理
| 参数 | 默认值 | 说明 |
|---|---|---|
BLUESKY_API_TIMEOUT | 5 | Bluesky profile/session API 超时(秒) |
BLUESKY_IMAGE_TIMEOUT | 5 | 头像 CDN 下载超时(秒) |
BLUESKY_CACHE_TIMEOUT | 3600 | 代理载荷缓存 TTL(秒) |
BLUESKY_IDENTIFIER / BLUESKY_APP_PASSWORD | os.environ.get(..., None) | App Password 凭据,走环境变量传入 |
3.10 性能与可观测
| 参数 | 默认值 | 说明 |
|---|---|---|
STATS_BATCH_SIZE | 100 | 访问计数批量落库大小 |
ASYNC_ACCESS_COUNT | True | 访问计数异步更新 |
ROBOHASH_OPTIMIZATION_ENABLED | True | Robohash 优化实现(注释称 6–22x 性能提升) |
ROBOHASH_CACHE_SIZE | 150 | 机器人部件内存缓存条数(约 10–30 MB) |
ROBOHASH_UPSCALE_CACHE_SIZE | 24 | 1024×1024 上采样部件 LRU 上限 |
PAGAN_CACHE_SIZE | 1000 | pagan(PGP 风格头像)缓存条数 |
LOGS_DIR | 无默认(注释示例 /var/log/ivatar) | 日志目录声明;注意 Django 的 LOGGING 文件处理器实际写 /opt/ivatar/logs/ivatar.log(settings.py 定义),两者不要混为一谈 |
| OpenTelemetry | 默认不导出 | 装了 instrumentation 但未配导出器,manage.py 每次都会打印提示(见 FAQ) |
3.11 界面与语言
| 参数 | 默认值 | 说明 |
|---|---|---|
LANGUAGES | 德/英/加泰罗尼亚/捷克/西/巴斯克/法/意/日/荷/葡/俄/阿尔巴尼亚/土/乌克兰 15 语 | 界面语言集合,配合 CustomLocaleMiddleware 按 Accept-Language 协商 |
BOOTSTRAP4 | 本地静态资源(/static/css/bootstrap.min.css 等,带 SRI integrity) | Bootstrap4 走本地文件,不引外网 CDN |
MESSAGE_TAGS | debug/info/success/warning→danger | 站内消息样式映射 |
四、参数生效实测
4.1 验证方法论
- 改 /opt/ivatar/config_local.py;
systemctl restart ivatar(gunicorn 不会热加载配置,必须重启);manage.py diffsettings确认生效值;- curl 用全新邮箱哈希做行为验证(避开 PHOTO_CACHE_TIMEOUT=7 天的响应缓存)。
4.2 开关两态实测汇总
| 参数 | 状态 | 实测命令要点 | 实测结果 |
|---|---|---|---|
ENABLE_WEBP_CONTENT_NEGOTIATION | True(默认) | curl -H "Accept: image/webp,image/*" 用户照片哈希 | 200 image/webp 96B |
| 同上 | False | 同命令(另一哈希) | 200 image/png |
DEFAULT_GRAVATARPROXY | False | 裸哈希(本库无头像) | 302 -> /static/img/nobody/80.png |
| 同上 | True | 同命令 | 302 -> /gravatarproxy/<hash>?s=80,内网无出口再 500 |
ALLOWED_HOSTS | ["*"] | 任意路径 | 正常 200 |
| 同上 | ["avatars.example.com"] | curl http://localhost:8080/version/(Host 不匹配) | HTTP/1.1 400 Bad Request,页面 Bad Request (400) |
AVATAR_MAX_SIZE | 512(默认) | s=600 与 s=512 对比 | 字节数同为 1880B,超限被钳制 |
ALLOWED_HOSTS 误配的完整实测(收紧后用不匹配的 Host 访问):
# 把 config_local.py 的 ALLOWED_HOSTS 改为 ["avatars.example.com"] 并重启
$ curl -s -i "http://localhost:8080/version/" | head -6
HTTP/1.1 400 Bad Request
Server: nginx/1.18.0 (Ubuntu)
Date: Wed, 28 Oct 2026 13:52:37 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
$ curl -s "http://localhost:8080/version/"
<!doctype html>
<html lang="en">
<head>
<title>Bad Request (400)</title>
</head>
<body>
<h1>Bad Request (400)</h1><p></p>
</body>
</html>
改回 ALLOWED_HOSTS = ["*"](或加上实际访问域名)并重启后恢复 200。注意 nginx 原样透传了 Host 头(配置里有 proxy_set_header Host $host),所以 Django 看到的 Host 就是浏览器/客户端发的原始值。
4.3 SECURE_BASE_URL 覆盖验证
config_local.py 写入后:
SECURE_BASE_URL = "https://avatars.example.com/avatar/"
BASE_URL = "http://avatars.example.com/avatar/"
重启后 diffsettings 显示:
# .virtualenv/bin/python manage.py diffsettings | grep BASE_URL
BASE_URL = 'http://avatars.example.com/avatar/' ###
SECURE_BASE_URL = 'https://avatars.example.com/avatar/' ###
/version/ 端点回显(确认 IVATAR_VERSION=2.0 在线上生效):
$ curl -s "http://localhost:8080/version/"
{"commit_hash": "6f4b7e01cb2a6be57fea63aeb9fc95414b2d2037", "short_hash": "6f4b7e0", "branch": "master", "commit_date": "2026-10-28 13:13:16 ", "deployment_date": "2026-10-28 13:13:16 ", "deployment_status": "active", "version": "master-6f4b7e0", "application_version": "2.0"}
五、排障实录
5.1 改了 config_local.py 不生效
现象:开关改完 curl 行为不变。
原因:gunicorn 是常驻进程,Django 配置只在进程启动时加载一次,systemctl restart ivatar 前改动全部无效。
解决:systemctl restart ivatar 后再验证。
5.2 清了配置、缓存还在——两态实验"失效"
现象:把 ENABLE_WEBP_CONTENT_NEGOTIATION 从 True 改 False 后,老哈希请求仍返回 image/webp。
原因:CACHE_RESPONSE = True + PHOTO_CACHE_TIMEOUT = 604800,memcached 里的整页响应最长活 7 天,比实验周期长得多。
解决:换全新邮箱哈希验证(最可靠),或 manage.py clear_cache(回显 Cache "default" has been cleared!)后重试。
5.3 ALLOWED_HOSTS 收紧后整站 400
现象:把 ALLOWED_HOSTS 从 ["*"] 改成具体域名后,所有请求 400 Bad Request。
原因:请求的 Host 头(如 localhost:8080 或 IP:8080)不在列表里,Django 安全中间件直接拒绝——这正是该参数的设计行为,不是故障。
解决:把实际对外域名写进列表;经 nginx 反代时确保 proxy_set_header Host $host 存在且域名与列表一致。多域名/加 IP 调试期可临时 ["域名", "IP"]。
5.4 开启 Gravatar 代理后未命中头像 500
现象:DEFAULT_GRAVATARPROXY = True 后,无头像邮箱请求 500,页面只有 "Server Error (500)"。
定位:journalctl -u ivatar 见 urllib.error.URLError: <urlopen error [Errno 101] Network is unreachable>——服务器访问不了 gravatar.com。
解决:内网/无出口环境保持 DEFAULT_GRAVATARPROXY = False(未命中直接走本站默认头像,行为确定);确需代理则给服务器配公网出口或 HTTP 代理。
5.5 每条 manage.py 命令都打印 "No metric readers configured"
现象:任何 manage.py ... 输出第一行都是:
No metric readers configured - metrics will not be exported
原因:ivatar 内置 OpenTelemetry 埋点,未配置 OTLP/Prometheus 导出器时 SDK 每次进程启动都会提示。这不是错误,命令本身照常执行。 处理:忽略;要接监控时按 README 的 OpenTelemetry 章节配置导出端点。
六、常见问题
Q1:最小生产配置要写哪几行?
A:config_local.py 五件事——DEBUG = False、SECRET_KEY(generate_secret_key 生成)、ALLOWED_HOSTS(实际域名)、DATABASES(PostgreSQL)、两个 *_BASE_URL。其余参数先用默认。
Q2:环境变量和 config_local.py 冲突谁赢?
A:config_local.py 赢。config.py 用 os.environ.get(...) 读环境变量是在 import 时发生的,而 config_local.py 在 config.py 末尾才被 import,后加载的显式赋值覆盖前者。想强制用环境变量,就别在 config_local.py 里写那个参数。
Q3:想看某个参数最终生效值,翻三个文件太麻烦?
A:manage.py diffsettings(找参数名)或 django-extensions 的 manage.py print_settings --format=pretty。
Q4:能把数据库换成 MySQL 吗?
A:可以,官方支持 MySQL/MariaDB。设 MYSQL_DATABASE/MYSQL_USER/MYSQL_PASSWORD 环境变量,或在 config_local.py 写 DATABASES(ENGINE 用 django.db.backends.mysql)。注意 MAX_LENGTH_URL=255 的注释提到 MySQL 行长度限制,URL 字段勿加长。
Q5:/stats/ 里的头像 URL 还是旧域名?
A:那是历史请求落库的统计值,改 BASE_URL 不会回写。以 diffsettings 为准。
七、参考链接
- ivatar 源码 config.py(本文全部参数的出处):/opt/ivatar/config.py(commit 6f4b7e0)
- ivatar 源码 config_local.py.example(模板):/opt/ivatar/config_local.py.example
- 本实例部署环境:/opt/ivatar/config_local.py(生产覆盖层)
- Libravatar Wiki《Running your own instance》:https://wiki.libravatar.org/running_your_own/
- 本专栏部署实测:《Ubuntu 22.04 安装 Libravatar 2.0 指南》
- 本专栏命令实操:《Libravatar 2.0 常用 CLI 命令与工具详解》
- 本专栏认知篇:《Libravatar 2.0 介绍与选型指南》