Dokku 架构深度解析:基于 Docker 与插件系统的 PaaS 内部工作原理
2026/9/10 13:52:08 网站建设 项目流程

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-dockerfilebuilder-herokuishbuilder-lambdabuilder-nixpacksbuilder-packbuilder-railpackbuilder-null等多个实现;
  • scheduler-*:部署调度器,包括scheduler-docker-local(默认)与scheduler-k3s
  • *-vhosts:反向代理实现,包括nginx-vhoststraefik-vhostscaddy-vhostshaproxy-vhostsopenresty-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_PATHPLUGIN_ENABLED_PATH分别指向其下的available/enabled/子目录,PLUGIN_CORE_PATH则指向/var/lib/dokku/core-pluginsenabled 目录中存放的是指向 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.gosubcommands.gotriggers.goreport.gofunctions.go均为 Go 源码,配合Makefile编译出可执行文件;同时保留functionsinternal-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 执行流程

  1. 某个插件调用plugn trigger <trigger-name> [args...]
  2. plugn 在$PLUGIN_ENABLED_PATH/*/下搜索名为<trigger-name>的文件;
  3. 每个匹配的可执行脚本都会携带参数被运行;
  4. Trigger 可以通过 stdout 返回数据,通过退出码报告错误。

关键 trigger 分类(完整清单见 docs/development/plugin-triggers.md):

CategoryExamplesPurpose
App lifecyclepost-create,pre-delete,post-deployReact to app events
Buildbuilder-detect,pre-build,builder-buildControl build process
Deployscheduler-deploy,check-deployManage deployments
Proxyproxy-build-config,nginx-pre-reloadConfigure reverse proxy
Gitgit-pre-pull,receive-appHandle git operations

以仓库中实际存在的 plugins/20_events 插件为例,其目录下排列着post-createpre-deletepost-deploybuilder-detectscheduler-deployreceive-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:addscheduler-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)按如下优先级解析命令:

  1. 检查$PLUGIN_ENABLED_PATH/<plugin>/subcommands/default(默认命令,如dokku apps);
  2. 检查$PLUGIN_ENABLED_PATH/<plugin>/subcommands/<command>(具名命令,如dokku apps:create);
  3. 遍历$PLUGIN_ENABLED_PATH/*/commands脚本作为兜底(catch-all)。

源码中还包含两层值得注意的细节:

其一,插件别名机制execute_dokku_cmd开头维护了一张别名表(dokku),例如docker-localscheduler-docker-localnginxnginx-vhostsherokuishbuilder-herokuishk3sscheduler-k3spackbuilder-pack等,用户输入简短名称时自动映射到完整插件名;case分支(dokku)还会把deployurlsreport等命令路由到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-deploy

5.1 阶段一:Receive(接收)

Git 接收阶段处理传入的推送并准备源代码:

  1. git-hook接收推送并校验分支;
  2. 触发receive-app
  3. 源代码被解压到临时目录;
  4. 触发post-extract允许修改源码。

该阶段对应 plugins/20_events 中的git-hookreceive-apppost-extract等 trigger 脚本。Git 相关的具体实现集中在 plugins/git(含git-from-archivegit-from-directorygit-from-image等部署源处理脚本)。

5.2 阶段二:Build(构建)

构建阶段根据源码创建 Docker 镜像:

  1. builder-detect判定构建器类型(herokuish、pack、dockerfile 等);
  2. pre-buildtrigger 执行构建前钩子;
  3. builder-build创建 Docker 镜像;
  4. post-buildtrigger 执行构建后钩子。

构建器的检测与构建逻辑分散在各builder-*插件中:builder-herokuishbuilder-dockerfilebuilder-packbuilder-nixpacksbuilder-railpackbuilder-lambda等均提供了builder-detectbuilder-release脚本(见各插件根目录)。

5.3 阶段三:Release(发布)

发布阶段为部署准备镜像:

  1. pre-release-builder允许修改镜像;
  2. builder-release在镜像中设置环境变量;
  3. post-release-builder执行最终发布钩子。

5.4 阶段四:Deploy(部署)

部署阶段启动容器并配置网络:

  1. scheduler-deploy启动新容器;
  2. check-deploy执行健康检查;
  3. core-post-deploy将流量切换到新容器;
  4. post-deploy执行部署后任务;
  5. 旧容器被 retire(回收)。

调度器相关的核心实现位于 plugins/scheduler-docker-local(默认 Docker 调度器)与 plugins/scheduler-k3s(Kubernetes 调度器),两者都提供scheduler-deployscheduler-detectscheduler-stopscheduler-runscheduler-enterscheduler-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(可指定索引插入)、PropertyListGetPropertyListSetPropertyListRemovePropertyListRemoveByPrefixPropertyListGetByIndexPropertyListGetByValue等(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/CONTAINERCONTAINER.<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/*后加载的配置可以覆盖先加载的默认值,这为运维提供了灵活的覆盖手段。

七、关键设计决策

DecisionRationale
Plugin-based architectureEnables extensibility without modifying core code. Community plugins can add databases, caching, and other services.
Bash + Go hybridBash for orchestration and simple commands; Go for performance-critical operations and complex logic.
Trigger systemLoose coupling between plugins. Plugins don't need to know about each other; they just fire and respond to events.
File-based stateSimple, transparent, and easy to debug. No database dependency. State can be inspected with standard Unix tools.
Docker as foundationLeverages 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 实现(对应各插件目录下的*.goMakefile);
  • 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-localscheduler-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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询