news 2026/9/20 5:21:06

Phoenix 应用部署到 Heroku 完整实战指南:Buildpack 与 Container 双方案详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Phoenix 应用部署到 Heroku 完整实战指南:Buildpack 与 Container 双方案详解

Phoenix 应用部署到 Heroku 完整实战指南:Buildpack 与 Container 双方案详解

【免费下载链接】phoenixPeace of mind from prototype to production项目地址: https://gitcode.com/gh_mirrors/ph/phoenix

本指南基于 Phoenix 官方部署文档 guides/deployment/heroku.md 编写,目标是把一个可运行的 Phoenix 应用完整部署到 Heroku 平台。你将掌握:通过 Elixir Buildpack 与 Container 容器栈两种方式部署的完整流程、elixir_buildpack.config/Procfile/heroku.yml等关键配置文件、生产环境 SSL 与 WebSocket 超时的正确配置,以及环境变量、数据库迁移与常见故障排查的实战技巧。

前置条件:一个可工作的 Phoenix 应用

本指南唯一需要的硬性条件是一个本地可运行的 Phoenix 应用。如果你还没有应用可以部署,请先跟随 Up and Running 入门指南 创建一个基础应用再继续。

在动手部署前,也建议先通读 部署概述(Deployment 简介),它梳理了生产部署的三步主线:应用密钥(secrets)处理 → 静态资源编译 → 生产环境启动服务器。Heroku 指南正是这条主线在 PaaS 平台上的具体落地。

认识平台限制:Heroku 与 Elixir/Phoenix 的边界

Heroku 是一个优秀的平台,Elixir 在其上运行良好。但如果你打算使用 Elixir 和 Phoenix 的进阶特性,需要注意以下限制:

  • 连接数受限:Heroku 限制了同时连接数与每个连接的持续时间。Elixir 常被用于实时应用,这类应用需要大量并发长连接——Phoenix 官方博客曾展示过单服务器支撑超 200 万 WebSocket 连接的场景,而 Heroku 的路由层会限制这种能力。
  • 无法分布式集群:Heroku 的防火墙将 dyno 相互隔离,这意味着分布式 Phoenix Channels、分布式任务等能力无法直接依赖 Erlang 内建分发,需要借助 Redis 之类的中间件。
  • 内存态数据每 24 小时丢失:Agent、GenServer、ETS 中保存的内存状态,会因 Heroku 每 24 小时强制重启 dyno 而全部丢失(无论节点是否健康)。
  • 内建 observer 不可用:Heroku 虽然允许连接进 dyno,但你无法用 observer 观察 dyno 内部状态。

什么时候 Heroku 够用?如果你刚起步,或并不依赖上述特性,Heroku 完全足够。例如:把运行在 Heroku 上的现有应用迁移到 Phoenix,且功能集相近时,Elixir 的表现会与现有技术栈相当甚至更好。如果需要一个没有这些限制的 PaaS,官方文档侧边栏列有其他替代方案;如果倾向自管云主机(EC2、Google Cloud 等),建议改用mix release部署,详见 releases 指南(其中包含可直接使用的示例 Dockerfile,仓库模板见 priv/templates/phx.gen.release/Dockerfile.eex)。

部署步骤总览

整个流程可以拆解为以下几步,便于跟踪进度:

  1. 初始化 Git 仓库
  2. 注册 Heroku 账号
  3. 安装 Heroku Toolbelt(CLI)
  4. 创建并配置 Heroku 应用
  5. 让项目适配 Heroku
  6. 正式部署
  7. 常用 Heroku 命令

初始化 Git 仓库

Heroku 通过 Git 推送代码完成部署,因此在推送之前,需要先在项目目录初始化本地 Git 仓库并提交文件:

$ git init $ git add . $ git commit -m "Initial commit"

注册 Heroku 账号

前往 Heroku 官网注册页填写表单即可完成注册。Free 计划会提供 1 个 web dyno 和 1 个 worker dyno,并附带免费的 PostgreSQL 与 Redis 实例——这些资源定位是测试与开发,存在各种限制;要运行生产应用,请升级到付费套餐。

安装 Heroku Toolbelt

