修改docker部署方式

This commit is contained in:
ChuNa7a committed 2026-10-17 13:13:26 +08:00
1 parent 3e7c9420fe
commit 3c6f2c0b7b
18 files changed
+323 -95

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
*
!Cargo.toml
!Cargo.lock
!apps/
!apps/api/
!apps/api/**
!external/
!external/ChuSQL/
!external/ChuSQL/**
!external/ChuSQL-RustDriver/
!external/ChuSQL-RustDriver/**
!deploy/
!deploy/**
**/.git
**/.ssh
**/target
**/.stack-work
**/node_modules
**/.env
**/.env.*
**/*.pem
**/*.key
deploy/bin
deploy/secrets
**/logs
+6 -1
View File
@@ -2,7 +2,7 @@ PUBLIC_ORIGIN=https://blog.example.com
SITE_ADDRESS=blog.example.com SITE_ADDRESS=blog.example.com
SITE_TITLE=ChuBlog SITE_TITLE=ChuBlog
ADMIN_USERNAME=admin ADMIN_USERNAME=admin
# Generate with chublog-api hash-password; keep single quotes around the hash. # Generate via the temporary API container in docs/deployment.md; keep single quotes around the hash.
ADMIN_PASSWORD_HASH='$argon2id$REPLACE_WITH_GENERATED_HASH' ADMIN_PASSWORD_HASH='$argon2id$REPLACE_WITH_GENERATED_HASH'
ALLOW_REGISTRATION=true ALLOW_REGISTRATION=true
CHUSQL_ROOT_PASSWORD=replace-with-random-root-password-at-least-16-characters CHUSQL_ROOT_PASSWORD=replace-with-random-root-password-at-least-16-characters
@@ -13,3 +13,8 @@ HTTPS_PORT=443
# Tencent Cloud VPC image mirror. Outside Tencent Cloud, clear this value. # Tencent Cloud VPC image mirror. Outside Tencent Cloud, clear this value.
REGISTRY_PREFIX=mirror.ccs.tencentyun.com/library/ REGISTRY_PREFIX=mirror.ccs.tencentyun.com/library/
# Tencent VPC package mirrors. Rust and GHC arrive through the image registry above.
DEBIAN_MIRROR=mirrors.tencentyun.com
NPM_REGISTRY=https://mirrors.tencentyun.com/npm/
# Tencent does not list a Cargo mirror; use RsProxy externally. Empty selects crates.io.
CARGO_MIRROR_INDEX=sparse+https://rsproxy.cn/index/
+1 -1
View File
@@ -10,7 +10,7 @@ Rust/Axum 博客后端。作者在独立 Git 仓库中编辑文本或 Markdown
| --- | --- | | --- | --- |
| [开发与依赖](docs/development.md) | 获取代码、双 HTTPS 依赖源、开发检查与联调 | | [开发与依赖](docs/development.md) | 获取代码、双 HTTPS 依赖源、开发检查与联调 |
| [前端](docs/frontend.md) | 页面、本地开发、浏览器联调和项目技能 | | [前端](docs/frontend.md) | 页面、本地开发、浏览器联调和项目技能 |
| [部署](docs/deployment.md) | Debian 13、Docker Compose、首次初始化 | | [部署](docs/deployment.md) | 容器内构建、Docker Compose、首次初始化 |
| [文章发布](docs/publishing.md) | 账户授权、个人仓库、文章清单与 Git 工作流 | | [文章发布](docs/publishing.md) | 账户授权、个人仓库、文章清单与 Git 工作流 |
| [API](docs/API.md) | HTTP 接口、认证、请求与错误响应 | | [API](docs/API.md) | HTTP 接口、认证、请求与错误响应 |
| [内容与隔离](docs/security.md) | 文件白名单、容量限制、SSH 与进程边界 | | [内容与隔离](docs/security.md) | 文件白名单、容量限制、SSH 与进程边界 |
+2 -1
View File
@@ -1,6 +1,6 @@
# ChuBlog — 后续待办计划 # ChuBlog — 后续待办计划
更新:2026-10-16。本文依据最新需求划分后续工作;`PLAN.md` 保留原始总体规划。已提供简单 Astro 前端与后端,Git 是唯一文章写入入口。以下未勾选项均为待办,不代表已经实现。 更新:2026-10-17。本文依据最新需求划分后续工作;`PLAN.md` 保留原始总体规划。已提供简单 Astro 前端与后端,Git 是唯一文章写入入口。以下未勾选项均为待办,不代表已经实现。
## 当前已实现 ## 当前已实现
@@ -15,6 +15,7 @@
- [x] 按作者隔离 SSH 接收仓库与 API 私有 Git 缓存;`index.toml` 登记文章 ID、目录、六项作者元信息与默认 `main.md` 的入口;支持可达多页、索引错误暂存、入口缺失自动撤回和系统生成作者/时间,增删条目自动同步阅读快照和 ChuSQL 元信息;未登记文件不导入,草稿不出现在公开接口。 - [x] 按作者隔离 SSH 接收仓库与 API 私有 Git 缓存;`index.toml` 登记文章 ID、目录、六项作者元信息与默认 `main.md` 的入口;支持可达多页、索引错误暂存、入口缺失自动撤回和系统生成作者/时间,增删条目自动同步阅读快照和 ChuSQL 元信息;未登记文件不导入,草稿不出现在公开接口。
- [x] 外部 `ChuSQL`/`ChuSQL-RustDriver` 子模块配置、GitHub HTTPS 默认来源及授权设备 SSH 优先/HTTPS 回退;ChuSQL 文章登记元信息、公开元数据/标签索引及失败自动重建,数据库不承担唯一内容或账户存储。 - [x] 外部 `ChuSQL`/`ChuSQL-RustDriver` 子模块配置、GitHub HTTPS 默认来源及授权设备 SSH 优先/HTTPS 回退;ChuSQL 文章登记元信息、公开元数据/标签索引及失败自动重建,数据库不承担唯一内容或账户存储。
- [x] Docker Compose、Caddy 反向代理、独立持久卷、容器资源限制、最小进程身份与网络隔离。 - [x] Docker Compose、Caddy 反向代理、独立持久卷、容器资源限制、最小进程身份与网络隔离。
- [x] Rust、ChuSQL 与 Astro 全部在 Docker 内构建;默认腾讯云内网镜像,提供独立公网源验证配置,宿主机无需安装编译工具链。
- [x] 后端/驱动自动测试、真实容器 SSH 联调脚本、运行/API/架构说明。 - [x] 后端/驱动自动测试、真实容器 SSH 联调脚本、运行/API/架构说明。
## 第一优先级:部署环境验收 ## 第一优先级:部署环境验收
+4 -3
View File
@@ -1,13 +1,14 @@
ARG NODE_IMAGE=node:24-bookworm-slim ARG NODE_IMAGE=mirror.ccs.tencentyun.com/library/node:24-bookworm-slim
FROM ${NODE_IMAGE} AS build FROM ${NODE_IMAGE} AS build
ARG NPM_REGISTRY=https://mirrors.tencentyun.com/npm/
WORKDIR /app WORKDIR /app
ENV ASTRO_TELEMETRY_DISABLED=1 ENV ASTRO_TELEMETRY_DISABLED=1
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund RUN npm ci --no-audit --no-fund --registry="${NPM_REGISTRY}"
COPY astro.config.mjs tsconfig.json ./ COPY astro.config.mjs tsconfig.json ./
COPY public ./public COPY public ./public
COPY src ./src COPY src ./src
RUN npm run build && npm prune --omit=dev --no-audit --no-fund RUN npm run build && npm prune --omit=dev --no-audit --no-fund --registry="${NPM_REGISTRY}"
FROM ${NODE_IMAGE} FROM ${NODE_IMAGE}
WORKDIR /app WORKDIR /app
-16
View File
@@ -1,16 +0,0 @@
*
!Dockerfile
!bin/
!bin/chublog-api
!bin/chusql-server
!bin/csql-bootstrap
!bin/libchusql_core_storage.so
!sshd_config
!gitconfig
!authorized-keys.sh
!ssh-entrypoint.sh
!init-volumes.sh
!db-entrypoint.sh
!chusql.toml
!hooks/
!hooks/pre-receive
+59 -13
View File
@@ -1,10 +1,55 @@
ARG DEBIAN_IMAGE=debian:trixie-slim ARG DEBIAN_IMAGE=mirror.ccs.tencentyun.com/library/debian:trixie-slim
ARG RUST_IMAGE=mirror.ccs.tencentyun.com/library/rust:1-trixie
ARG HASKELL_IMAGE=mirror.ccs.tencentyun.com/library/haskell:9.10.3-slim-bookworm
FROM ${HASKELL_IMAGE} AS haskell-toolchain
FROM ${RUST_IMAGE} AS rust-toolchain
ENV CARGO_BUILD_JOBS=2
ARG CARGO_MIRROR_INDEX=sparse+https://rsproxy.cn/index/
RUN if [ -n "$CARGO_MIRROR_INDEX" ]; then \
printf '[source.crates-io]\nreplace-with = "build-mirror"\n[source.build-mirror]\nregistry = "%s"\n' \
"$CARGO_MIRROR_INDEX" > /usr/local/cargo/config.toml; \
fi
FROM rust-toolchain AS rust-build
WORKDIR /src
COPY Cargo.toml Cargo.lock ./
COPY apps/api ./apps/api
COPY external/ChuSQL-RustDriver ./external/ChuSQL-RustDriver
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
cargo build --locked --release && mkdir /out && cp target/release/chublog-api /out/
FROM rust-toolchain AS database-build
ARG DEBIAN_MIRROR=mirrors.tencentyun.com
RUN sed -i "s|deb.debian.org|${DEBIAN_MIRROR}|g" /etc/apt/sources.list.d/debian.sources \
&& apt-get update && apt-get install -y --no-install-recommends \
binutils-gold pkg-config libgmp-dev libffi-dev zlib1g-dev libnuma-dev libtinfo6 \
&& rm -rf /var/lib/apt/lists/*
# GHC version must match the pinned ChuSQL Stack resolver.
COPY --from=haskell-toolchain /opt/ghc/9.10.3 /opt/ghc/9.10.3
COPY --from=haskell-toolchain /usr/local/bin/stack /usr/local/bin/stack
ENV PATH=/opt/ghc/9.10.3/bin:$PATH
WORKDIR /src/ChuSQL
COPY external/ChuSQL ./
RUN --mount=type=cache,target=/usr/local/cargo/registry \
cargo build --manifest-path chusql-core/storage/Cargo.toml --locked --release
ENV LD_LIBRARY_PATH=/src/ChuSQL/chusql-core/storage/target/release
RUN --mount=type=cache,target=/root/.stack \
mkdir /out && cd chusql-server \
&& stack --system-ghc --no-install-ghc --no-terminal --jobs 2 build --fast --copy-bins --local-bin-path /out \
&& cd ../chusql-bootstrap \
&& stack --system-ghc --no-install-ghc --no-terminal --jobs 2 build --fast --copy-bins --local-bin-path /out \
&& cp ../chusql-core/storage/target/release/libchusql_core_storage.so /out/
FROM ${DEBIAN_IMAGE} AS base FROM ${DEBIAN_IMAGE} AS base
ARG DEBIAN_MIRROR=deb.debian.org ARG DEBIAN_MIRROR=mirrors.tencentyun.com
RUN sed -i "s|deb.debian.org|${DEBIAN_MIRROR}|g" /etc/apt/sources.list.d/debian.sources RUN sed -i "s|deb.debian.org|${DEBIAN_MIRROR}|g" /etc/apt/sources.list.d/debian.sources
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates libgmp10 curl util-linux && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates libgmp10 curl util-linux && rm -rf /var/lib/apt/lists/*
COPY bin/chublog-api /usr/local/bin/chublog-api COPY --from=rust-build /out/chublog-api /usr/local/bin/chublog-api
COPY gitconfig /etc/chublog/gitconfig COPY deploy/gitconfig /etc/chublog/gitconfig
RUN groupadd -g 10002 ingress && useradd -u 10001 -g ingress -M -s /usr/sbin/nologin blog RUN groupadd -g 10002 ingress && useradd -u 10001 -g ingress -M -s /usr/sbin/nologin blog
FROM base AS api FROM base AS api
@@ -14,26 +59,27 @@ ENTRYPOINT ["chublog-api"]
FROM base AS ssh FROM base AS ssh
RUN apt-get update && apt-get install -y --no-install-recommends openssh-server && rm -rf /var/lib/apt/lists/* && useradd -u 10002 -g ingress -M -d /srv/ingress -s /bin/sh git && passwd -d git RUN apt-get update && apt-get install -y --no-install-recommends openssh-server && rm -rf /var/lib/apt/lists/* && useradd -u 10002 -g ingress -M -d /srv/ingress -s /bin/sh git && passwd -d git
COPY sshd_config /etc/ssh/sshd_config COPY deploy/sshd_config /etc/ssh/sshd_config
COPY hooks /hooks COPY deploy/hooks /hooks
COPY ssh-entrypoint.sh /usr/local/bin/ssh-entrypoint COPY deploy/ssh-entrypoint.sh /usr/local/bin/ssh-entrypoint
COPY authorized-keys.sh /usr/local/bin/chublog-authorized-keys COPY deploy/authorized-keys.sh /usr/local/bin/chublog-authorized-keys
RUN chmod 755 /hooks/pre-receive /usr/local/bin/ssh-entrypoint /usr/local/bin/chublog-authorized-keys RUN chmod 755 /hooks/pre-receive /usr/local/bin/ssh-entrypoint /usr/local/bin/chublog-authorized-keys
ENTRYPOINT ["/usr/local/bin/ssh-entrypoint"] ENTRYPOINT ["/usr/local/bin/ssh-entrypoint"]
FROM base AS init FROM base AS init
COPY init-volumes.sh /usr/local/bin/init-volumes COPY deploy/init-volumes.sh /usr/local/bin/init-volumes
RUN chmod 755 /usr/local/bin/init-volumes RUN chmod 755 /usr/local/bin/init-volumes
ENTRYPOINT ["/usr/local/bin/init-volumes"] ENTRYPOINT ["/usr/local/bin/init-volumes"]
FROM ${DEBIAN_IMAGE} AS database FROM ${DEBIAN_IMAGE} AS database
ARG DEBIAN_MIRROR=deb.debian.org ARG DEBIAN_MIRROR=mirrors.tencentyun.com
RUN sed -i "s|deb.debian.org|${DEBIAN_MIRROR}|g" /etc/apt/sources.list.d/debian.sources RUN sed -i "s|deb.debian.org|${DEBIAN_MIRROR}|g" /etc/apt/sources.list.d/debian.sources
RUN apt-get update && apt-get install -y --no-install-recommends libgmp10 ca-certificates && rm -rf /var/lib/apt/lists/* && useradd -u 10003 -M -s /usr/sbin/nologin chusql RUN apt-get update && apt-get install -y --no-install-recommends libgmp10 ca-certificates && rm -rf /var/lib/apt/lists/* && useradd -u 10003 -M -s /usr/sbin/nologin chusql
COPY bin/chusql-server bin/csql-bootstrap bin/libchusql_core_storage.so /usr/local/bin/ COPY --from=database-build /out/chusql-server /out/csql-bootstrap /out/libchusql_core_storage.so /usr/local/bin/
COPY chusql.toml /etc/chusql/settings.toml COPY deploy/chusql.toml /etc/chusql/settings.toml
COPY db-entrypoint.sh /usr/local/bin/db-entrypoint COPY deploy/db-entrypoint.sh /usr/local/bin/db-entrypoint
RUN chmod 755 /usr/local/bin/db-entrypoint RUN chmod 755 /usr/local/bin/db-entrypoint
ENV LD_LIBRARY_PATH=/usr/local/bin ENV LD_LIBRARY_PATH=/usr/local/bin
RUN chusql-server --help && csql-bootstrap --help
USER 10003:10003 USER 10003:10003
ENTRYPOINT ["/usr/local/bin/db-entrypoint"] ENTRYPOINT ["/usr/local/bin/db-entrypoint"]
+20 -7
View File
@@ -4,9 +4,18 @@ x-logging: &logging
options: options:
max-size: "10m" max-size: "10m"
max-file: "3" max-file: "3"
x-backend-build: &backend-build
context: ..
dockerfile: deploy/Dockerfile
args:
DEBIAN_MIRROR: ${DEBIAN_MIRROR:-mirrors.tencentyun.com}
DEBIAN_IMAGE: ${REGISTRY_PREFIX-mirror.ccs.tencentyun.com/library/}debian:trixie-slim
RUST_IMAGE: ${REGISTRY_PREFIX-mirror.ccs.tencentyun.com/library/}rust:1-trixie
HASKELL_IMAGE: ${REGISTRY_PREFIX-mirror.ccs.tencentyun.com/library/}haskell:9.10.3-slim-bookworm
CARGO_MIRROR_INDEX: ${CARGO_MIRROR_INDEX-sparse+https://rsproxy.cn/index/}
services: services:
init: init:
build: { context: ., target: init, args: { DEBIAN_MIRROR: "${DEBIAN_MIRROR:-deb.debian.org}", DEBIAN_IMAGE: "${REGISTRY_PREFIX:-}debian:trixie-slim" } } build: { <<: *backend-build, target: init }
volumes: volumes:
- content:/data - content:/data
- authorization:/auth - authorization:/auth
@@ -17,7 +26,7 @@ services:
network_mode: none network_mode: none
logging: *logging logging: *logging
api: api:
build: { context: ., target: api, args: { DEBIAN_MIRROR: "${DEBIAN_MIRROR:-deb.debian.org}", DEBIAN_IMAGE: "${REGISTRY_PREFIX:-}debian:trixie-slim" } } build: { <<: *backend-build, target: api }
depends_on: depends_on:
init: { condition: service_completed_successfully } init: { condition: service_completed_successfully }
environment: environment:
@@ -49,7 +58,11 @@ services:
retries: 3 retries: 3
logging: *logging logging: *logging
web: web:
build: { context: ../apps/web, args: { NODE_IMAGE: "${REGISTRY_PREFIX:-}node:24-bookworm-slim" } } build:
context: ../apps/web
args:
NODE_IMAGE: ${REGISTRY_PREFIX-mirror.ccs.tencentyun.com/library/}node:24-bookworm-slim
NPM_REGISTRY: ${NPM_REGISTRY:-https://mirrors.tencentyun.com/npm/}
depends_on: depends_on:
api: { condition: service_healthy } api: { condition: service_healthy }
environment: environment:
@@ -73,7 +86,7 @@ services:
retries: 3 retries: 3
logging: *logging logging: *logging
ssh: ssh:
build: { context: ., target: ssh, args: { DEBIAN_MIRROR: "${DEBIAN_MIRROR:-deb.debian.org}", DEBIAN_IMAGE: "${REGISTRY_PREFIX:-}debian:trixie-slim" } } build: { <<: *backend-build, target: ssh }
depends_on: depends_on:
init: { condition: service_completed_successfully } init: { condition: service_completed_successfully }
api: { condition: service_healthy } api: { condition: service_healthy }
@@ -96,7 +109,7 @@ services:
restart: unless-stopped restart: unless-stopped
logging: *logging logging: *logging
db: db:
build: { context: ., target: database, args: { DEBIAN_MIRROR: "${DEBIAN_MIRROR:-deb.debian.org}", DEBIAN_IMAGE: "${REGISTRY_PREFIX:-}debian:trixie-slim" } } build: { <<: *backend-build, target: database }
depends_on: depends_on:
init: { condition: service_completed_successfully } init: { condition: service_completed_successfully }
environment: environment:
@@ -115,7 +128,7 @@ services:
restart: unless-stopped restart: unless-stopped
logging: *logging logging: *logging
db-init: db-init:
build: { context: ., target: api, args: { DEBIAN_MIRROR: "${DEBIAN_MIRROR:-deb.debian.org}", DEBIAN_IMAGE: "${REGISTRY_PREFIX:-}debian:trixie-slim" } } build: { <<: *backend-build, target: api }
profiles: [setup] profiles: [setup]
entrypoint: [chublog-api, provision-db] entrypoint: [chublog-api, provision-db]
environment: environment:
@@ -130,7 +143,7 @@ services:
restart: "no" restart: "no"
logging: *logging logging: *logging
caddy: caddy:
image: ${REGISTRY_PREFIX:-}caddy:2 image: ${REGISTRY_PREFIX-mirror.ccs.tencentyun.com/library/}caddy:2
environment: environment:
SITE_ADDRESS: ${SITE_ADDRESS:-localhost} SITE_ADDRESS: ${SITE_ADDRESS:-localhost}
ports: ["${HTTP_BIND:-0.0.0.0}:${HTTP_PORT:-80}:80", "${HTTP_BIND:-0.0.0.0}:${HTTPS_PORT:-443}:443"] ports: ["${HTTP_BIND:-0.0.0.0}:${HTTP_PORT:-80}:80", "${HTTP_BIND:-0.0.0.0}:${HTTPS_PORT:-443}:443"]
+5
View File
@@ -0,0 +1,5 @@
# Build/test overrides only; contains no credentials or production settings.
REGISTRY_PREFIX=public.ecr.aws/docker/library/
DEBIAN_MIRROR=mirrors.ustc.edu.cn
NPM_REGISTRY=https://registry.npmjs.org/
CARGO_MIRROR_INDEX=sparse+https://rsproxy.cn/index/
+23
View File
@@ -0,0 +1,23 @@
# ADR-006:在 Docker 内编译部署产物
## 状态与日期
已接受,2026-10-17。
## 背景
旧部署流程要求宿主机安装 Rust、C 编译工具和 Haskell Stack,先生成 `deploy/bin/`,再复制到镜像。只有 Docker 的服务器会在调用 Cargo 时失败,也容易因宿主机与运行镜像的系统库不同而产生兼容问题。
## 决定
Rust API、ChuSQL 的 Rust 存储库、Haskell 服务端与 bootstrap 均改为 Docker 多阶段构建,使用 Debian 13 系列构建和运行镜像。Astro 保持容器内构建。运行阶段只复制必要的二进制与共享库,不包含构建工具、源码和凭据。ChuSQL 不构建 Web 管理端。
依赖仍由宿主机 Git 与 Python 初始化,保留固定版本、HTTPS 回退和服务器已有 Git 配置。Docker 构建只消费已取得的源码,不挂载 SSH 私钥,也不复制 Git 元数据、宿主机编译产物或环境密码文件。
部署准备脚本调用 Compose 构建全部镜像。管理员密码哈希在不启动依赖服务的临时 API 容器中生成。宿主机不需要执行博客或数据库二进制。
## 影响
宿主机只需 Docker Engine、Compose 插件、Python 3 和 Git。首次构建仍需要足够的内存、磁盘和网络,Rust 与 GHC 编译器来自预装工具链的容器镜像,优先通过腾讯云内网注册表拉取;Cargo 与 Haskell 包仍从各自软件包源下载。Debian 与 npm 默认使用腾讯云内网源。腾讯云未列出的 Cargo/Hackage 不伪造镜像地址,Cargo 源可配置;公开验证源单独存放,不覆盖生产秘密配置。构建缓存留在 BuildKit 内;运行容器的权限、网络和持久卷规则保持原有配置。
旧 `--build-database` 参数兼容保留;默认已构建数据库。移除依赖宿主机产物的 `--no-build`、`--chusql-source` 用法。
+1
View File
@@ -9,5 +9,6 @@
| [ADR-003](0003-author-repositories-and-manifest.md) | 作者独立仓库;文本索引方案由 ADR-004 替代 | | [ADR-003](0003-author-repositories-and-manifest.md) | 作者独立仓库;文本索引方案由 ADR-004 替代 |
| [ADR-004](0004-toml-multipage-staging.md) | TOML 元信息、多页正文、系统时间与索引暂存 | | [ADR-004](0004-toml-multipage-staging.md) | TOML 元信息、多页正文、系统时间与索引暂存 |
| [ADR-005](0005-astro-reading-frontend.md) | Astro 前端、同源 API 与 Docker/Caddy 编排 | | [ADR-005](0005-astro-reading-frontend.md) | Astro 前端、同源 API 与 Docker/Caddy 编排 |
| [ADR-006](0006-container-builds.md) | Docker 内编译后端与数据库,宿主机无需编译工具链 |
旧决策保留其历史背景;当前作者隔离与文章索引规则以 ADR-003 和 ADR-004 为准。 旧决策保留其历史背景;当前作者隔离与文章索引规则以 ADR-003 和 ADR-004 为准。
+53 -16
View File
@@ -4,13 +4,19 @@
## 构建与启动 ## 构建与启动
- 部署基于 Debian 13。 - 构建与运行镜像基于 Debian 13;宿主机只需满足 Docker 运行要求。
- `scripts/deploy.sh` 会使用同一个依赖初始化脚本,自动按[依赖源顺序](development.md)进行 HTTPS 回退,再构建外部数据库与 Docker 镜像;沿用运行者现有的 Git 配置,不在项目内覆盖服务器环境。 - `scripts/deploy.sh` 会使用同一个依赖初始化脚本,自动按[依赖源顺序](development.md)进行 HTTPS 回退,再构建外部数据库与 Docker 镜像;沿用运行者现有的 Git 配置,不在项目内覆盖服务器环境。
```sh ```sh
scripts/deploy.sh scripts/deploy.sh
``` ```
## 宿主机要求
宿主机只需 Docker Engine(含 BuildKit)、Docker Compose 插件、Python 3 和 Git,以及执行 Docker 命令的权限。不需要安装 Cargo、Rust、GHC、Stack、Node.js 或 C 编译工具。
Rust API、ChuSQL 存储库和 Haskell 服务端在 Docker 多阶段构建中编译,Astro 前端也在 Docker 中构建。构建工具和源码只保留在构建阶段,运行镜像仅包含应用产物及运行依赖。首次构建会拉取预装工具链的镜像并下载软件包,需要网络、足够的内存及磁盘空间;后续构建复用 Docker 缓存。
## 服务与路由 ## 服务与路由
Compose 编排 Astro 前端 `web`、Rust API `api`、文章 SSH 接收 `ssh`、ChuSQL 数据库 `db` 和 Caddy。`init` 初始化持久卷,`db-init` 用于首次数据库授权。 Compose 编排 Astro 前端 `web`、Rust API `api`、文章 SSH 接收 `ssh`、ChuSQL 数据库 `db` 和 Caddy。`init` 初始化持久卷,`db-init` 用于首次数据库授权。
@@ -21,40 +27,71 @@ Caddy 将 `/api/*` 转发到 `api:3001`,其余请求转发到 `web:4321`。Nod
## 腾讯云镜像 ## 腾讯云镜像
`.env.example` 默认配置 `REGISTRY_PREFIX=mirror.ccs.tencentyun.com/library/`,为 Debian、Node 与 Caddy 使用腾讯云内网 Docker 镜像。该地址仅供腾讯云内网访问,见[腾讯云官方说明](https://cloud.tencent.com/document/practice/1207/45596)。 `.env.example` 默认配置 `REGISTRY_PREFIX=mirror.ccs.tencentyun.com/library/`,为 Rust、Debian、Node 与 Caddy 使用腾讯云内网 Docker 镜像。该地址仅供腾讯云内网访问,见[腾讯云官方说明](https://cloud.tencent.com/document/practice/1207/45596)。
在腾讯云内网部署时保留该值;其他环境将 `REGISTRY_PREFIX` 留空以使用原始镜像名,或改为可访问且受信的镜像前缀。项目不修改宿主机 Docker 配置、不关闭 TLS,也不重启 Docker 守护进程。 在腾讯云内网部署时保留该值;其他环境将 `REGISTRY_PREFIX` 留空以使用原始镜像名,或改为可访问且受信的镜像前缀。项目不修改宿主机 Docker 配置、不关闭 TLS,也不重启 Docker 守护进程。
镜像注册表与 Debian 软件包源是不同配置。`REGISTRY_PREFIX` 控制基础镜像和 Caddy,`DEBIAN_MIRROR` 控制容器构建时的 apt 源。 镜像注册表与软件包源分别配置:
| 配置 | 默认值与用途 |
| --- | --- |
| `REGISTRY_PREFIX` | `mirror.ccs.tencentyun.com/library/`,用于 Rust、GHC/Haskell、Debian、Node 与 Caddy 镜像 |
| `DEBIAN_MIRROR` | `mirrors.tencentyun.com`,容器内 Debian 软件包源,保留 Debian 签名校验 |
| `NPM_REGISTRY` | `https://mirrors.tencentyun.com/npm/`,前端依赖源 |
| `CARGO_MIRROR_INDEX` | 默认 `sparse+https://rsproxy.cn/index/`;显式留空可使用 crates.io |
腾讯云的[公开软件源目录](https://mirrors.tencent.com/)没有列出 Cargo 与 Hackage 镜像,因此不拼接未经确认的腾讯云地址。Cargo 使用 [RsProxy](https://rsproxy.cn/) 等可配置外部源;Haskell 包沿用 Stack 默认配置。腾讯云的[软件源说明](https://cloud.tencent.com/document/product/213/8623)区分了公网和内网域名。
Rust 工具链直接来自 Rust 镜像;GHC 9.10.3 和 Stack 来自匹配 ChuSQL resolver 的 Haskell 镜像。构建时禁止 Stack 再下载 GHC,也不调用 rustup 安装或更新编译器。编译仍在 Debian 13 中进行。
### 使用外网源验证
项目提供不含密码的 `deploy/sources.public.env`,使用公开 ECR Docker Library 镜像、USTC Debian 源、官方 npm 源及 RsProxy Cargo 源。外网构建与隔离联调时运行:
```sh
python3 scripts/prepare-deploy.py --sources-env-file deploy/sources.public.env
python3 scripts/smoke.py --key-type rsa \
--sources-env-file deploy/sources.public.env
```
第二份配置只覆盖下载来源,不改写 `.env` 的域名、口令等生产设置。Docker Compose 会优先使用当前 shell 中已导出的同名环境变量;切换测试配置前须检查这些覆盖。公网源连通性不能证明腾讯云内网镜像可用,内网源仍需在腾讯云服务器验证。
## 首次部署 ## 首次部署
先按[开发与依赖](development.md)初始化子模块,再执行以下同架构构建步骤。使用 `scripts/deploy.sh` 时,也须先准备 `.env`。 在项目根目录执行。先按[开发与依赖](development.md)初始化子模块,再准备配置和构建镜像:
```sh ```sh
# ChuSQL 尚未编译时,先按其项目说明构建 Rust 存储库、server 与 bootstrap。
python3 scripts/prepare-deploy.py --build-database
cp .env.example .env cp .env.example .env
chmod 600 .env chmod 600 .env
# 从标准输入输入管理员口令,Ctrl-D 结束;不要把明文写进命令行历史。 # 编辑 .env,配置域名、镜像前缀及两份不同的随机 DB 口令。
./deploy/bin/chublog-api hash-password python3 scripts/prepare-deploy.py
# 将输出以单引号包裹写入 .env 的 ADMIN_PASSWORD_HASH;同时配置域名、镜像前缀及两份不同的随机 DB 口令。 # 在临时容器内生成管理员密码哈希;不启动其他服务。
docker compose --env-file .env -f deploy/compose.yaml up -d --build # 输入口令后按 Ctrl-D 结束,不要把明文写进命令行历史。
docker compose --env-file .env -f deploy/compose.yaml \
run --rm --no-deps -T api hash-password
# 将输出以单引号包裹写入 .env 的 ADMIN_PASSWORD_HASH。
docker compose --env-file .env -f deploy/compose.yaml up -d --no-build
# db 启动就绪后,创建 chublog 库和非超级用户 chublog_index;失败时检查日志后重试。 # db 启动就绪后,创建 chublog 库和非超级用户 chublog_index;失败时检查日志后重试。
docker compose --env-file .env -f deploy/compose.yaml run --rm db-init docker compose --env-file .env -f deploy/compose.yaml run --rm db-init
``` ```
后续部署可运行 `scripts/deploy.sh`,自动初始化依赖、在 Docker 内构建并启动服务。
部署入口启动服务后,首次仍需执行上面的 `db-init` 命令;入口不会在数据库启动竞态中静默吞掉初始化错误。 部署入口启动服务后,首次仍需执行上面的 `db-init` 命令;入口不会在数据库启动竞态中静默吞掉初始化错误。
## 构建选项与平台 ## 构建选项与平台
构建时若 Debian 官方下载源较慢,可在 `.env` 配置 `DEBIAN_MIRROR=mirrors.ustc.edu.cn`,仍使用 Debian 签名校验。 需要切换默认腾讯云内网 Debian 包源时,可在 `.env` 配置 `DEBIAN_MIRROR=mirrors.ustc.edu.cn`,仍使用 Debian 签名校验。
- `prepare-deploy.py` 默认从数据库子模块读取编译产物;`--build-database` 会先构建其 Rust 存储库、Haskell server 和 bootstrap。 - `prepare-deploy.py` 调用 Compose 构建全部应用镜像,不执行宿主机编译、不生成 `deploy/bin/`。
- 也可用 `--chusql-source /已有构建的/ChuSQL` 选择已有外部构建,没有产物会明确失败。 - `--build-database` 保留为兼容参数;数据库始终在 Docker 中构建。旧的 `--no-build` 和 `--chusql-source` 不再支持。
- 编译产物只暂存在被忽略的 `deploy/bin/`。 - `--sources-env-file PATH` 可追加只含下载源配置的 Compose 环境文件;优先级高于第一份环境文件。
- 跨平台部署须先在目标 Debian/架构构建,不能复制 macOS、Windows 或其他架构二进制。 - `--env-file PATH` 可选择配置文件;默认读取项目 `.env`,不存在时读取 `.env.example`(仅用于构建,不用于生产启动)。
- 镜像不含编译器、源码、`.env` 或密码文件。 - 外部依赖仍由宿主机 `init-deps.py` 初始化,沿用现有 Git 配置;构建过程不将 SSH 私钥或 Git 凭据复制到镜像。
- GHC 工具链镜像支持 amd64、arm64;更新 ChuSQL resolver 时,须同步核对 GHC 镜像版本与 Dockerfile 中的复制路径。
- 镜像默认按 Docker 主机架构构建,不复用宿主机已有二进制;构建与运行阶段均使用 Debian 13 系列镜像。
- Cargo、Stack 软件包缓存保存在 Docker BuildKit 中。镜像前缀控制编译器镜像来源,Cargo 与 Haskell 包下载独立于镜像注册表。
- 运行镜像不含编译器、源码、`.env` 或密码文件。
## 管理员初始化 ## 管理员初始化
+12 -4
View File
@@ -59,12 +59,20 @@ cargo clippy --all-targets -- -D warnings
## 联调 ## 联调
部署镜像在 Docker 内编译,不需要宿主机 Rust 或 Haskell 工具链;运行上面的源码开发检查时仍须自行安装 Rust。准备脚本的轻量回归检查:
```sh ```sh
python3 scripts/prepare-deploy.py --build-database python3 scripts/test-prepare-deploy.py
python3 scripts/smoke.py ```
python3 scripts/smoke.py --key-type rsa
```sh
python3 scripts/prepare-deploy.py --sources-env-file deploy/sources.public.env
python3 scripts/smoke.py --sources-env-file deploy/sources.public.env
python3 scripts/smoke.py --key-type rsa \
--sources-env-file deploy/sources.public.env
# 安装 Python Playwright 和 Chromium 后,可追加前端真实浏览器验证。 # 安装 Python Playwright 和 Chromium 后,可追加前端真实浏览器验证。
python3 scripts/smoke.py --key-type rsa --browser-test python3 scripts/smoke.py --key-type rsa --browser-test \
--sources-env-file deploy/sources.public.env
``` ```
- 联调使用独立随机命名的 Compose 项目、本机 18080/18443/12222 端口,测试结束清理自己创建的容器与卷。 - 联调使用独立随机命名的 Compose 项目、本机 18080/18443/12222 端口,测试结束清理自己创建的容器与卷。
+3 -2
View File
@@ -47,12 +47,13 @@ Markdown 禁用原始 HTML,并在渲染后清理输出。文章内链接只映
## 浏览器检查 ## 浏览器检查
浏览器联调需要 Python Playwright 及 Chromium: 以下检查用于开发电脑,需要 Python Playwright 及 Chromium;生产部署无需安装这些工具:
```sh ```sh
python3 -m pip install playwright python3 -m pip install playwright
python3 -m playwright install chromium python3 -m playwright install chromium
python3 scripts/smoke.py --key-type rsa --browser-test python3 scripts/smoke.py --key-type rsa --browser-test \
--sources-env-file deploy/sources.public.env
``` ```
该命令使用隔离的 Docker 项目,验证真实注册登录、密码修改、公钥保存、站长授权、多页阅读及手机布局,测试后清理自己创建的容器与卷。截图默认放在 `/tmp/chublog-web-screenshots`。系统缺少浏览器库时,按 Playwright 安装说明准备运行环境。 该命令使用隔离的 Docker 项目,验证真实注册登录、密码修改、公钥保存、站长授权、多页阅读及手机布局,测试后清理自己创建的容器与卷。截图默认放在 `/tmp/chublog-web-screenshots`。系统缺少浏览器库时,按 Playwright 安装说明准备运行环境。
+6 -2
View File
@@ -3,6 +3,10 @@
set -eu set -eu
PROJECT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) PROJECT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
python3 "$PROJECT_ROOT/scripts/init-deps.py" python3 "$PROJECT_ROOT/scripts/init-deps.py"
python3 "$PROJECT_ROOT/scripts/prepare-deploy.py" --build-database if [ ! -f "$PROJECT_ROOT/.env" ]; then
docker compose --env-file "$PROJECT_ROOT/.env" -f "$PROJECT_ROOT/deploy/compose.yaml" up -d --build printf '%s\n' 'Create and configure .env first; see docs/deployment.md.' >&2
exit 1
fi
python3 "$PROJECT_ROOT/scripts/prepare-deploy.py"
docker compose --env-file "$PROJECT_ROOT/.env" -f "$PROJECT_ROOT/deploy/compose.yaml" up -d --no-build
printf '%s\n' 'When the database is ready, run the documented db-init command once.' printf '%s\n' 'When the database is ready, run the documented db-init command once.'
+26 -25
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Build the API and stage external ChuSQL artifacts without committing dependencies.""" """Build deployment images inside Docker; no host Rust or Haskell tools required."""
import argparse import argparse
from pathlib import Path from pathlib import Path
import shutil import shutil
@@ -7,28 +7,29 @@ import subprocess
root = Path(__file__).resolve().parents[1] root = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--no-build', action='store_true', help='use an existing API release binary') parser.add_argument('--env-file', type=Path, help='Compose environment file (default: .env, or .env.example when absent)')
parser.add_argument('--chusql-source', type=Path, default=root / 'external/ChuSQL', help='initialized/built external ChuSQL checkout') parser.add_argument('--build-database', action='store_true', help='compatibility option; database is always built inside Docker')
parser.add_argument('--build-database', action='store_true', help='build external database artifacts before staging') parser.add_argument('--sources-env-file', type=Path, help='optional second Compose env file overriding build download sources')
args = parser.parse_args() args = parser.parse_args()
chusql = args.chusql_source.resolve() if shutil.which('docker') is None:
if not (root / 'external/ChuSQL-RustDriver/Cargo.toml').is_file(): parser.error('Docker with the Compose plugin is required; see docs/deployment.md.')
parser.error('Initialize external dependencies with scripts/init-deps.py first.') for manifest in ['external/ChuSQL-RustDriver/Cargo.toml', 'external/ChuSQL/chusql-core/storage/Cargo.toml']:
if not (chusql / 'chusql-core/storage/Cargo.toml').is_file(): if not (root / manifest).is_file():
parser.error(f'ChuSQL source is missing: {chusql}; initialize dependencies first.') parser.error('Initialize external dependencies with python3 scripts/init-deps.py first.')
if not args.no_build: envfile = args.env_file or (root / '.env' if (root / '.env').is_file() else root / '.env.example')
subprocess.run(['cargo', 'build', '--locked', '--release'], cwd=root, check=True) if not envfile.is_file():
if args.build_database: parser.error(f'Environment file does not exist: {envfile}')
subprocess.run(['cargo', 'build', '--locked', '--release'], cwd=chusql / 'chusql-core/storage', check=True) command = ['docker', 'compose', '--env-file', str(envfile.resolve())]
for package in ['chusql-server', 'chusql-bootstrap']: if args.sources_env_file:
subprocess.run(['stack', 'build', '--fast'], cwd=chusql / package, check=True) if not args.sources_env_file.is_file():
out = root / 'deploy/bin' parser.error(f'Source configuration does not exist: {args.sources_env_file}')
out.mkdir(parents=True, exist_ok=True) command.extend(['--env-file', str(args.sources_env_file.resolve())])
shutil.copy2(root / 'target/release/chublog-api', out / 'chublog-api') command.extend([
for package, executable in [('chusql-server', 'chusql-server'), ('chusql-bootstrap', 'csql-bootstrap')]: '-f', str(root / 'deploy/compose.yaml'),
matches = list((chusql / package / '.stack-work/install').glob(f'*/**/bin/{executable}')) 'build', 'init', 'api', 'ssh', 'db', 'db-init', 'web',
if len(matches) != 1: ])
raise SystemExit(f'Expected one built {executable}, found {len(matches)}; use --build-database or select a built --chusql-source.') try:
shutil.copy2(matches[0], out / executable) subprocess.run(command, cwd=root, check=True)
shutil.copy2(chusql / 'chusql-core/storage/target/release/libchusql_core_storage.so', out) except subprocess.CalledProcessError as error:
print('Prepared ignored deploy/bin. Build and run Compose on the same Debian 13 architecture.') raise SystemExit(error.returncode) from None
print('Deployment images built inside Docker. No host binaries were created.')
+15 -4
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Exercise real Docker, SSH, Git, HTTP authorization, database outage, and restart recovery. """Exercise real Docker, SSH, Git, HTTP authorization, database outage, and restart recovery.
Uses an isolated Compose project and removes only that project's containers/volumes. Uses an isolated Compose project and removes only that project's containers/volumes.
Run prepare-deploy.py first. Ports 18080, 18443, 12222 must be free. Initializes no dependencies; Docker builds the images. Ports 18080, 18443, 12222 must be free.
""" """
import argparse import argparse
import http.cookiejar import http.cookiejar
@@ -19,6 +19,7 @@ import urllib.request
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
ORIGIN = 'http://localhost:18080' ORIGIN = 'http://localhost:18080'
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--sources-env-file', type=Path, help='Compose build source overrides, e.g. deploy/sources.public.env')
parser.add_argument('--browser-test', action='store_true', help='also run Python Playwright frontend journeys') parser.add_argument('--browser-test', action='store_true', help='also run Python Playwright frontend journeys')
parser.add_argument('--key-type', choices=['ed25519', 'rsa'], default='ed25519') parser.add_argument('--key-type', choices=['ed25519', 'rsa'], default='ed25519')
args = parser.parse_args() args = parser.parse_args()
@@ -66,7 +67,7 @@ class Client:
with tempfile.TemporaryDirectory(prefix='chublog-smoke-') as temporary: with tempfile.TemporaryDirectory(prefix='chublog-smoke-') as temporary:
tmp = Path(temporary) tmp = Path(temporary)
password = secrets.token_urlsafe(24) password = secrets.token_urlsafe(24)
hashed = run([str(ROOT / 'deploy/bin/chublog-api'), 'hash-password'], input=password, capture_output=True).stdout.strip() hashed = 'temporary-build-placeholder'
envfile = tmp / '.env' envfile = tmp / '.env'
envfile.write_text(f"PUBLIC_ORIGIN={ORIGIN}\nSITE_ADDRESS=http://localhost:80\nADMIN_PASSWORD_HASH='{hashed}'\n" envfile.write_text(f"PUBLIC_ORIGIN={ORIGIN}\nSITE_ADDRESS=http://localhost:80\nADMIN_PASSWORD_HASH='{hashed}'\n"
f"CHUSQL_ROOT_PASSWORD={secrets.token_urlsafe(24)}\nCHUSQL_PASSWORD={secrets.token_urlsafe(24)}\n" f"CHUSQL_ROOT_PASSWORD={secrets.token_urlsafe(24)}\nCHUSQL_PASSWORD={secrets.token_urlsafe(24)}\n"
@@ -74,7 +75,12 @@ with tempfile.TemporaryDirectory(prefix='chublog-smoke-') as temporary:
'DEBIAN_MIRROR=mirrors.ustc.edu.cn\nALLOW_REGISTRATION=true\nHTTP_BIND=127.0.0.1\nHTTP_PORT=18080\nHTTPS_PORT=18443\nSSH_BIND=127.0.0.1\nSSH_PORT=12222\n') 'DEBIAN_MIRROR=mirrors.ustc.edu.cn\nALLOW_REGISTRATION=true\nHTTP_BIND=127.0.0.1\nHTTP_PORT=18080\nHTTPS_PORT=18443\nSSH_BIND=127.0.0.1\nSSH_PORT=12222\n')
envfile.chmod(0o600) envfile.chmod(0o600)
project = 'chublog-test-' + secrets.token_hex(4) project = 'chublog-test-' + secrets.token_hex(4)
compose = ['docker', 'compose', '--env-file', str(envfile), '-p', project, '-f', str(ROOT / 'deploy/compose.yaml')] compose = ['docker', 'compose', '--env-file', str(envfile)]
if args.sources_env_file:
if not args.sources_env_file.is_file():
parser.error(f'Source configuration does not exist: {args.sources_env_file}')
compose.extend(['--env-file', str(args.sources_env_file.resolve())])
compose.extend(['-p', project, '-f', str(ROOT / 'deploy/compose.yaml')])
def database_articles(): def database_articles():
# Query the private DB from inside its container; no host database port is opened. # Query the private DB from inside its container; no host database port is opened.
probe = r'''set -eu probe = r'''set -eu
@@ -95,7 +101,12 @@ printf '%s\n' "$reply"
return response['rows'] return response['rows']
try: try:
run(compose + ['up', '-d', '--build']) run(compose + ['build', 'init', 'api', 'ssh', 'db', 'db-init', 'web'])
hashed = run(compose + ['run', '--rm', '--no-deps', '-T', 'api', 'hash-password'],
input=password, capture_output=True).stdout.strip()
assert hashed.startswith('$argon2id$'), 'Container did not produce an Argon2id hash'
envfile.write_text(envfile.read_text().replace('temporary-build-placeholder', hashed))
run(compose + ['up', '-d', '--no-build'])
admin = Client() admin = Client()
wait_for(lambda: admin.call('GET', '/api/health')) wait_for(lambda: admin.call('GET', '/api/health'))
wait_for(lambda: subprocess.run(compose + ['run', '--rm', 'db-init'], capture_output=True).returncode == 0) wait_for(lambda: subprocess.run(compose + ['run', '--rm', 'db-init'], capture_output=True).returncode == 0)
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Check deployment preparation without host compilers or a Docker daemon."""
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
SCRIPT = Path(__file__).with_name('prepare-deploy.py')
class PrepareDeployment(unittest.TestCase):
def test_docker_only_build_and_failure_propagation(self):
with tempfile.TemporaryDirectory(prefix='chublog deploy ') as temporary:
root = Path(temporary)
(root / 'scripts').mkdir()
shutil.copy2(SCRIPT, root / 'scripts/prepare-deploy.py')
for name in ['external/ChuSQL-RustDriver/Cargo.toml',
'external/ChuSQL/chusql-core/storage/Cargo.toml']:
manifest = root / name
manifest.parent.mkdir(parents=True)
manifest.touch()
(root / '.env.example').touch()
tools = root / 'tools'
tools.mkdir()
docker = tools / 'docker'
docker.write_text(f'#!{sys.executable}\nimport json, os, sys\n'
'from pathlib import Path\n'
'Path(os.environ["CALL_LOG"]).write_text(json.dumps(sys.argv[1:]))\n'
'sys.exit(int(os.environ.get("DOCKER_EXIT", "0")))\n')
docker.chmod(0o755)
log = root / 'call.json'
env = {**os.environ, 'PATH': str(tools), 'CALL_LOG': str(log)}
command = [sys.executable, str(root / 'scripts/prepare-deploy.py')]
result = subprocess.run(command + ['--build-database'], env=env, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
call = json.loads(log.read_text())
self.assertEqual(call[:3], ['compose', '--env-file', str(root / '.env.example')])
self.assertIn('db', call)
self.assertIn('web', call)
self.assertFalse((root / 'deploy/bin').exists())
override = root / 'public sources.env'
override.write_text('REGISTRY_PREFIX=public.ecr.aws/docker/library/\n')
result = subprocess.run(command + ['--sources-env-file', str(override)],
env=env, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads(log.read_text())[3:5], ['--env-file', str(override)])
(root / '.env').touch()
result = subprocess.run(command, env={**env, 'DOCKER_EXIT': '17'}, capture_output=True, text=True)
self.assertEqual(result.returncode, 17)
self.assertEqual(json.loads(log.read_text())[2], str(root / '.env'))
docker.unlink()
result = subprocess.run(command, env=env, capture_output=True, text=True)
self.assertEqual(result.returncode, 2)
self.assertIn('Docker with the Compose plugin is required', result.stderr)
if __name__ == '__main__':
unittest.main()