From f6ef99e34ff9d9d3a4da4561e181d295b947fbec Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Sun, 11 Oct 2026 23:42:08 +0200 Subject: [PATCH] MQTT-FIX-T02: bound Docker logs and document production rollout --- README.md | 147 +++++++++++++++++++++++++- docker-compose.yml | 10 ++ docs/design/mqtt-reliability-fixes.md | 16 +-- tests/test_deployment.py | 17 +++ 4 files changed, 181 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ba5ab7b..a2d9cbd 100644 --- a/README.md +++ b/README.md @@ -590,10 +590,155 @@ docker compose -f docker-compose.yml pull docker compose -f docker-compose.yml up -d ``` +### Docker 日志轮转与生产应用 + +源码 Compose 为 `app` 和一次性 `migration` 服务配置 Docker `json-file` 日志轮转:单个 +日志文件最多约 10 MiB,最多保留 3 个文件。开发 override 继承这项设置。Docker 管理这些 +文件,通常位于宿主机 +`/var/lib/docker/containers/<完整容器ID>/<完整容器ID>-json.log`;路径取决于 Docker data +root,`docker logs` / `docker compose logs` 是查看容器日志的接口。轮转会限制每个容器的 +日志保留量,较旧内容会被 Docker 轮转淘汰。不要手动删除或编辑 Docker 管理的日志文件。 + +`docker restart` 只重启现有容器,不会更新容器创建时保存的 logging 配置;须重建容器。 +本机开发可重新执行上面的 Compose 命令。生产设备的 Compose 位于 +`/home/tianyu/.local/share/homeautomation`,与本仓库源码 Compose 分离。部署时只把两个 +服务的 `logging` 块合并到生产 Compose 对应服务中,并保留生产端已有的 `.env`、设备映射、 +`group_add`、volume、端口和其他部署设置;不要用完整源码 Compose 覆盖生产文件。 + +要同步的配置块如下,分别放在生产文件的 `services.app` 和 `services.migration` 下: + +```yaml +logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" +``` + +生产变更只供人工维护窗口执行,需在生产目录操作。先备份生产 Compose(以及已有的环境文件), +并且只把 `app`、`migration` 的 `logging` 块同步到生产文件;不要复制或覆盖整份生产 Compose。 +保留原有设备映射、`group_add`、volume、端口、环境变量和其他部署设置。编辑后用生产实际使用的 +Compose 文件组合及合成环境值运行 `docker compose ... config --quiet` 检查;后续所有 Compose 命令都要带上相同生产文件组合和 +`.mqtt-fix-image-pin.yml`,任何重建命令都带 `--pull never --no-build`;不要把展开后的配置 +(可能含凭据)打印或贴入报告。以下命令在生产目录执行,示例假设生产基础文件名为 +`docker-compose.yml`;若实际启动时显式使用其他 `-f` 文件,应在每条命令中原样保留该文件组合。 + +先确定本次要走的镜像路径,并创建仅用于固定镜像的持久 override。该文件只设置两个服务的 +`image`,生产 Compose 仍负责其余设置。只应用日志配置时,不能假设本地 `latest` 标签仍指向 +正在运行的镜像:先从运行中的 app 容器读取不可变 image ID,将其记录并加上受控本地标签, +然后固定这个标签。命令示例: + +```bash +cd /home/tianyu/.local/share/homeautomation +MQTT_FIX_BACKUP="compose.before-mqtt-fix-$(date +%Y%m%d-%H%M%S).yml" +MQTT_FIX_ENV_BACKUP="env.before-mqtt-fix-$(date +%Y%m%d-%H%M%S).bak" +MQTT_FIX_RECORD="mqtt-fix-maintenance-record.txt" +MQTT_FIX_PIN_BACKUP="image-pin.before-mqtt-fix.yml" +MQTT_FIX_APP_ID="$(docker inspect -f '{{.Image}}' home-automation-app)" +MQTT_FIX_OLD_REPODIGESTS="$(docker image inspect -f '{{json .RepoDigests}}' "$MQTT_FIX_APP_ID")" +printf 'previous app image ID: %s\nprevious app RepoDigests: %s\n' "$MQTT_FIX_APP_ID" "$MQTT_FIX_OLD_REPODIGESTS" | tee -a "$MQTT_FIX_RECORD" +MQTT_FIX_APP_IMAGE="home-automation:prod-before-mqtt-fix" +docker image tag "$MQTT_FIX_APP_ID" "$MQTT_FIX_APP_IMAGE" +printf 'previous app local pin: %s\n' "$MQTT_FIX_APP_IMAGE" | tee -a "$MQTT_FIX_RECORD" +cp docker-compose.yml "$MQTT_FIX_BACKUP" +if [ -f .env ]; then cp .env "$MQTT_FIX_ENV_BACKUP"; fi +if [ -f .mqtt-fix-image-pin.yml ]; then cp .mqtt-fix-image-pin.yml "$MQTT_FIX_PIN_BACKUP"; fi +printf 'Compose backup: %s\n.env backup (if present): %s\nprevious image pin backup (if present): %s\n' "$MQTT_FIX_BACKUP" "$MQTT_FIX_ENV_BACKUP" "$MQTT_FIX_PIN_BACKUP" | tee -a "$MQTT_FIX_RECORD" +cat > .mqtt-fix-image-pin.yml <` 应替换为已核验的版本: + +```bash +MQTT_FIX_RELEASE_TAG='code.wanderingbadger.dev/tliu93/home-automation:REPLACE_WITH_VERIFIED_TAG' +docker pull "$MQTT_FIX_RELEASE_TAG" +MQTT_FIX_NEW_REPODIGESTS="$(docker image inspect -f '{{json .RepoDigests}}' "$MQTT_FIX_RELEASE_TAG")" +printf 'target release tag: %s\ntarget RepoDigests: %s\n' "$MQTT_FIX_RELEASE_TAG" "$MQTT_FIX_NEW_REPODIGESTS" | tee -a "$MQTT_FIX_RECORD" +MQTT_FIX_APP_IMAGE='code.wanderingbadger.dev/tliu93/home-automation@sha256:REPLACE_WITH_VERIFIED_DIGEST' +cat > .mqtt-fix-image-pin.yml </dev/null +cat > .mqtt-fix-image-pin.yml <<'YAML' +services: + app: + image: home-automation:prod-before-mqtt-fix + migration: + image: home-automation:prod-before-mqtt-fix +YAML +WARMTELINK_DEVICE_PATH=/dev/synthetic-device WARMTELINK_SERIAL_GID=12345 TZ=Europe/Amsterdam \ + docker compose --env-file /dev/null -f docker-compose.yml -f .mqtt-fix-image-pin.yml config --quiet +docker compose -f docker-compose.yml -f .mqtt-fix-image-pin.yml create --pull never --no-build --force-recreate migration +docker compose -f docker-compose.yml -f .mqtt-fix-image-pin.yml up -d --no-deps --pull never --no-build --force-recreate app +``` + +源码更新路径也使用开头记录的旧 app image ID/pin 回退,无需依赖已变化的 `latest`。若本地 +旧 image ID 已不存在,则用维护记录中保存的旧 RepoDigest 显式拉取旧镜像,核对身份后打回原 pin +标签,再按上面的回退步骤操作。记录本次和回退使用的 image ID/digest、固定 pin 文件内容、生产 +Compose 及 `.env` 备份文件名、logging inspect 结果及验证时间。回退同样会重建容器、造成短暂 +中断;重建前需另行导出想保留的当前容器日志。保留 `.mqtt-fix-image-pin.yml` 供后续 Compose +操作继续使用;不要删除 Docker 日志、备份或其他生产文件。 + 持续查看日志: ```bash -docker compose logs -f app +docker compose -f docker-compose.yml -f .mqtt-fix-image-pin.yml logs -f app ``` ## Container Image CI diff --git a/docker-compose.yml b/docker-compose.yml index c45a9a0..7ec5691 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,11 @@ services: user: "1000:1000" restart: "no" init: true + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" command: ["python", "-m", "scripts.run_migrations"] environment: TZ: "${TZ:-Europe/Amsterdam}" @@ -19,6 +24,11 @@ services: user: "1000:1000" restart: unless-stopped init: true + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" environment: TZ: "${TZ:-Europe/Amsterdam}" depends_on: diff --git a/docs/design/mqtt-reliability-fixes.md b/docs/design/mqtt-reliability-fixes.md index 8beb9da..1925dba 100644 --- a/docs/design/mqtt-reliability-fixes.md +++ b/docs/design/mqtt-reliability-fixes.md @@ -82,7 +82,7 @@ MQTT broker(包括 `10.238.75.82:1883`)、访问真实串口、改动生产 ### MQTT-FIX-T02 — 为 Docker 日志配置轮转 -- **Status**: `todo` +- **Status**: `done` - **Depends**: MQTT-FIX-T01 - **Context**: 为本项目容器限定日志保留,明确源码修改与生产容器应用配置是两个操作。 @@ -109,13 +109,13 @@ MQTT broker(包括 `10.238.75.82:1883`)、访问真实串口、改动生产 - 不纳入用户已有的串口映射/group_add 改动。 **Acceptance criteria** -- [ ] app、migration 最终 Compose 均含指定字符串 logging options。 -- [ ] 开发 override 合并后日志配置正确。 -- [ ] Compose 经 config --quiet 或等效静态检查,不输出凭据。 -- [ ] 文档说明日志路径、docker logs、保留策略、restart 无效而须重建。 -- [ ] 文档明确生产路径、最小同步、镜像更新前提、采集中断、验证与回退;区分镜像发布和日志配置应用。 -- [ ] 无生产操作、数据/日志删除,已有无关改动保留。 -- [ ] pytest、ruff 全绿,独立 Reviewer 重跑并核查合并及部署可执行性。 +- [x] app、migration 最终 Compose 均含指定字符串 logging options。 +- [x] 开发 override 合并后日志配置正确。 +- [x] Compose 经 config --quiet 或等效静态检查,不输出凭据。 +- [x] 文档说明日志路径、docker logs、保留策略、restart 无效而须重建。 +- [x] 文档明确生产路径、最小同步、镜像更新前提、采集中断、验证与回退;区分镜像发布和日志配置应用。 +- [x] 无生产操作、数据/日志删除,已有无关改动保留。 +- [x] pytest、ruff 全绿,独立 Reviewer 重跑并核查合并及部署可执行性。 **Reviewer checklist** - 核查 logging options 类型及基础/开发配置合并;静态配置检查的范围和凭据保护。 diff --git a/tests/test_deployment.py b/tests/test_deployment.py index 8b7f707..bc3c165 100644 --- a/tests/test_deployment.py +++ b/tests/test_deployment.py @@ -87,6 +87,23 @@ def test_compose_defaults_business_timezone_and_dev_inherits_it() -> None: assert "TZ" not in dev["services"].get(service_name, {}).get("environment", {}) +def test_compose_log_rotation_is_string_typed_and_dev_inherits_it() -> None: + """Both services use bounded json-file logs; dev layering keeps base policy.""" + base = _read_yaml("docker-compose.yml") + dev = _read_yaml("docker-compose.dev.yml") + expected = { + "driver": "json-file", + "options": {"max-size": "10m", "max-file": "3"}, + } + + for service_name in ("migration", "app"): + base_logging = base["services"][service_name]["logging"] + assert base_logging == expected + assert all(isinstance(value, str) for value in base_logging["options"].values()) + # The override does not define logging, so Compose retains the base value. + assert "logging" not in dev["services"].get(service_name, {}) + + def test_compose_keeps_app_non_root_and_maps_minimal_warmtelink_serial_access() -> None: """Base Compose maps the configured device with only pyserial's required access.