Add the compose file

It runs the same image with the settings a service that stays up wants: it
restarts unless stopped, mounts the filesystem read-only because the service
writes nothing, drops the ability to gain privileges, and probes the service
every thirty seconds.

The probe is written against the shell rather than against a tool, because
the image carries no HTTP client. It opens the connection as a file
descriptor with `bash`, writes the request, and reads the status line back.
That was checked against a running service and against a stopped one: it
reports healthy in the first case and unhealthy in the second, which is the
only thing a probe has to get right.

The host port is a variable with a default, because which port a machine has
free is a matter of taste, and the build context is the directory above, as
the Dockerfile's is.
This commit is contained in:
dev 2026-10-02 14:37:10 +08:00
parent 081c7d9275
commit 6403e50192
3 changed files with 118 additions and 2 deletions

View File

@ -121,7 +121,22 @@ The command names the host explicitly. The program defaults to the loopback
address, where it answers only itself, so a container started without it would address, where it answers only itself, so a container started without it would
serve nothing to anyone. serve nothing to anyone.
There is no `HEALTHCHECK`. Probing `GET /health` is what an orchestrator `docker-compose.yml` does the same thing with the settings a long-running
service wants: it restarts unless stopped, mounts the filesystem read-only
because nothing is written to it, drops the ability to gain privileges, and
probes the service every thirty seconds.
```sh
cd apsis-book
docker compose up --build
```
The probe is written against the shell rather than against a tool, because
the image carries no HTTP client: it carries one binary and nothing else, and
adding a client for a probe would undo that. `bash` opens the connection,
writes the request and reads the status line back.
There is no `HEALTHCHECK` in the image itself. Probing `GET /health` is what an orchestrator
should do, and a probe inside the container would mean adding an HTTP client should do, and a probe inside the container would mean adding an HTTP client
to an image that needs no other tool. A Kubernetes probe says the same thing to an image that needs no other tool. A Kubernetes probe says the same thing
without one: without one:
@ -230,6 +245,7 @@ apsis-book/
│ └── concepts/<layer>/ documents, one directory per layer │ └── concepts/<layer>/ documents, one directory per layer
├── Dockerfile the release image, in three stages ├── Dockerfile the release image, in three stages
├── Dockerfile.dockerignore what the image build is not sent ├── Dockerfile.dockerignore what the image build is not sent
├── docker-compose.yml runs the image with the settings it wants
├── frontend/ Svelte and TypeScript, built by Vite ├── frontend/ Svelte and TypeScript, built by Vite
│ ├── src/lib/ API client, router, components, formatting │ ├── src/lib/ API client, router, components, formatting
│ ├── src/pages/ one component per route │ ├── src/pages/ one component per route

View File

@ -105,7 +105,19 @@ docker run --rm -p 8080:8080 apsis-book:0.1.0
命令中显式给出主机。程序的默认值是回环地址,那里它只回应自己,因此不带这一项启动 命令中显式给出主机。程序的默认值是回环地址,那里它只回应自己,因此不带这一项启动
的容器不会向任何人提供服务。 的容器不会向任何人提供服务。
镜像没有 `HEALTHCHECK`。探测 `GET /health` 是编排系统应当做的事,而在容器内部 `docker-compose.yml` 做同样的事,并带上长时间运行的服务所需要的设置:除非被停止
否则自动重启,因文件系统不被写入而以只读方式挂载,去掉取得更多权限的能力,并且
每三十秒探测一次服务。
```sh
cd apsis-book
docker compose up --build
```
该探针用 shell 写成而不是用某个工具,因为镜像不携带 HTTP 客户端:它只携带一个
二进制,为探针添加客户端会破坏这一点。`bash` 打开连接、写入请求并读回状态行。
镜像本身没有 `HEALTHCHECK`。探测 `GET /health` 是编排系统应当做的事,而在容器内部
探测意味着给一个不需要其他工具的体积里加上 HTTP 客户端。Kubernetes 的探针无须 探测意味着给一个不需要其他工具的体积里加上 HTTP 客户端。Kubernetes 的探针无须
它即可表达同一件事: 它即可表达同一件事:
@ -202,6 +214,7 @@ apsis-book/
│ └── concepts/<layer>/ 文档,每层一个目录 │ └── concepts/<layer>/ 文档,每层一个目录
├── Dockerfile 发布镜像,分三个阶段 ├── Dockerfile 发布镜像,分三个阶段
├── Dockerfile.dockerignore 镜像构建不接收的内容 ├── Dockerfile.dockerignore 镜像构建不接收的内容
├── docker-compose.yml 以长期运行所需的设置来运行镜像
├── frontend/ Svelte 与 TypeScript,由 Vite 构建 ├── frontend/ Svelte 与 TypeScript,由 Vite 构建
│ ├── src/lib/ API 客户端、路由器、组件、格式化 │ ├── src/lib/ API 客户端、路由器、组件、格式化
│ ├── src/pages/ 每个路由一个组件 │ ├── src/pages/ 每个路由一个组件