注册完成后,下载对应系统的 Heroku Toolbelt。其中包含的Heroku CLI非常有用,可以完成:创建 Heroku 应用、列出某应用正在运行的 dyno、实时查看日志(tail logs)、以及运行一次性命令(例如执行 mix 任务)。

方式一:通过 Buildpack 创建并配置 Heroku 应用

在 Heroku 上部署 Phoenix 应用有两条路线:使用 Heroku buildpacks,或使用其 container 栈。两者的核心区别在于如何告诉 Heroku 处理构建过程:

  • Buildpack 路线:需要在 Heroku 上配置 Phoenix/Elixir 专属 buildpack,由平台按既定流程编译;
  • Container 路线:通过Dockerfileheroku.yml完全自定义容器镜像,对应用组装拥有更多控制权(通常建议配合 release 使用,后面详述)。

本节先深入 buildpack 路线。

创建应用:两个必备 Buildpack

Buildpack 是一种打包框架/运行时支持的便捷机制。Phoenix 需要两个 buildpack 才能在 Heroku 上运行:第一个提供基础 Elixir 支持,第二个补充 Phoenix 专属命令。

安装好 Toolbelt 后,用最新版 Elixir buildpack 创建应用:

$ heroku create --buildpack hashnuke/elixir Creating app... done, ⬢ mysterious-meadow-6277 Setting buildpack to hashnuke/elixir... done https://mysterious-meadow-6277.herokuapp.com/ | https://git.heroku.com/mysterious-meadow-6277.git

几个要点说明:

  • 首次使用 Heroku 命令时可能会提示登录,输入注册时的邮箱和密码即可;
  • 输出中 "Creating" 后面的随机字符串(mysterious-meadow-6277)就是应用名,每次创建都会不同;
  • 输出里的 URL 就是应用地址,现在在浏览器打开会看到 Heroku 默认欢迎页;
  • 如果执行heroku create之前没有初始化 Git 仓库,此时 Heroku 远程仓库不会自动配置好,需要手动执行heroku git:remote -a [your-app-name]补上。

固定 Elixir / Erlang 版本:elixir_buildpack.config

Buildpack 内置了一套预定义的 Elixir 和 Erlang 版本。为避免部署时出现意外,最佳实践是在elixir_buildpack.config中显式声明与开发环境、CI 环境一致的版本。在项目根目录创建该文件:

# Elixir version elixir_version=1.18.4 # Erlang version # https://github.com/HashNuke/heroku-buildpack-elixir-otp-builds/blob/master/otp-versions erlang_version=27.0 # Invoke assets.deploy defined in your mix.exs to deploy assets with esbuild # Note we nuke the esbuild executable from the image hook_post_compile="eval mix assets.deploy && rm -f _build/esbuild*"

这里hook_post_compile调用的mix assets.deploy是 Phoenix 项目mix.exs中定义的 alias——它负责编译资源并生成带摘要的静态清单文件,供生产环境快速服务资源(参见 部署概述 中“编译应用资源”一节;本仓库的 alias 定义见 mix.exs)。

声明启动方式:Procfile

接着在项目根目录创建Procfile,告诉 buildpack 如何启动 Web 服务器:

web: mix phx.server

可选:Node / npm 与 Phoenix Static buildpack

默认情况下,Phoenix 使用esbuild替你管理全部前端资源。但如果你用的是nodenpm,就需要额外安装 Phoenix Static buildpack 来处理它们:

$ heroku buildpacks:add https://github.com/gigalixir/gigalixir-buildpack-phoenix-static.git Buildpack added. Next release on mysterious-meadow-6277 will use: 1. https://github.com/HashNuke/heroku-buildpack-elixir.git 2. https://github.com/gigalixir/gigalixir-heroku-buildpack-phoenix-static.git

使用该 buildpack 时,应把资源打包全部委托给npm,因此需要elixir_buildpack.config中移除hook_post_compile,把它挪到assets/package.json的 deploy 脚本里:

{ ... "scripts": { "deploy": "cd .. && mix assets.deploy && rm -f _build/esbuild*" } ... }

同样地,Phoenix Static buildpack 内置了预定义 Node.js 版本,为消除部署差异,请在项目根目录创建phoenix_static_buildpack.config显式声明版本:

