# 🐳 dockerctl — 强化版 Docker 运维工具包
**类型化 Python CLI · 声明式强化 compose stack · 供应链安全门控 CI**
*类型化 Python CLI · 声明式强化 compose stack · 供应链安全门控 CI*
[](https://github.com/www8351/Docker-DevOps-Tooling/actions/workflows/ci.yml)
[](https://github.com/www8351/Docker-DevOps-Tooling/actions/workflows/release.yml)
[](https://github.com/www8351?tab=packages&repo_name=Docker-DevOps-Tooling)






## 🌍 这是什么? · 这是什么?
|
### 🇬🇧 中文
一个生产级的 Docker 工具包:**compose 负责管理状态**,**类型化 Python CLI** 负责
动态操作,**Taskfile** 负责将它们粘合在一起,而 **CI 负责执行所有检查**。每个
长时间运行的容器都是 **只读、非 root、已丢弃 capabilities 并且经过 healthcheck** 的。
|
### 🇮🇱 中文
一个生产级的 Docker 工具包:**compose 负责管理状态**,**类型化 Python CLI** 负责
动态操作,**Taskfile** 负责将它们粘合在一起,而 **CI 负责执行所有检查**。每个
长时间运行的容器都是 **只读、非 root、已丢弃 capabilities 并且经过 healthcheck** 的。
|
```
$ docker compose ps
NAME IMAGE STATUS PORTS
compose-counter-1 compose-counter Up 18 seconds (healthy)
compose-httpd-1 httpd:2.4-alpine Up 18 seconds (healthy) 0.0.0.0:8081->80/tcp
compose-nginx-1 nginx:1.27-alpine Up 18 seconds (healthy) 0.0.0.0:8080->80/tcp
compose-portainer-1 portainer/portainer-ce:2.27.9 Up 18 seconds 0.0.0.0:9000->9000/tcp
```
## ⚡ 核心亮点
- **78 MB Ubuntu → ~7 MB 强化版 Alpine** — counter 镜像的[前后对比](#-counter--from-78-mb-of-attack-surface-to-a-few-mb)
- **只读容器,`cap drop: ALL`,`no-new-privileges`** — 参见 [`compose/docker-compose.yml`](compose/docker-compose.yml)
- **SHA 固定 actions,Trivy 安全门控,CycloneDX SBOMs** — 参见 [`ci.yml`](.github/workflows/ci.yml)
- **类型化 CLI:mypy strict,45 个测试,100% 覆盖率** — 参见 [`cli/`](cli/)
- **已发布 [v0.2.0](https://github.com/www8351/Docker-DevOps-Tooling/releases/tag/v0.2.0)**,包含 GHCR 上的 semver 镜像 + [CHANGELOG](CHANGELOG.md)
## 💡 为什么会有这个项目
这个仓库最初是几年前为了学习 Docker 而编写的一套**交互式 bash 菜单**。
它们在敲击键盘时很好用,但非常脆弱且无法实现自动化。现在一切都
被重建为声明式配置、标志驱动的 CLI 以及每次 push 时的 CI 验证。
最终结果是**非交互式、可重现且 pipeline 安全的**。
## 🏗 架构
```
flowchart TD
Dev([👩💻 Engineer / CI]) --> Task[Taskfile.yml
single entrypoint]
Task --> Compose[docker compose
infrastructure + state]
Task --> CLI[dockerctl
dynamic ops + logic]
Compose --> Nginx[nginx
:8080]
Compose --> Httpd[httpd
:8081]
Compose --> Portainer[portainer
:9000]
Compose --> Counter[counter
custom image]
Nginx -. mounts .-> Web[web/index.html]
Counter -. builds .-> Img[images/counter/]
Compose -. profile: observability .-> Obs[prometheus :9090 · grafana :3000
cadvisor · nginx-exporter]
Obs -. scrapes .-> Nginx
CLI --> Docker[(Docker Engine)]
```
**关注点分离** — 每个工具只负责一项工作:
| 层级 | 职责 | 工具 |
|-------|------|------|
| 🧱 基础设施 | 容器生命周期、网络、卷、状态 | `docker-compose` |
| 🐍 逻辑 | 动态镜像/容器操作、错误处理 | Python + Typer |
| ▶️ 编排 | 日常命令 | Taskfile |
| 🤖 质量门禁 | 验证、无人工干预 | GitHub Actions |
## 📂 项目布局
```
.
├── compose/
│ ├── docker-compose.yml # declarative stack (nginx, httpd, portainer, counter)
│ └── .env.example # ports + image tags → copy to .env
├── images/
│ └── counter/ # custom image, built by compose
│ ├── Dockerfile # multi-stage, hardened, Alpine, non-root
│ └── counter.sh # POSIX sh (no bash needed)
├── web/
│ └── index.html # served by nginx from a read-only mount
├── cli/ # dockerctl — non-interactive Python CLI
│ ├── Dockerfile # multi-stage build → slim Alpine, non-root
│ ├── .dockerignore
│ ├── pyproject.toml
│ └── dockerctl/
│ ├── __main__.py # entrypoint + subcommand wiring
│ ├── _docker.py # typed wrapper around the docker CLI
│ ├── images.py # pull / rm
│ └── containers.py # ls / rm / deploy
├── Taskfile.yml # engineering entrypoint
└── .github/workflows/ci.yml # lint + validate on every push
```
## ✅ 环境要求
| 工具 | 原因 | 安装 |
|------|-----|---------|
| Docker Engine + Compose v2 | 运行 stack |
|
| Python ≥ 3.10 | 运行 `dockerctl` |
|
| Task (可选) | 任务运行器 |
|
## 🚀 快速开始
```
# 配置 ports / image tags
cp compose/.env.example compose/.env
# 在后台启动所有内容
task up # or: docker compose -f compose/docker-compose.yml up -d
# 打开服务
# nginx → http://localhost:8080
# httpd → http://localhost:8081
# portainer → http://localhost:9000
# 销毁
task down # or: docker compose -f compose/docker-compose.yml down
```
## 🧱 Stack (docker-compose)
定义在 [`compose/docker-compose.yml`](compose/docker-compose.yml) 中。端口和标签
来自 `compose/.env`(绝不硬编码);标签默认**固定**。
| 服务 | 镜像 | 默认端口 | 健康检查 | 网络 | 备注 |
|---------|-------|--------------|--------|---------|-------|
| `nginx` | `nginx:1.27-alpine` | `8080:80` | `wget` 探针 | `web` | 只读服务 `web/` |
| `httpd` | `httpd:2.4-alpine` | `8081:80` | `wget` 探针 | `web` | 替换旧的不存在的 `apache2` |
| `portainer` | `portainer/portainer-ce:2.27.9` | `9000:9000` | — (scratch 镜像) | `mgmt` | Docker UI,替换已失效的 `dockerui` |
| `counter` | 基于 `images/counter/` 构建 | — | `pgrep` 探针 | `web` | 递增计数器;强化版 Alpine,非 root(参见[套件](#-devops-tooling--containerization-suite)) |
每个服务都以**只读根文件系统**(已知的写入路径使用 tmpfs)、**丢弃所有 capabilities**(Web 服务器仅重新添加 80 端口所需权限)、`no-new-privileges`、CPU/内存限制以及有界的 json-file 日志方式运行。
`mgmt` 网络将 docker.sock 消费者与面向公网的层级隔离开来。
Alpine 标签是承重的:busybox `wget` 为健康检查提供支持。
### 📈 可观测性概况
```
docker compose -f compose/docker-compose.yml --profile observability up -d --wait
# prometheus → http://localhost:9090 grafana → http://localhost:3000
```
| 服务 | 镜像 | 端口 | 角色 |
|---------|-------|------|------|
| `prometheus` | `prom/prometheus:v3.13.0` | `9090` | 每 15s 抓取一次 cadvisor + nginx-exporter |
| `grafana` | `grafana/grafana:12.3.8` | `3000` | **预配置**的数据源 + 已提交的[容器仪表板](compose/grafana/dashboards/containers.json) — 零点击操作 |
| `cadvisor` | `gcr.io/cadvisor/cadvisor:v0.55.1` | — | 每个容器的 CPU/内存/网络指标 |
| `nginx-exporter` | `nginx/nginx-prometheus-exporter:1.5.1` | — | nginx `stub_status` → Prometheus 指标 |
这四个组件都遵循相同的强化基准(只读,`cap_drop: ALL`,
`no-new-privileges`,内存限制,固定标签)。CI 也会启动此配置,如果任何 Prometheus 目标宕机,则构建失败。故意排除在范围之外的:
Alertmanager 和 Loki。
```
task build # docker compose --profile tools build (counter + dockerctl)
task pull # docker compose pull
task logs # docker compose logs -f
task clean # down + remove volumes
```
## 🛡 DevOps 工具与容器化套件
两个专门构建的镜像,每个都作为在**最小化 Alpine 基础上的多阶段构建**,并由**非特权用户**运行。目标是体积小、可重现且攻击面低 —
构建工具链永远不会进入最终镜像,root 同样不会。
| 镜像 | 源码 | 基础镜像 | 多阶段布局 | 运行身份 |
|-------|--------|------|--------------------|---------|
| `counter` | [`images/counter/`](images/counter/) | `alpine:3.20` | `lint` (shellcheck 门控) → `runtime` | 非 root `app` (UID 10001) |
| `dockerctl` | [`cli/`](cli/) | `python:3.12-alpine` | `builder` (通过 `python:3.12-slim` 构建 venv) → `runtime` | 非 root `app` (UID 10001) |
### 🔢 `counter` — 从 78 MB 的攻击面到几 MB
原始镜像在 `ubuntu:24.04` 上运行一个简单的 bash 循环,同时安装了
`openssh-server`、`sshpass`、`tcpdump`、`net-tools` 和 `python3` — **这些脚本根本没用到**。
仅 `sshpass` 和 `tcpdump` 就正是镜像扫描器首先标记的目标。
| | 之前 ❌ | 之后 ✅ |
|--|----------|---------|
| 基础镜像 | `ubuntu:24.04` (~78 MB) | `alpine:3.20` (~7 MB) |
| 额外包 | ssh, sshpass, tcpdump, net-tools, python3 | 无 |
| 用户 | root | 非 root `app` (UID/GID 10001) |
| 阶段 | 1 | 2 (lint 门控 → runtime) |
| 脚本 | bash + 失效的 `/var/log/my` 符号链接 | POSIX `sh`,无需 bash |
**强化选择**
- **构建时 lint 门控** — 阶段 1 对 `counter.sh` 运行 `shellcheck`;损坏的脚本
会导致 `docker build` 失败,因此糟糕的 shell 脚本永远不会进入镜像。
- **仅携带运行所需内容** — runtime 阶段仅复制经验证的脚本;旧镜像中的所有
包都不复存在。
- **构造上即非 root** — 固定 ID 的 `app` 用户不拥有任何它不需要的权限;该脚本
以仅读和执行权限 (`0555`) 安装。
- **POSIX `sh`** — 重写唯一的 bashism (`((counter++))` → `$((counter + 1))`) 让它可以在
Alpine 的 busybox `ash` 上运行,因此完全不安装 `bash` 包。
### 🐍 `dockerctl` — 多阶段 Python,runtime 中无工具链
```
docker compose --profile tools build dockerctl # or: docker build -t dockerctl cli
docker run --rm dockerctl version # → 0.1.0 (no socket needed)
docker run --rm dockerctl images --help
# 通过挂载 host socket 来驱动真实引擎:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
dockerctl images pull nginx:latest
```
**强化选择**
- **Builder/runtime 分离** — `builder` 阶段使用完整的 `python:3.12-slim` 将
项目安装到 `/opt/venv` 的独立 venv 中;runtime 阶段仅复制该 venv,因此 pip、
构建工具和缓存永远不会被打包进去。
- **最小化 runtime** — `python:3.12-alpine` 加上仅有的 `docker-cli`(客户端,非 daemon),
因为 `dockerctl` 需要 shell out 调用 `docker`(参见 [`cli/dockerctl/_docker.py`](cli/dockerctl/_docker.py))。
- **非 root `app` 用户**,并通过 [`cli/.dockerignore`](cli/.dockerignore) 精简构建上下文
(测试、缓存和 Dockerfile 本身已被排除)。
- **不内置 socket** — 引擎仅在运行时通过显式挂载的
`/var/run/docker.sock` 访问,因此镜像本身不赋予自身任何权限。
### 🔎 在 CI 中扫描
两个镜像在每次 push 时都会由 **Trivy** 构建和扫描(参见
[CI](#-continuous-integration))。扫描**对 HIGH/CRITICAL 级别的问题进行门控**
(`ignore-unfixed: true`,因此无法修补的上游基础镜像 CVE 不会中断构建)。
## 🐍 CLI (dockerctl)
一个小巧的**非交互式** CLI,用于处理 compose 不应拥有的
动态操作(临时拉取、删除、一次性部署)。仅使用标志 — 没有提示 — 因此它在 CI 中是安全的。
每个命令都返回有意义的**退出码**并将错误写入 stderr。
```
pip install -e cli # installs the `dockerctl` command
```
```
dockerctl containers deploy nginx:latest -n web -p 8080:80
dockerctl containers stats --json
dockerctl images push ghcr.io/acme/counter:1.0
dockerctl --version # or -V, or the `version` subcommand
```
完整命令参考 (2 个组,14 条命令)
| 命令 | 替代项 | 示例 |
|---------|----------|---------|
| `images ls [--json]` | 菜单列表 | `dockerctl images ls --json` |
| `images build -t ` | 构建菜单 | `dockerctl images build images/counter -t counter:dev` |
| `images pull [` | `pull_images.sh` | `dockerctl images pull nginx:latest` |
| `images rm [-f]` | `remove_images.sh` | `dockerctl images rm nginx:latest` |
| `images tag ` | — | `dockerctl images tag counter: ghcr.io/acme/counter:1.0` |
| `images push ][` | — | `dockerctl images push ghcr.io/acme/counter:1.0` |
| `containers ls [-a] [--json]` | 菜单列表 | `dockerctl containers ls --all` |
| `containers rm [-f]` | `remove_Container.sh` | `dockerctl containers rm web --force` |
| `containers deploy ]
` | 部署菜单 | `dockerctl containers deploy nginx:latest -n web -p 8080:80` |
| `containers logs / stop / start / restart` | 菜单操作 | `dockerctl containers restart -t 5 web` |
| `containers exec / inspect / stats` | 菜单操作 | `dockerctl containers stats --json` |
`dockerctl --help` 会打印完整的命令树;每个组都有各自的 `--help`。
CLI **完全符合 mypy-strict**,并且其测试套件**覆盖门控为
90%**(目前为 100%);版本号单方面源自 `pyproject.toml`。
## ▶️ 任务运行器
[`Taskfile.yml`](Taskfile.yml) 是所有人(以及 CI)使用的唯一入口。
```
task # list all tasks
task up # start the stack
task down # stop + remove
task build # build images
task pull # pull images
task logs # tail logs
task clean # down + volumes
task lint # run every linter locally
task cli -- images pull nginx:latest # proxy to dockerctl
```
## 🤖 持续集成
[`.github/workflows/ci.yml`](.github/workflows/ci.yml) 在每次 push 和 pull request 时运行 —
**完全无人值守,没有任何提示** — 在最小权限下运行,并且
每个 action 都**固定到提交 SHA**,同时具有按分支的并发控制:
**`lint` 任务**
1. ✅ **Compose** — `docker compose config` 验证 stack
2. 🐳 **Dockerfiles** — `hadolint` 检查 `images/counter/Dockerfile` 和 `cli/Dockerfile`
3. 🐚 **Shell** — 对 shell 脚本运行 `shellcheck`
4. 🐍 **Python** — `ruff` lint + 格式检查
5. 🔍 **Types** — strict 模式下的 `mypy`
6. 📦 **Tests** — 带有 90% 覆盖率门控的 `pytest`
**`image` 任务** (矩阵: counter, dockerctl)
- 通过 buildx 构建,利用 GitHub Actions **层缓存**
- 使用 **Trivy** 扫描(门控 HIGH/CRITICAL,`ignore-unfixed`)
- 为每个镜像生成 **CycloneDX SBOM**,并作为 artifact 上传
**`publish` 任务** (仅 push 到 `main` 时)
- 将两个镜像推送到 **GHCR**,标记为 `latest` + 短提交 SHA
[Dependabot](.github/dependabot.yml) 每周保持 GitHub Actions、pip 和
Dockerfiles 的更新。
## 🚢 发布与已发布的镜像
打上 `vX.Y.Z` 标签(与 `cli/pyproject.toml` 匹配 — workflow 会对此进行断言)
会触发 [`release.yml`](.github/workflows/release.yml):在 GHCR 上发布带有 semver 标签的镜像,
并自动生成 GitHub Release。精选的更新记录在
[CHANGELOG.md](CHANGELOG.md) 中。
```
docker pull ghcr.io/www8351/docker-devops-tooling/dockerctl:0.2.0
docker pull ghcr.io/www8351/docker-devops-tooling/counter:0.2.0
```
## 🎯 设计原则
- **声明式优于命令式** — compose 描述了期望的状态;我们不编写 `docker run` 脚本。
- **无交互** — 仅使用标志和环境变量,因此一切都能在 pipeline 中运行。
- **有意义的退出码** — 失败会明确报错并停止 CI。
- **单一入口点** — Taskfile,这样没人需要记住冗长的命令。
- **物尽其用** — compose 用于基础设施,Python 用于逻辑,Task 用于粘合,CI 用于强制执行。
## 🧰 故障排除
常见问题与修复
| 症状 | 修复 |
|---------|-----|
| `task: command not found` | 安装 Task,或者使用上面提到的 `docker compose …` 等效命令。 |
| 端口已被占用 | 更改 `compose/.env` 中的端口,然后重新运行 `task up`。 |
| 页面看起来没有样式 | 强制刷新;`web/index.html` 是实时挂载的(无需重新构建)。 |
| `dockerctl: command not found` | 首先运行 `pip install -e cli`。 |
| docker socket 权限被拒绝 | 将您的用户添加到 `docker` 组,或使用 `sudo` 运行。 |
| `dockerctl` 容器无法连接到 socket | 该镜像是非 root 的;为其赋予 socket 的组权限:`docker run --group-add "$(stat -c '%g' /var/run/docker.sock)" …` |
| 服务不健康 / 不断崩溃重启 | 检查 `docker compose ps` + `task logs`;该 stack 以只读方式运行 — 请参阅 compose 文件中的 tmpfs 列表。 |
## 🤝 贡献 / 安全 / 更新日志
- [CONTRIBUTING.md](CONTRIBUTING.md) — 设置、质量门控、约定、发布步骤
- [SECURITY.md](SECURITY.md) — 报告漏洞、安全态势
- [CHANGELOG.md](CHANGELOG.md) — 精选内容,Keep-a-Changelog 格式