diff --git a/README.md b/README.md index 6f39462..9ade256 100644 --- a/README.md +++ b/README.md @@ -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 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 to an image that needs no other tool. A Kubernetes probe says the same thing without one: @@ -230,6 +245,7 @@ apsis-book/ │ └── concepts// documents, one directory per layer ├── Dockerfile the release image, in three stages ├── 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 │ ├── src/lib/ API client, router, components, formatting │ ├── src/pages/ one component per route diff --git a/README.zh.md b/README.zh.md index 91fa81e..bb38328 100644 --- a/README.zh.md +++ b/README.zh.md @@ -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 的探针无须 它即可表达同一件事: @@ -202,6 +214,7 @@ apsis-book/ │ └── concepts// 文档,每层一个目录 ├── Dockerfile 发布镜像,分三个阶段 ├── Dockerfile.dockerignore 镜像构建不接收的内容 +├── docker-compose.yml 以长期运行所需的设置来运行镜像 ├── frontend/ Svelte 与 TypeScript,由 Vite 构建 │ ├── src/lib/ API 客户端、路由器、组件、格式化 │ ├── src/pages/ 每个路由一个组件 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bf7c146 --- /dev/null +++ b/docker-compose.yml @@ -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