# Node.js version node_version=10.20.1

(完整配置项请参考该 buildpack 的官方文档;你也可以编写自定义构建脚本,这里使用其默认脚本即可。)

最后注意:由于使用了多个 buildpack,可能出现顺序错乱的问题(Elixir buildpack 必须先于 Phoenix Static buildpack 运行)。请务必确保Phoenix Static buildpack 排在最后

让项目适配 Heroku:SSL、Host 与 WebSocket 超时

每个新 Phoenix 项目都会自带config/runtime.exs,它在**启动时(boot time)**从环境变量加载配置和密钥——这与 Heroku 的最佳实践(12-factor app)天然契合。参考仓库模板 installer/templates/phx_single/config/runtime.exs.eex 可以看到:SECRET_KEY_BASE缺失时应用会直接raise并提示用mix phx.gen.secret生成;PHX_HOST默认回落到example.com;HTTP 端口从PORT环境变量读取。因此剩余工作主要是配置 URL 与 SSL。

第一步:强制 HTTPS(编译期配置)

告诉 Phoenix 只使用 HTTPS 版本的网站。找到config/prod.exs中的 endpoint 配置:

config :scaffold, ScaffoldWeb.Endpoint, url: [port: 443, scheme: "https"],

……然后加上force_ssl

config :scaffold, ScaffoldWeb.Endpoint, url: [port: 443, scheme: "https"], force_ssl: [rewrite_on: [:x_forwarded_proto]],

关键原因force_ssl属于编译期(compile time)配置,在runtime.exs中设置不会生效,所以必须写在这里。新项目模板 installer/templates/phx_single/config/prod.exs.eex 默认就带上了force_ssl: [rewrite_on: [:x_forwarded_proto], exclude: [...]]的示例,其中rewrite_on: [:x_forwarded_proto]正是为了让 Heroku 这类反向代理后面的应用能识别 HTTPS 请求头。

第二步:配置 Host(运行时配置)

然后在config/runtime.exs中加上host

config :scaffold, ScaffoldWeb.Endpoint, url: [host: host, port: 443, scheme: "https"]

第三步:数据库连接启用 SSL

取消仓库(repository)配置中# ssl: true,一行的注释,最终如下:

config :hello, Hello.Repo, ssl: true, url: database_url, pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")

第四步:降低 WebSocket 超时

如果你计划使用 WebSocket,需要降低lib/hello_web/endpoint.ex中 WebSocket 传输的超时时间;如果不用 WebSocket,保持默认即可。参考仓库端点模板 installer/templates/phx_web/endpoint.ex.eex:

defmodule HelloWeb.Endpoint do use Phoenix.Endpoint, otp_app: :hello socket "/socket", HelloWeb.UserSocket, websocket: [timeout: 45_000] ... end

同时在 Heroku 上设置 host:

$ heroku config:set PHX_HOST="mysterious-meadow-6277.herokuapp.com"

为什么是 45 秒?从 Phoenix 源码(lib/phoenix/endpoint.ex)的socket/3文档可以看到,WebSocket 的:timeout表示“连接最后一次收到数据后保持打开的超时时间”,默认是60_000ms。而 Heroku 的 HTTP 超时窗口是55 秒。把 Phoenix 侧的超时设为 45 秒,可以确保任何空闲连接都会在到达 Heroku 55 秒超时窗口之前被 Phoenix 主动关闭,避免连接被平台意外切断。

在 Heroku 中创建环境变量

DATABASE_URL 与 POOL_SIZE

DATABASE_URL配置变量会在添加 Heroku Postgres add-on 时自动创建。先用 Toolbelt 创建数据库:

$ heroku addons:create heroku-postgresql:mini

然后设置POOL_SIZE

$ heroku config:set POOL_SIZE=18

这个值应略低于可用连接数,留出几条给迁移和 mix 任务使用。mini 数据库允许 20 条连接,所以这里设为 18。如果多个 dyno 共享同一数据库,需要按 dyno 数量均分,相应调低POOL_SIZE

之后在 Heroku 上运行 mix 任务时(项目已推送之后),也要限制其连接池:

