diff --git a/DOCKER_DEPLOY.md b/DOCKER_DEPLOY.md index ea0ddd0..690d63f 100644 --- a/DOCKER_DEPLOY.md +++ b/DOCKER_DEPLOY.md @@ -145,7 +145,45 @@ docker compose restart xiaobai-review docker compose down ``` -更新程序: +### 使用 Gitea 更新程序(推荐) + +代码仓库为: + +```text +http://192.168.200.36:3200/leefer/xiaobaifupan.git +``` + +首次在服务器部署代码时,可以直接克隆到目标目录: + +```bash +sudo mkdir -p /opt/xiaobai-review +sudo chown "$USER":"$USER" /opt/xiaobai-review +git clone http://192.168.200.36:3200/leefer/xiaobaifupan.git /opt/xiaobai-review +cd /opt/xiaobai-review +``` + +私有仓库会提示输入 Gitea 用户名和密码或访问令牌。不要把密码写入仓库 URL、 +`compose.yaml` 或脚本。然后把原 `.env` 与 `data/` 放回该目录;这两项已被 Git +忽略,后续拉取代码不会覆盖数据库与密钥。 + +每次更新前先创建 SQLite 一致性备份,再拉取并重建容器: + +```bash +cd /opt/xiaobai-review +docker compose exec -T xiaobai-review python -c "import sqlite3; s=sqlite3.connect('/app/data/review.db'); d=sqlite3.connect('/app/data/review-before-update.db'); s.backup(d); d.close(); s.close()" +git pull --ff-only origin main +docker compose up -d --build +docker compose ps +curl --fail http://127.0.0.1:8765/api/health +``` + +`docker compose up -d --build` 会原地替换应用容器,不删除宿主机的 `data` 目录。 +数据库迁移会在新容器启动时自动执行。若 `git pull --ff-only` 提示本地代码有修改, +先用 `git status` 查明原因,不要用强制重置覆盖 `.env` 或 `data`。 + +### 不使用 Git 时更新 + +重新上传代码后执行: ```bash docker compose down diff --git a/tests/test_deployment_contract.py b/tests/test_deployment_contract.py new file mode 100644 index 0000000..932b374 --- /dev/null +++ b/tests/test_deployment_contract.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +class DeploymentContractTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.compose = (ROOT / "compose.yaml").read_text(encoding="utf-8") + cls.dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8") + cls.dockerignore = (ROOT / ".dockerignore").read_text(encoding="utf-8") + + def test_compose_exposes_only_requested_lan_port(self): + self.assertIn('"0.0.0.0:8765:8765/tcp"', self.compose) + self.assertIn("read_only: true", self.compose) + self.assertIn("target: /app/data", self.compose) + self.assertIn("no-new-privileges:true", self.compose) + + def test_image_runs_as_non_root_with_healthcheck(self): + self.assertIn("USER xiaobai", self.dockerfile) + self.assertIn("HEALTHCHECK", self.dockerfile) + self.assertIn('"--host", "0.0.0.0", "--port", "8765"', self.dockerfile) + + def test_secrets_and_runtime_data_are_not_copied_into_image(self): + for pattern in (".env", "data/*.db", "data/*.db-wal", "data/*.db-shm"): + self.assertIn(pattern, self.dockerignore) + + +if __name__ == "__main__": + unittest.main()