87
docker-compose.yml Normal file
View File

@ -0,0 +1,87 @@
# Running the release image.
#
# The service keeps nothing between runs: the documents, the graphs, the
# interface catalogue and the frontend are all inside the binary, and the
# only thing it writes is what it sends. The service is therefore mounted
# read-only and can be replaced at any time.
#
# 运行发布镜像。
#
# 服务在两次运行之间不保留任何东西:文档、图谱、接口目录与前端都在二进制里,它
# 唯一写出的东西就是它发送的内容。因此该服务以只读方式挂载,且可以随时被替换。
#
# From this directory:
#
# docker compose up --build
#
# 在本目录执行上面那条命令。
#
# The build context is the directory above, because the crate depends on
# `apsis-rs` by path. A reader who only wants to run a published image can
# drop the `build` section and keep `image`.
#
# 构建上下文是上一级目录,因为该 crate 以路径依赖引用 `apsis-rs`。如果只想运行
# 已发布的镜像,可以删去 `build` 一节而保留 `image`。
name: apsis-book
services:
apsis-book:
build:
context: ..
dockerfile: apsis-book/Dockerfile
image: apsis-book:0.1.0
# The container listens on 8080 and the host side is a matter of taste,
# so it is a variable with a default rather than a constant.
#
# 容器监听 8080,而宿主机一侧是个人偏好,因此它是一个带默认值的变量而不是
# 常量。
ports:
- "${APSIS_BOOK_PORT:-8080}:8080"
# A documentation service has no reason to stop, and a machine that
# restarts should bring it back.
#
# 文档服务没有理由停止,而重启的机器应当把它带回来。
restart: unless-stopped
# The filesystem is not written to, so it is mounted read-only, and the
# process runs as `nobody` with no way to gain more than it has.
#
# 文件系统不被写入,因此以只读方式挂载;进程以 `nobody` 运行,且无法取得超出
# 自身已有的权限。
read_only: true
security_opt:
- no-new-privileges:true
# The probe is written against the shell rather than against a tool,
# because the image carries no HTTP client: it carries one binary and
# nothing else, and adding a client for a probe would undo that.
#
# 该探针用 shell 写成而不是用某个工具,因为镜像不携带 HTTP 客户端:它只携带
# 一个二进制,为探针添加客户端会破坏这一点。
#
# `bash` opens the connection as a file descriptor, the request is written
# to it, and the status line is read back.
#
# `bash` 把连接打开为一个文件描述符,请求写入其中,状态行从中读回。
healthcheck:
test:
- CMD-SHELL
- >-
exec 3<>/dev/tcp/127.0.0.1/8080 || exit 1;
printf 'GET /health HTTP/1.0\r\n\r\n' >&3;
grep -q '200 OK' <&3
interval: 30s
timeout: 5s
# Three failures in a row, which is ninety seconds of a service that is
# not answering, before the container is called unhealthy.
#
# 连续三次失败,即服务九十秒没有回应,才把容器判为不健康。
retries: 3
# The content is parsed at startup and is small, so the service is
# listening within the first second.
#
# 内容在启动时解析且体量很小,因此服务在第一秒内就已监听。
start_period: 5s