$ heroku run "POOL_SIZE=2 mix hello.task"

这样 Ecto 就不会试图打开超过可用上限的连接。

SECRET_KEY_BASE

还需要基于随机字符串创建SECRET_KEY_BASE。先用mix phx.gen.secret生成新密钥:

$ mix phx.gen.secret xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53

你的随机字符串一定不同,切勿使用示例值。

从实现上看(lib/mix/tasks/phx.gen.secret.ex),mix phx.gen.secret默认生成64 字符的密钥,也接受自定义长度参数(mix phx.gen.secret [length],最小 32),内部通过:crypto.strong_rand_bytes/1取强随机字节再 Base64 编码得到。

然后把它设置到 Heroku:

$ heroku config:set SECRET_KEY_BASE="xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53" Setting config vars and restarting mysterious-meadow-6277... done, v3 SECRET_KEY_BASE: xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53

该值由config/runtime.exs在启动时读取,用于签名/加密 Cookie 与其他机密数据;开发/测试环境使用的是模板内置的默认值,生产环境必须通过环境变量提供(缺失即报错),且不应提交到版本控制系统。

部署时刻!

提交所有改动并推送到 Heroku:

$ git add elixir_buildpack.config $ git commit -a -m "Use production config from Heroku ENV variables and decrease socket timeout"
$ git push heroku main Counting objects: 55, done. Delta compression using up to 8 threads. Compressing objects: 100% (49/49), done. Writing objects: 100% (55/55), 48.48 KiB | 0 bytes/s, done. Total 55 (delta 1), reused 0 (delta 0) remote: Compressing source files... done. remote: Building source: remote: remote: -----> Multipack app detected remote: -----> Fetching custom git buildpack... done remote: -----> elixir app detected remote: -----> Checking Erlang and Elixir versions remote: WARNING: elixir_buildpack.config wasn't found in the app remote: Using default config from Elixir buildpack remote: Will use the following versions: remote: * Stack cedar-14 remote: * Erlang 17.5 remote: * Elixir 1.0.4 remote: Will export the following config vars: remote: * Config vars DATABASE_URL remote: * MIX_ENV=prod remote: -----> Stack changed, will rebuild remote: -----> Fetching Erlang 17.5 remote: -----> Installing Erlang 17.5 (changed) remote: remote: -----> Fetching Elixir v1.0.4 remote: -----> Installing Elixir v1.0.4 (changed) remote: -----> Installing Hex remote: 2015-07-07 00:04:00 URL:https://s3.amazonaws.com/s3.hex.pm/installs/1.0.0/hex.ez [262010/262010] -> "/app/.mix/archives/hex.ez" [1] remote: * creating /app/.mix/archives/hex.ez remote: -----> Installing rebar remote: * creating /app/.mix/rebar remote: -----> Fetching app dependencies with mix remote: Running dependency resolution remote: Dependency resolution completed successfully remote: [...] remote: -----> Compiling remote: [...] remote: Generated phoenix_heroku app remote: [...] remote: Consolidated protocols written to _build/prod/consolidated remote: -----> Creating .profile.d with env vars remote: -----> Fetching custom git buildpack... done remote: -----> Phoenix app detected remote: remote: -----> Loading configuration and environment remote: Loading config... remote: [...] remote: Will export the following config vars: remote: * Config vars DATABASE_URL remote: * MIX_ENV=prod remote: remote: -----> Compressing... done, 82.1MB remote: -----> Launching... done, v5 remote: https://mysterious-meadow-6277.herokuapp.com/ deployed to Heroku remote: remote: Verifying deploy... done. To https://git.heroku.com/mysterious-meadow-6277.git * [new branch] master -> master

