Dokku 架构深度解析:基于 Docker 与插件系统的 PaaS 内部工作原理
【免费下载链接】dokkuA docker-powered PaaS that helps you build and manage the lifecycle of applications项目地址: https://gitcode.com/GitHub_Trending/do/dokku
导读:本文面向希望深入理解 Dokku 内部运作机制的开发者和插件贡献者,系统拆解 Dokku 的高层架构、插件系统、命令解析流程、部署流水线与基于文件的状态管理机制。读完本文,你将掌握
dokku命令从 SSH 接收到插件执行完成的完整调用链,理解 trigger(触发器)与 subcommand(子命令)两种插件通信方式,并能在源码层面定位 builder、scheduler、proxy 等核心组件的实现位置,为后续编写自定义插件打下基础。
一、高层架构:一个 Docker 驱动的 PaaS 如何组织
Dokku 是一个基于 Docker 的 Platform as a Service(PaaS),提供类似 Heroku 的部署体验。从 docs/development/architecture.md 的架构图可以看出,整个系统的数据流是一条清晰的单向链路:
+------------------+ | User/Client | +--------+---------+ | | SSH / git push v +--------+---------+ | dokku binary | | (bash script) | +--------+---------+ | | parse_args / execute_dokku_cmd v +--------+---------+ | Plugin System | | (plugn) | +--------+---------+ | | triggers / subcommands v +--------+---------+ +------------------+ | Core Plugins |---->| Docker / K8s | | (apps, git, | | Runtime | | config, etc.) | +------------------+ +------------------+四个核心组件构成了整个系统:
- dokku binary:主入口脚本(仓库根目录的 dokku),负责认证、参数解析,并把命令路由到对应插件;
- Plugin System:基于 plugn 执行触发器、发现命令;
- Core Plugins:实现 Dokku 的全部功能——应用管理(apps)、Git 推送(git)、环境变量(config)、构建后端(builder-)、调度器(scheduler-)、反向代理(*-vhosts)等;
- Runtime:默认通过
scheduler-docker-local使用 Docker,也可通过scheduler-k3s使用 Kubernetes。
从源码看,dokku 脚本的第 5~8 行首先加载/etc/default/dokku系统级默认配置,随后在第 10 行设置DOKKU_ROOT(默认~dokku),并依次 source$DOKKU_ROOT/dokkurc与$DOKKU_ROOT/.dokkurc/*(第 11~28 行)。这些配置加载完成后,脚本在第 68 行引入$PLUGIN_CORE_AVAILABLE_PATH/common/functions公共函数库,然后调用parse_args解析全局参数,最终在第 286 行进入execute_dokku_cmd执行具体命令。这一初始化顺序正是架构图中"配置加载 → 参数解析 → 命令路由"的具体实现。
二、源码目录与运行时目录结构
2.1 源码树:按插件职责组织
仓库的顶层源码结构如下(与 docs/development/architecture.md 中 Source Tree 一致):
dokku/ ├── dokku # Main CLI entry point (bash script) ├── plugins/ # All plugin source code │ ├── apps/ # App management │ ├── git/ # Git push handling │ ├── config/ # Environment variables │ ├── builder-*/ # Build backends (herokuish, pack, dockerfile, etc.) │ ├── scheduler-*/ # Deployment schedulers (docker-local, k3s) │ ├── *-vhosts/ # Proxy implementations (nginx, traefik, caddy, etc.) │ └── common/ # Shared functions and utilities ├── docs/ # Documentation (markdown) ├── debian/ # Debian packaging files ├── contrib/ # Installation scripts and helpers └── tests/ # Integration tests (bats)其中plugins/目录下每一类插件都对应架构图中的一个能力维度:
- apps(plugins/apps):应用生命周期管理;
- git(plugins/git):处理
git push推送; - config(plugins/config):环境变量管理;
- builder-*:构建后端,仓库中实际存在
builder-dockerfile、builder-herokuish、builder-lambda、builder-nixpacks、builder-pack、builder-railpack、builder-null等多个实现; - scheduler-*:部署调度器,包括
scheduler-docker-local(默认)与scheduler-k3s; - *-vhosts:反向代理实现,包括
nginx-vhosts、traefik-vhosts、caddy-vhosts、haproxy-vhosts、openresty-vhosts; - common(plugins/common):共享函数与工具库,是整个插件生态的公共依赖。
2.2 运行时目录:安装后的文件布局
Dokku 安装后会在主机上创建如下目录结构:
/var/lib/dokku/ ├── core-plugins/ # Core plugin binaries (installed from source) │ ├── available/ # All available core plugins │ └── enabled/ # Symlinks to enabled plugins ├── plugins/ # Community plugins │ ├── available/ │ └── enabled/ └── data/ # Plugin data storage └── <plugin>/ # Per-plugin data └── <app>/ # Per-app properties $DOKKU_ROOT (~dokku by default) ├── <app>/ # Per-app data │ ├── refs/ # Git refs │ ├── HEAD # Current git HEAD │ ├── tls/ # SSL certificates │ └── ... # Other app-specific files └── .dokkurc/ # Global configuration overrides这些路径在 dokku 脚本中均有对应定义:PLUGIN_PATH默认指向$DOKKU_LIB_ROOT/plugins(即/var/lib/dokku/plugins),PLUGIN_AVAILABLE_PATH与PLUGIN_ENABLED_PATH分别指向其下的available/与enabled/子目录,PLUGIN_CORE_PATH则指向/var/lib/dokku/core-plugins。enabled 目录中存放的是指向 available 目录的符号链接,这正是插件启用/禁用机制的文件系统基础:启用插件即建立 symlink,禁用即移除。
需要指出的是,现代版本的属性数据实际存放于$DOKKU_LIB_ROOT/config/<plugin>/<app>/而非文档旧图所示的data/目录(详见下文"属性系统"一节的源码依据),二者逻辑结构一致,均为"插件 → 应用 → 属性"的三级组织。
2.3 插件目录布局:统一的约定
每个插件遵循一致的结构(对应 docs/development/architecture.md 的 Plugin Directory Layout):
plugins/<plugin-name>/ ├── plugin.toml # Plugin metadata (description, version) ├── commands # Help output and catch-all command handler ├── subcommands/ # Individual command implementations │ ├── default # Default command (e.g., `dokku apps`) │ └── <command> # Named commands (e.g., `dokku apps:create`) ├── functions # Public functions for other plugins to source ├── internal-functions # Private functions ├── triggers.go # Go-based trigger implementations ├── *.go # Additional Go code └── Makefile # Build configuration以 plugins/apps/plugin.toml 为例,元数据文件内容为:
[plugin] description = "dokku core apps plugin" version = "0.38.27" [plugin.config]版本号在每次发布时由构建流程同步更新。注意,现代核心插件大量使用 Go 编写——plugins/apps 目录下apps.go、subcommands.go、triggers.go、report.go、functions.go均为 Go 源码,配合Makefile编译出可执行文件;同时保留functions、internal-functions等 bash 脚本供其他插件 source。这正是"Bash + Go 混合"设计决策的直接体现。
三、插件系统架构:一切皆是插件
Dokku 的全部功能都通过插件实现。这种架构带来三个关键特性:
- 可扩展性(Extensibility):无需修改核心代码即可添加新功能;
- 松耦合(Loose coupling):插件通过定义良好的 trigger 通信;
- 可组合性(Composability):builder、scheduler、proxy 可以自由混搭。
例如,用户可以在同一套 Dokku 上为不同应用选择不同构建后端(herokuish / pack / dockerfile),也可以把代理从默认的 nginx 切换到 traefik 或 caddy——这些都由插件机制天然支持。
3.1 插件间通信:Trigger 触发系统
插件通过由 plugn 驱动的trigger 系统通信。当一个 trigger 被触发时,plugn 会执行所有已启用插件中同名的匹配脚本:
+---------------+ plugn trigger +---------------+ | Plugin A | -------------------> | Plugin B | | (fires event) | "post-deploy" | (listens for | +---------------+ | event) | +---------------+ | v +---------------+ | Plugin C | | (also listens)| +---------------+Trigger 执行流程:
- 某个插件调用
plugn trigger <trigger-name> [args...]; - plugn 在
$PLUGIN_ENABLED_PATH/*/下搜索名为<trigger-name>的文件; - 每个匹配的可执行脚本都会携带参数被运行;
- Trigger 可以通过 stdout 返回数据,通过退出码报告错误。
关键 trigger 分类(完整清单见 docs/development/plugin-triggers.md):
| Category | Examples | Purpose |
|---|---|---|
| App lifecycle | post-create,pre-delete,post-deploy | React to app events |
| Build | builder-detect,pre-build,builder-build | Control build process |
| Deploy | scheduler-deploy,check-deploy | Manage deployments |
| Proxy | proxy-build-config,nginx-pre-reload | Configure reverse proxy |
| Git | git-pre-pull,receive-app | Handle git operations |
以仓库中实际存在的 plugins/20_events 插件为例,其目录下排列着post-create、pre-delete、post-deploy、builder-detect、scheduler-deploy、receive-app等几十个 trigger 脚本文件——它们正是上述分类表中各 trigger 的真实实现与注册位置。
从 docs/development/plugin-triggers.md 的说明可知,trigger 本质就是可执行脚本,可以使用任意语言编写,只要满足两个条件:可执行、具备运行时依赖。例如实现nginx-hostnametrigger 来反转部署时提供给 nginx 的主机名:
#!/usr/bin/env bash set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x APP="$1"; SUBDOMAIN="$2"; VHOST="$3" NEW_SUBDOMAIN=`echo $SUBDOMAIN | rev` echo "$NEW_SUBDOMAIN.$VHOST"这个脚本名为nginx-hostname且可执行时,就会在正常部署过程中被 Dokku 调用。插件可以通过实现nginx-hostname这样的 trigger 来修改输出或覆盖内部配置。
3.2 从 Go 代码调用 Trigger
Go 编写的插件使用common.CallPlugnTrigger函数触发事件。该函数的完整签名定义在 plugins/common/plugn.go,输入结构体PlugnTriggerInput支持:
Args:传给 trigger 的参数列表;Env:传给 trigger 的环境变量;Stdin/StreamStdio/StreamStdout/StreamStderr:输入与控制输出流行为;DisableStdioBuffer:禁用 stdio 缓冲;PrintCommand:执行前打印命令。
调用示例(对应文档中的代码):
result, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "post-deploy", Args: []string{appName, port, ip, imageTag}, })实现上,CallPlugnTrigger 实际执行plugn trigger <Trigger> [Args...]命令并返回ExecCommandResponse(包含 stdout/stderr 与退出码)。若设置了PrintCommand或环境变量DOKKU_TRACE=1,还会把 trigger 的 stdout/stderr 逐行写入调试日志,便于排查跨插件调用问题。
仓库中有大量真实调用示例,例如 plugins/apps/subcommands.go 在克隆应用时触发post-app-clone-setup:
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "post-app-clone-setup", Args: []string{oldAppName, newAppName}, StreamStdio: true, })四、命令执行流程:一次dokku apps:list的完整旅程
当用户执行ssh dokku@host apps:list时,完整调用链如下(对应 docs/development/architecture.md 的 Command Flow 图):
User runs: ssh dokku@host apps:list | v +-------------------+-------------------+ | dokku bash script | | 1. Source /etc/default/dokku | | 2. Set DOKKU_ROOT, PLUGIN_PATH | | 3. Source common/functions | | 4. Call parse_args() | | 5. Check user permissions | +-------------------+-------------------+ | v +-------------------+-------------------+ | execute_dokku_cmd() | | 1. Handle plugin aliases | | 2. Check for subcommands/default | | 3. Check for subcommands/<cmd> | | 4. Fall back to commands scripts | +-------------------+-------------------+ | v +-------------------+-------------------+ | plugins/apps/subcommands/list | | (executes the actual command) | +-------------------+-------------------+4.1 认证与权限检查
命令到达后首先经过认证环节。dokku_auth()函数定义于 plugins/common/functions:它调用user-authtrigger 进行权限校验。当系统中不存在任何user-authtrigger(或仅存在核心20_events插件内置的实现)时直接放行;否则执行plugn trigger user-auth "$SSH_USER" "$SSH_NAME" "$@",通过各插件的user-auth脚本来决定是否授权。
此外,dokku 脚本中还有一道身份检查:非dokku用户执行命令时,会通过sudo -u dokku -E -H "$0" "$@"自动降权到 dokku 用户重跑;而plugin:*、ssh-keys:add、scheduler-k3s:initialize等管理类命令则要求以 root 身份运行。
4.2 SSH 原始命令的处理
当通过 SSH 执行(环境变量SSH_ORIGINAL_COMMAND非空)时,dokku 会解析原始命令:git-*或git:*形式的命令走 git 专用分支,其余命令通过xargs -n 1切分参数后再次调用自身。这使得ssh dokku@host apps:list与本地dokku apps:list走完全相同的命令解析路径。
4.3 命令解析顺序
execute_dokku_cmd()(定义于 dokku)按如下优先级解析命令:
- 检查
$PLUGIN_ENABLED_PATH/<plugin>/subcommands/default(默认命令,如dokku apps); - 检查
$PLUGIN_ENABLED_PATH/<plugin>/subcommands/<command>(具名命令,如dokku apps:create); - 遍历
$PLUGIN_ENABLED_PATH/*/commands脚本作为兜底(catch-all)。
源码中还包含两层值得注意的细节:
其一,插件别名机制。execute_dokku_cmd开头维护了一张别名表(dokku),例如docker-local→scheduler-docker-local、nginx→nginx-vhosts、herokuish→builder-herokuish、k3s→scheduler-k3s、pack→builder-pack等,用户输入简短名称时自动映射到完整插件名;case分支(dokku)还会把deploy、urls、report等命令路由到00_dokku-standard插件。
其二,退出码约定。兜底的commands脚本以退出码DOKKU_NOT_IMPLEMENTED_EXIT=10表示"本插件未实现该命令"(dokku),此时循环继续尝试下一个插件的commands脚本;若所有插件都返回 10,则最终输出`$*` is not a dokku command.并退出(dokku)。
全局参数解析则由parse_args()(plugins/common/functions)完成,支持--quiet(抑制输出)、--trace(开启 bash trace 调试)、--force(强制删除应用,对应DOKKU_APPS_FORCE_DELETE=1)、--app(指定目标应用并设置DOKKU_APP_NAME)四个全局 flag。
五、部署流水线:一次 git push 的四阶段旅程
当用户执行git push dokku@host:myapp时,Dokku 依次执行接收(Receive)、构建(Build)、发布(Release)、部署(Deploy)四个阶段:
+-------------+ +-------------+ +-------------+ +-------------+ | Receive |--->| Build |--->| Release |--->| Deploy | +-------------+ +-------------+ +-------------+ +-------------+ | | | | v v v v git-hook builder-detect builder-release scheduler-deploy receive-app pre-build core-post-deploy post-extract builder-build post-deploy5.1 阶段一:Receive(接收)
Git 接收阶段处理传入的推送并准备源代码:
git-hook接收推送并校验分支;- 触发
receive-app; - 源代码被解压到临时目录;
- 触发
post-extract允许修改源码。
该阶段对应 plugins/20_events 中的git-hook、receive-app、post-extract等 trigger 脚本。Git 相关的具体实现集中在 plugins/git(含git-from-archive、git-from-directory、git-from-image等部署源处理脚本)。
5.2 阶段二:Build(构建)
构建阶段根据源码创建 Docker 镜像:
builder-detect判定构建器类型(herokuish、pack、dockerfile 等);pre-buildtrigger 执行构建前钩子;builder-build创建 Docker 镜像;post-buildtrigger 执行构建后钩子。
构建器的检测与构建逻辑分散在各builder-*插件中:builder-herokuish、builder-dockerfile、builder-pack、builder-nixpacks、builder-railpack、builder-lambda等均提供了builder-detect与builder-release脚本(见各插件根目录)。
5.3 阶段三:Release(发布)
发布阶段为部署准备镜像:
pre-release-builder允许修改镜像;builder-release在镜像中设置环境变量;post-release-builder执行最终发布钩子。
5.4 阶段四:Deploy(部署)
部署阶段启动容器并配置网络:
scheduler-deploy启动新容器;check-deploy执行健康检查;core-post-deploy将流量切换到新容器;post-deploy执行部署后任务;- 旧容器被 retire(回收)。
调度器相关的核心实现位于 plugins/scheduler-docker-local(默认 Docker 调度器)与 plugins/scheduler-k3s(Kubernetes 调度器),两者都提供scheduler-deploy、scheduler-detect、scheduler-stop、scheduler-run、scheduler-enter、scheduler-logs等完整 trigger 集合。
六、状态管理:以文件为基础,透明且可调试
Dokku 采用基于文件的状态系统,追求简单、透明、易调试:无数据库依赖,任何标准 Unix 工具都能直接检查状态。
6.1 属性系统(Property System)
插件专属配置存储在属性系统中。现代版本的实际存储路径(依据 plugins/common/properties.go 的getPropertyPath/getPluginConfigPath实现)为:
$DOKKU_LIB_ROOT/config/<plugin>/<app>/<property>即默认/var/lib/dokku/config/<plugin>/<app>/<property>——每个属性就是一个普通文件,值为文件内容。读写均通过辅助函数完成:
# Shell fn-plugin-property-write "git" "$APP" "deploy-branch" "main" fn-plugin-property-get "git" "$APP" "deploy-branch"// Go common.PropertyWrite("git", appName, "deploy-branch", "main") common.PropertyGet("git", appName, "deploy-branch")plugins/common/properties.go 中的 API 远比文档示例丰富,从源码可以梳理出完整的能力矩阵:
| 函数 | 作用 |
|---|---|
PropertyWrite/PropertyGet/PropertyGetDefault | 单值属性的读写,支持默认值(L490 / L108 / L177) |
PropertyGetAll/PropertyGetAllByPrefix | 枚举某应用的全部属性或按前缀过滤(L113 / L143) |
PropertyExists/PropertyDelete/PropertyDestroy | 存在性检查、单属性删除、整应用销毁(_all_表示清空整个插件配置)(L101 / L76 / L90) |
PropertyClone | 应用克隆时把一个应用的全部属性复制到新应用(L60) |
PropertyList* | 列表型属性:PropertyListAdd(可指定索引插入)、PropertyListGet、PropertyListSet、PropertyListRemove、PropertyListRemoveByPrefix、PropertyListGetByIndex、PropertyListGetByValue等(L194 起) |
PropertyMap* | Map 型属性,以单个 JSON 文件持久化,key 可含任意字节(如/、换行),提供PropertyMapWrite/PropertyMapGet/PropertyMapSet/PropertyMapDelete/PropertyMapLength(L522 起) |
CommandPropertySet | 命令层入口,校验应用名、校验属性名合法性、支持--global全局属性(L16) |
属性文件权限统一设置为 0600(属主可读写),目录为 0755,保证了多用户环境下的数据安全。
此外,MigrateConfigToProperties(plugins/common/properties.go#L670)提供了从旧版环境变量到属性系统的幂等迁移机制:通过config-get/config-get-globaltrigger 读取旧配置值,写入属性后调用config-unset清理旧变量,并输出"迁移完成,今后请使用dokku <plugin>:set管理"的提示。
6.2 应用级数据
应用特定数据有时存放在$DOKKU_ROOT/<app>/下:
refs/- Git 引用tls/- SSL 证书ENV- 环境变量文件- 容器 ID、端口映射等
例如 plugins/common/functions 的get_app_container_ids就是从$DOKKU_ROOT/$APP/CONTAINER及CONTAINER.<type>.*系列文件中读取容器 ID 的。
文档明确指出:所有非 git 数据正在逐步迁移到属性系统。从源码看,MigrateConfigEntry结构体(plugins/common/properties.go#L649-L665)与migrateLegacyEnvFiles(L714)正是这一迁移运动的实现载体——后者会在升级安装时触发config-migrate-envtrigger,将 0.38 之前遗留的ENV文件搬入 config 属性路径。
6.3 全局配置
全局设置可通过以下层级配置:
/etc/default/dokku- 系统级默认值$DOKKU_ROOT/dokkurc- 用户级配置$DOKKU_ROOT/.dokkurc/*- 附加配置文件
这三层配置的加载顺序与 dokku 脚本完全对应:先 source/etc/default/dokku,再 source$DOKKU_ROOT/dokkurc,最后逐个 source$DOKKU_ROOT/.dokkurc/*。后加载的配置可以覆盖先加载的默认值,这为运维提供了灵活的覆盖手段。
七、关键设计决策
| Decision | Rationale |
|---|---|
| Plugin-based architecture | Enables extensibility without modifying core code. Community plugins can add databases, caching, and other services. |
| Bash + Go hybrid | Bash for orchestration and simple commands; Go for performance-critical operations and complex logic. |
| Trigger system | Loose coupling between plugins. Plugins don't need to know about each other; they just fire and respond to events. |
| File-based state | Simple, transparent, and easy to debug. No database dependency. State can be inspected with standard Unix tools. |
| Docker as foundation | Leverages Docker's container runtime, networking, and image management. Allows multiple scheduler backends. |
从仓库实际情况看,这五项决策均有直接源码支撑:
- 插件化架构:
plugins/下 40 余个插件目录 +plugn触发系统,社区插件与核心插件同构共存; - Bash + Go 混合:主入口 dokku 与 plugins/common/functions 是 bash,而 apps、config、network、ports、ps、proxy、registry、repo、scheduler、storage 等核心插件均已迁移为 Go 实现(对应各插件目录下的
*.go与Makefile); - Trigger 系统:核心
20_events插件集中承载了上百个 trigger 脚本,插件之间通过事件解耦; - 文件状态:
/var/lib/dokku/config/<plugin>/<app>/<property>的纯文件属性系统,PropertyGetAll等函数直接用os.ReadDir枚举目录即可列出全部属性; - Docker 基础:
DOKKU_GLOBAL_BUILD_ARGS/DOKKU_GLOBAL_RUN_ARGS(dokku)将org.label-schema等标签注入每次 build/run,同时scheduler-docker-local与scheduler-k3s的存在证明运行时后端可替换。
八、延伸阅读
以下文档与源码可帮助你进一步深入:
- docs/development/plugin-creation.md - 如何创建自定义插件
- docs/development/plugin-triggers.md - 全部可用 trigger 的完整清单与示例
- docs/development/testing.md - 如何测试 Dokku 与插件(对应仓库
tests/下的 bats 测试) - docs/deployment/application-deployment.md - 面向用户的部署指南
- docs/deployment/builders/builder-management.md - 可用的构建后端
- docs/deployment/schedulers/scheduler-management.md - 可用的调度器后端
源码层面建议优先阅读 dokku(主入口与命令路由)、plugins/common/plugn.go(trigger 调用封装)与 plugins/common/properties.go(属性系统),这三处是理解整个架构的钥匙。
【免费下载链接】dokkuA docker-powered PaaS that helps you build and manage the lifecycle of applications项目地址: https://gitcode.com/GitHub_Trending/do/dokku
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考