修改docker部署方式
This commit is contained in:
1 parent
3e7c9420fe
commit
3c6f2c0b7b
18 files changed
+323
-95
No files matched your search
@@ -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
@@ -2,7 +2,7 @@ PUBLIC_ORIGIN=https://blog.example.com
|
||||
SITE_ADDRESS=blog.example.com
|
||||
SITE_TITLE=ChuBlog
|
||||
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'
|
||||
ALLOW_REGISTRATION=true
|
||||
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.
|
||||
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/
|
||||
@@ -10,7 +10,7 @@ Rust/Axum 博客后端。作者在独立 Git 仓库中编辑文本或 Markdown
|
||||
| --- | --- |
|
||||
| [开发与依赖](docs/development.md) | 获取代码、双 HTTPS 依赖源、开发检查与联调 |
|
||||
| [前端](docs/frontend.md) | 页面、本地开发、浏览器联调和项目技能 |
|
||||
| [部署](docs/deployment.md) | Debian 13、Docker Compose、首次初始化 |
|
||||
| [部署](docs/deployment.md) | 容器内构建、Docker Compose、首次初始化 |
|
||||
| [文章发布](docs/publishing.md) | 账户授权、个人仓库、文章清单与 Git 工作流 |
|
||||
| [API](docs/API.md) | HTTP 接口、认证、请求与错误响应 |
|
||||
| [内容与隔离](docs/security.md) | 文件白名单、容量限制、SSH 与进程边界 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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] 外部 `ChuSQL`/`ChuSQL-RustDriver` 子模块配置、GitHub HTTPS 默认来源及授权设备 SSH 优先/HTTPS 回退;ChuSQL 文章登记元信息、公开元数据/标签索引及失败自动重建,数据库不承担唯一内容或账户存储。
|
||||
- [x] Docker Compose、Caddy 反向代理、独立持久卷、容器资源限制、最小进程身份与网络隔离。
|
||||
- [x] Rust、ChuSQL 与 Astro 全部在 Docker 内构建;默认腾讯云内网镜像,提供独立公网源验证配置,宿主机无需安装编译工具链。
|
||||
- [x] 后端/驱动自动测试、真实容器 SSH 联调脚本、运行/API/架构说明。
|
||||
|
||||
## 第一优先级:部署环境验收
|
||||
|
||||
+4
-3
@@ -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
|
||||
ARG NPM_REGISTRY=https://mirrors.tencentyun.com/npm/
|
||||
WORKDIR /app
|
||||
ENV ASTRO_TELEMETRY_DISABLED=1
|
||||
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 public ./public
|
||||
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}
|
||||
WORKDIR /app
|
||||
|
||||
@@ -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
@@ -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
|
||||
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 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 gitconfig /etc/chublog/gitconfig
|
||||
COPY --from=rust-build /out/chublog-api /usr/local/bin/chublog-api
|
||||
COPY deploy/gitconfig /etc/chublog/gitconfig
|
||||
RUN groupadd -g 10002 ingress && useradd -u 10001 -g ingress -M -s /usr/sbin/nologin blog
|
||||
|
||||
FROM base AS api
|
||||
@@ -14,26 +59,27 @@ ENTRYPOINT ["chublog-api"]
|
||||
|
||||
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
|
||||
COPY sshd_config /etc/ssh/sshd_config
|
||||
COPY hooks /hooks
|
||||
COPY ssh-entrypoint.sh /usr/local/bin/ssh-entrypoint
|
||||
COPY authorized-keys.sh /usr/local/bin/chublog-authorized-keys
|
||||
COPY deploy/sshd_config /etc/ssh/sshd_config
|
||||
COPY deploy/hooks /hooks
|
||||
COPY deploy/ssh-entrypoint.sh /usr/local/bin/ssh-entrypoint
|
||||
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
|
||||
ENTRYPOINT ["/usr/local/bin/ssh-entrypoint"]
|
||||
|
||||
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
|
||||
ENTRYPOINT ["/usr/local/bin/init-volumes"]
|
||||
|
||||
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 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 chusql.toml /etc/chusql/settings.toml
|
||||
COPY db-entrypoint.sh /usr/local/bin/db-entrypoint
|
||||
COPY --from=database-build /out/chusql-server /out/csql-bootstrap /out/libchusql_core_storage.so /usr/local/bin/
|
||||
COPY deploy/chusql.toml /etc/chusql/settings.toml
|
||||
COPY deploy/db-entrypoint.sh /usr/local/bin/db-entrypoint
|
||||
RUN chmod 755 /usr/local/bin/db-entrypoint
|
||||
ENV LD_LIBRARY_PATH=/usr/local/bin
|
||||
RUN chusql-server --help && csql-bootstrap --help
|
||||
USER 10003:10003
|
||||
ENTRYPOINT ["/usr/local/bin/db-entrypoint"]
|
||||
+20
-7
@@ -4,9 +4,18 @@ x-logging: &logging
|
||||
options:
|
||||
max-size: "10m"
|
||||
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:
|
||||
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:
|
||||
- content:/data
|
||||
- authorization:/auth
|
||||
@@ -17,7 +26,7 @@ services:
|
||||
network_mode: none
|
||||
logging: *logging
|
||||
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:
|
||||
init: { condition: service_completed_successfully }
|
||||
environment:
|
||||
@@ -49,7 +58,11 @@ services:
|
||||
retries: 3
|
||||
logging: *logging
|
||||
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:
|
||||
api: { condition: service_healthy }
|
||||
environment:
|
||||
@@ -73,7 +86,7 @@ services:
|
||||
retries: 3
|
||||
logging: *logging
|
||||
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:
|
||||
init: { condition: service_completed_successfully }
|
||||
api: { condition: service_healthy }
|
||||
@@ -96,7 +109,7 @@ services:
|
||||
restart: unless-stopped
|
||||
logging: *logging
|
||||
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:
|
||||
init: { condition: service_completed_successfully }
|
||||
environment:
|
||||
@@ -115,7 +128,7 @@ services:
|
||||
restart: unless-stopped
|
||||
logging: *logging
|
||||
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]
|
||||
entrypoint: [chublog-api, provision-db]
|
||||
environment:
|
||||
@@ -130,7 +143,7 @@ services:
|
||||
restart: "no"
|
||||
logging: *logging
|
||||
caddy:
|
||||
image: ${REGISTRY_PREFIX:-}caddy:2
|
||||
image: ${REGISTRY_PREFIX-mirror.ccs.tencentyun.com/library/}caddy:2
|
||||
environment:
|
||||
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"]
|
||||
|
||||
@@ -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/
|
||||
@@ -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` 用法。
|
||||
@@ -9,5 +9,6 @@
|
||||
| [ADR-003](0003-author-repositories-and-manifest.md) | 作者独立仓库;文本索引方案由 ADR-004 替代 |
|
||||
| [ADR-004](0004-toml-multipage-staging.md) | TOML 元信息、多页正文、系统时间与索引暂存 |
|
||||
| [ADR-005](0005-astro-reading-frontend.md) | Astro 前端、同源 API 与 Docker/Caddy 编排 |
|
||||
| [ADR-006](0006-container-builds.md) | Docker 内编译后端与数据库,宿主机无需编译工具链 |
|
||||
|
||||
旧决策保留其历史背景;当前作者隔离与文章索引规则以 ADR-003 和 ADR-004 为准。
|
||||
+53
-16
@@ -4,13 +4,19 @@
|
||||
|
||||
## 构建与启动
|
||||
|
||||
- 部署基于 Debian 13。
|
||||
- 构建与运行镜像基于 Debian 13;宿主机只需满足 Docker 运行要求。
|
||||
- `scripts/deploy.sh` 会使用同一个依赖初始化脚本,自动按[依赖源顺序](development.md)进行 HTTPS 回退,再构建外部数据库与 Docker 镜像;沿用运行者现有的 Git 配置,不在项目内覆盖服务器环境。
|
||||
|
||||
```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` 用于首次数据库授权。
|
||||
@@ -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 守护进程。
|
||||
|
||||
镜像注册表与 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
|
||||
# ChuSQL 尚未编译时,先按其项目说明构建 Rust 存储库、server 与 bootstrap。
|
||||
python3 scripts/prepare-deploy.py --build-database
|
||||
cp .env.example .env
|
||||
chmod 600 .env
|
||||
# 从标准输入输入管理员口令,Ctrl-D 结束;不要把明文写进命令行历史。
|
||||
./deploy/bin/chublog-api hash-password
|
||||
# 将输出以单引号包裹写入 .env 的 ADMIN_PASSWORD_HASH;同时配置域名、镜像前缀及两份不同的随机 DB 口令。
|
||||
docker compose --env-file .env -f deploy/compose.yaml up -d --build
|
||||
# 编辑 .env,配置域名、镜像前缀及两份不同的随机 DB 口令。
|
||||
python3 scripts/prepare-deploy.py
|
||||
# 在临时容器内生成管理员密码哈希;不启动其他服务。
|
||||
# 输入口令后按 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;失败时检查日志后重试。
|
||||
docker compose --env-file .env -f deploy/compose.yaml run --rm db-init
|
||||
```
|
||||
|
||||
后续部署可运行 `scripts/deploy.sh`,自动初始化依赖、在 Docker 内构建并启动服务。
|
||||
|
||||
部署入口启动服务后,首次仍需执行上面的 `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。
|
||||
- 也可用 `--chusql-source /已有构建的/ChuSQL` 选择已有外部构建,没有产物会明确失败。
|
||||
- 编译产物只暂存在被忽略的 `deploy/bin/`。
|
||||
- 跨平台部署须先在目标 Debian/架构构建,不能复制 macOS、Windows 或其他架构二进制。
|
||||
- 镜像不含编译器、源码、`.env` 或密码文件。
|
||||
- `prepare-deploy.py` 调用 Compose 构建全部应用镜像,不执行宿主机编译、不生成 `deploy/bin/`。
|
||||
- `--build-database` 保留为兼容参数;数据库始终在 Docker 中构建。旧的 `--no-build` 和 `--chusql-source` 不再支持。
|
||||
- `--sources-env-file PATH` 可追加只含下载源配置的 Compose 环境文件;优先级高于第一份环境文件。
|
||||
- `--env-file PATH` 可选择配置文件;默认读取项目 `.env`,不存在时读取 `.env.example`(仅用于构建,不用于生产启动)。
|
||||
- 外部依赖仍由宿主机 `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
@@ -59,12 +59,20 @@ cargo clippy --all-targets -- -D warnings
|
||||
|
||||
## 联调
|
||||
|
||||
部署镜像在 Docker 内编译,不需要宿主机 Rust 或 Haskell 工具链;运行上面的源码开发检查时仍须自行安装 Rust。准备脚本的轻量回归检查:
|
||||
|
||||
```sh
|
||||
python3 scripts/prepare-deploy.py --build-database
|
||||
python3 scripts/smoke.py
|
||||
python3 scripts/smoke.py --key-type rsa
|
||||
python3 scripts/test-prepare-deploy.py
|
||||
```
|
||||
|
||||
```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 后,可追加前端真实浏览器验证。
|
||||
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 端口,测试结束清理自己创建的容器与卷。
|
||||
|
||||
+3
-2
@@ -47,12 +47,13 @@ Markdown 禁用原始 HTML,并在渲染后清理输出。文章内链接只映
|
||||
|
||||
## 浏览器检查
|
||||
|
||||
浏览器联调需要 Python Playwright 及 Chromium:
|
||||
以下检查用于开发电脑,需要 Python Playwright 及 Chromium;生产部署无需安装这些工具:
|
||||
|
||||
```sh
|
||||
python3 -m pip install playwright
|
||||
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 安装说明准备运行环境。
|
||||
|
||||
+6
-2
@@ -3,6 +3,10 @@
|
||||
set -eu
|
||||
PROJECT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
python3 "$PROJECT_ROOT/scripts/init-deps.py"
|
||||
python3 "$PROJECT_ROOT/scripts/prepare-deploy.py" --build-database
|
||||
docker compose --env-file "$PROJECT_ROOT/.env" -f "$PROJECT_ROOT/deploy/compose.yaml" up -d --build
|
||||
if [ ! -f "$PROJECT_ROOT/.env" ]; then
|
||||
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.'
|
||||
+26
-25
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
@@ -7,28 +7,29 @@ import subprocess
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--no-build', action='store_true', help='use an existing API release binary')
|
||||
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='build external database artifacts before staging')
|
||||
parser.add_argument('--env-file', type=Path, help='Compose environment file (default: .env, or .env.example when absent)')
|
||||
parser.add_argument('--build-database', action='store_true', help='compatibility option; database is always built inside Docker')
|
||||
parser.add_argument('--sources-env-file', type=Path, help='optional second Compose env file overriding build download sources')
|
||||
args = parser.parse_args()
|
||||
chusql = args.chusql_source.resolve()
|
||||
if not (root / 'external/ChuSQL-RustDriver/Cargo.toml').is_file():
|
||||
parser.error('Initialize external dependencies with scripts/init-deps.py first.')
|
||||
if not (chusql / 'chusql-core/storage/Cargo.toml').is_file():
|
||||
parser.error(f'ChuSQL source is missing: {chusql}; initialize dependencies first.')
|
||||
if not args.no_build:
|
||||
subprocess.run(['cargo', 'build', '--locked', '--release'], cwd=root, check=True)
|
||||
if args.build_database:
|
||||
subprocess.run(['cargo', 'build', '--locked', '--release'], cwd=chusql / 'chusql-core/storage', check=True)
|
||||
for package in ['chusql-server', 'chusql-bootstrap']:
|
||||
subprocess.run(['stack', 'build', '--fast'], cwd=chusql / package, check=True)
|
||||
out = root / 'deploy/bin'
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(root / 'target/release/chublog-api', out / 'chublog-api')
|
||||
for package, executable in [('chusql-server', 'chusql-server'), ('chusql-bootstrap', 'csql-bootstrap')]:
|
||||
matches = list((chusql / package / '.stack-work/install').glob(f'*/**/bin/{executable}'))
|
||||
if len(matches) != 1:
|
||||
raise SystemExit(f'Expected one built {executable}, found {len(matches)}; use --build-database or select a built --chusql-source.')
|
||||
shutil.copy2(matches[0], out / executable)
|
||||
shutil.copy2(chusql / 'chusql-core/storage/target/release/libchusql_core_storage.so', out)
|
||||
print('Prepared ignored deploy/bin. Build and run Compose on the same Debian 13 architecture.')
|
||||
if shutil.which('docker') is None:
|
||||
parser.error('Docker with the Compose plugin is required; see docs/deployment.md.')
|
||||
for manifest in ['external/ChuSQL-RustDriver/Cargo.toml', 'external/ChuSQL/chusql-core/storage/Cargo.toml']:
|
||||
if not (root / manifest).is_file():
|
||||
parser.error('Initialize external dependencies with python3 scripts/init-deps.py first.')
|
||||
envfile = args.env_file or (root / '.env' if (root / '.env').is_file() else root / '.env.example')
|
||||
if not envfile.is_file():
|
||||
parser.error(f'Environment file does not exist: {envfile}')
|
||||
command = ['docker', 'compose', '--env-file', str(envfile.resolve())]
|
||||
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}')
|
||||
command.extend(['--env-file', str(args.sources_env_file.resolve())])
|
||||
command.extend([
|
||||
'-f', str(root / 'deploy/compose.yaml'),
|
||||
'build', 'init', 'api', 'ssh', 'db', 'db-init', 'web',
|
||||
])
|
||||
try:
|
||||
subprocess.run(command, cwd=root, check=True)
|
||||
except subprocess.CalledProcessError as error:
|
||||
raise SystemExit(error.returncode) from None
|
||||
print('Deployment images built inside Docker. No host binaries were created.')
|
||||
+15
-4
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""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.
|
||||
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 http.cookiejar
|
||||
@@ -19,6 +19,7 @@ import urllib.request
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ORIGIN = 'http://localhost:18080'
|
||||
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('--key-type', choices=['ed25519', 'rsa'], default='ed25519')
|
||||
args = parser.parse_args()
|
||||
@@ -66,7 +67,7 @@ class Client:
|
||||
with tempfile.TemporaryDirectory(prefix='chublog-smoke-') as temporary:
|
||||
tmp = Path(temporary)
|
||||
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.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"
|
||||
@@ -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')
|
||||
envfile.chmod(0o600)
|
||||
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():
|
||||
# Query the private DB from inside its container; no host database port is opened.
|
||||
probe = r'''set -eu
|
||||
@@ -95,7 +101,12 @@ printf '%s\n' "$reply"
|
||||
return response['rows']
|
||||
|
||||
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()
|
||||
wait_for(lambda: admin.call('GET', '/api/health'))
|
||||
wait_for(lambda: subprocess.run(compose + ['run', '--rm', 'db-init'], capture_output=True).returncode == 0)
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user