上面的输出是早期版本的示例日志(注意其中WARNING: elixir_buildpack.config wasn't found——当时还没提交该文件,所以回落到默认版本),今天执行时流程一致但版本号、输出细节会不同。上面的日志也印证了部署流水线:检测到 elixir 应用 → 检查/安装 Erlang 与 Elixir → 安装 Hex/rebar → 拉取依赖并编译 → 导出MIX_ENV=prod等配置 → 压缩并启动。

在终端执行heroku open即可用浏览器打开 Phoenix 欢迎页。如果应用使用 Ecto 访问数据库,首次部署后还需要运行迁移:

$ heroku run "POOL_SIZE=2 mix ecto.migrate"

大功告成!

方式二:通过 Container 栈部署

创建 Heroku 应用

把应用的栈设置为container,即可用Dockerfile定义应用组装方式:

$ heroku create Creating app... done, ⬢ mysterious-meadow-6277 $ heroku stack:set container

在项目根目录新增heroku.yml文件,可在其中定义应用使用的 addons、镜像构建方式以及传递给镜像的配置。示例:

setup: addons: - plan: heroku-postgresql as: DATABASE build: docker: web: Dockerfile config: MIX_ENV: prod SECRET_KEY_BASE: $SECRET_KEY_BASE DATABASE_URL: $DATABASE_URL

使用 release 并编写 Dockerfile

接下来需要在项目根目录定义包含应用的Dockerfile强烈建议配合 release 使用——release 只打包实际用到的 Erlang/Elixir 部分,能显著缩小镜像体积。请先阅读 releases 指南,其末尾附有可直接使用的示例 Dockerfile;该模板在仓库中的位置是 priv/templates/phx.gen.release/Dockerfile.eex,采用多阶段构建:builder 阶段安装 Hex/rebar、拉取 prod 依赖、编译代码与资源并mix release,final 阶段仅拷贝编译产物,以nobody用户运行/app/bin/server

镜像定义就绪后,推送应用到 Heroku,平台会自动开始构建镜像并部署。

常用 Heroku 命令

查看应用日志(加--tail可实时跟踪):

$ heroku logs # use --tail if you want to tail them

启动一个附加到终端的 IEx 会话,在应用环境中做实验:

$ heroku run "POOL_SIZE=2 iex -S mix"

事实上,heroku run可以运行任何命令,比如前面用到的 Ecto 迁移任务:

$ heroku run "POOL_SIZE=2 mix ecto.migrate"

连接进你的 dyno(远程 IEx 调试)

Heroku 允许用 IEx shell 连接进 dyno,从而执行数据库查询等 Elixir 代码。步骤如下:

  1. 修改 Procfile 中的web进程,使其运行一个命名节点

    web: elixir --sname server -S mix phx.server
  2. 重新部署到 Heroku;

  3. heroku ps:exec连接进 dyno(同一仓库下若有多个应用,需用--app APP_NAME--remote REMOTE_NAME指定);

  4. 启动远程 IEx 会话:iex --sname console --remsh server

之后你就拥有一个通向 dyno 内部 Erlang VM 的 IEx 会话了。注意此方式无法使用 observer 查看状态(见前文“平台限制”)。

故障排查

编译错误(Compilation Error)

偶尔会出现“本地编译成功、Heroku 上编译失败”的情况,错误形如:

remote: == Compilation error on file lib/postgrex/connection.ex == remote: could not compile dependency :postgrex, "mix compile" failed. You can recompile this dependency with "mix deps.compile postgrex", update it with "mix deps.update postgrex" or clean it with "mix deps.clean postgrex" remote: ** (CompileError) lib/postgrex/connection.ex:207: Postgrex.Connection.__struct__/0 is undefined, cannot expand struct Postgrex.Connection remote: (elixir) src/elixir_map.erl:58: :elixir_map.translate_struct/4 remote: (stdlib) lists.erl:1353: :lists.mapfoldl/3 remote: (stdlib) lists.erl:1354: :lists.mapfoldl/3 remote: remote: ! Push rejected, failed to compile elixir app remote: remote: Verifying deploy... remote: remote: ! Push rejected to mysterious-meadow-6277. To https://git.heroku.com/mysterious-meadow-6277.git

原因通常是过期的依赖没有正确重新编译。可以在应用根目录的elixir_buildpack.config中加入一行,强制 Heroku 每次部署都重编所有依赖:

always_rebuild=true

提交该文件后重新推送即可解决。

连接超时错误(Connection Timeout Error)

如果执行heroku run时频繁出现连接超时,可能是你的网络运营商屏蔽了 5000 端口

heroku run "POOL_SIZE=2 mix myapp.task" Running POOL_SIZE=2 mix myapp.task on mysterious-meadow-6277... ! ETIMEDOUT: connect ETIMEDOUT 50.19.103.36:5000

解决办法是给 run 命令加上detached选项(后台运行):

heroku run:detached "POOL_SIZE=2 mix ecto.migrate" Running POOL_SIZE=2 mix ecto.migrate on mysterious-meadow-6277... done, run.8089 (Free)

补充:多实例下的 Long-Polling 与状态问题

heroku.md通篇假设单一 web dyno 部署。若生产环境运行多台机器(这也是官方推荐的做法),部署时还需要留意 Long-Polling 传输的问题:Phoenix 的 Socket 同时支持 WebSocket 与 Long-Polling 两种传输(installer/templates/phx_web/endpoint.ex.eex 中同时声明了websocket:longpoll:配置),客户端会在 WebSocket 连接失败时按配置的longPollFallbackMs自动回退到 Long-Polling(前端实现见 assets/js/phoenix/socket.js)。由于 Long-Polling 的每个请求都可能落到不同机器,要让长连接状态得以保留,部署方案必须满足其一:启用 Erlang VM 集群能力(默认Phoenix.PubSub可跨节点广播)、更换Phoenix.PubSub适配器(如 Redis)、或由部署平台实现粘性会话(sticky sessions)。这一点在 部署概述 的“Clustering and Long-Polling Transports”一节有更完整的论证——而正如前文“平台限制”所说,Heroku 的 dyno 默认互相隔离,既不支持原生集群,也未必提供粘性会话,这正是需要考虑替代 PaaS 或自管主机的原因。

【免费下载链接】phoenixPeace of mind from prototype to production项目地址: https://gitcode.com/gh_mirrors/ph/phoenix

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 5:16:30

LangGraph:多Agent协作框架的核心优势与实践

1. 为什么LangGraph会成为2025年的Agent框架首选?三年前当我第一次接触Agent框架时,面对市面上十几种选择简直眼花缭乱。直到去年在开发一个复杂电商推荐系统时尝试了LangGraph,才发现这个基于图计算的框架在处理多Agent协作场景时的独特优势…

作者头像 李华
网站建设 2026/9/20 5:13:45

软件测试全流程解析:从单元测试到验收测试

1. 软件测试过程全景解析在软件开发领域,测试工作绝不是简单的"找bug",而是一个系统化的质量保障体系。作为一名从业十余年的测试工程师,我见过太多项目因为轻视测试环节而付出惨痛代价。今天,我将带大家深入理解软件测…

作者头像 李华
网站建设 2026/9/20 5:10:51

Turborepo 内部包(Internal Packages)创建与组织完整指南

Turborepo 内部包(Internal Packages)创建与组织完整指南 【免费下载链接】turbo Build system optimized for JavaScript and TypeScript, written in Rust 项目地址: https://gitcode.com/gh_mirrors/tu/turbo 导读 在 Turborepo monorepo 中&…

作者头像 李华
网站建设 2026/9/20 5:10:21

智能体测试实战指南:应对不确定性,构建分层质量保障体系

1. 智能体测试与传统软件测试的根本差异1.1 需求从“明确函数”变成了一段“自由对话”我最早接触智能体测试时,第一反应是拿之前做接口测试的经验往上套:构造输入、校验输出、断言通过就完事。结果第一个用例就把我难住了——同一个问题,让智…

作者头像 李华
网站建设 2026/9/20 5:07:37

LinkSwift 网盘直链解析指南:9 大网盘文件 5 分钟拿到真实下载地址

LinkSwift 网盘直链解析指南:9 大网盘文件 5 分钟拿到真实下载地址 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 ,支持 百度网盘 / 阿里云盘 / 中国移动…

作者头像 李华
网站建设 2026/9/20 5:07:23

OpenResearch 实践指南:构建透明可复现的研究工作流

不知道你有没有过这种感觉——花三个月做完一个研究课题,回头想分享成果时,却发现自己连中间删掉的关键分支、当时为什么选这个样本、跳过某个方法的原因全都想不起来了。我之前经常这样。明明过程里踩了无数坑,最后交出去的报告却很"光…

作者头像 李华