当前位置:运行时 dist/
生产环境中运行的是什么
为什么弄清二进制文件自己做什么、把什么交给别人,其余的部署方式也就随之确定。
cybertrain build 把框架、你的控制器和模型以及模板解释器编译进 dist/blog,嵌入所有视图,并在旁边复制 public/。运行它的地方无需安装其他任何东西:不需要 Ruby,也不需要 gem。
browser ──HTTPS──▶ reverse proxy (kamal-proxy or Caddy)
└─ HTTP/1.1 ──▶ dist/blog (port 3000)
├─ public/ static files, 404.html, 500.html
└─ storage/production.sqlite3
- 它链接系统的 SQLite 和 libc,因此要在与运行它的机器相同的操作系统和 CPU 上构建。像 Kamal 配置那样在 Docker 镜像内构建,就自然满足了这一点。
- 它只说普通的 HTTP/1.1。TLS 和 HTTP/2 是反向代理的工作。生产环境中会话 cookie 带有
Secure,所以必须通过 HTTPS 提供服务,否则表单会因Invalid authenticity token而失败。 - 数据库是一个 SQLite 文件。在一台服务器上以单个进程运行,并把文件放在部署之后仍然保留的存储上。
- 它默认就是生产模式,没有
CYBERTRAIN_SECRET_KEY_BASE就拒绝启动。./blog migrate应用编译进其中的迁移。
两种部署方式
为什么两者最终都在同一个二进制文件前放上 HTTPS;区别在于在哪里构建,以及部署时如何替换。
| Kamal | systemd 加 Caddy | |
|---|---|---|
| 构建位置 | 在 Docker 镜像中,在你的机器或远程构建器上 | 在服务器上,从 git 检出构建 |
| 前端 | kamal-proxy,带 Let’s Encrypt 证书 | Caddy,带 Let’s Encrypt 证书 |
| 静态文件 | 由应用提供 public/ | 由 Caddy 直接提供 dist/public/ |
| 一次部署 | 新容器通过健康检查后才接收流量:不会拒绝任何连接 | 一次重启:几秒钟内显示 Caddy 的 500.html |
| 回滚 | kamal rollback VERSION | 把 current 指向较早的发布并重启 |
| 服务器需要 | SSH 和 Docker(由 kamal setup 安装),外加一个镜像仓库 | Ruby、C 工具链、SQLite 头文件、cybertrain CLI 和 Spinel |
无论哪种方式,应用都是一台服务器上的一个进程加上它的 SQLite 文件。
用 Kamal 部署
为什么cybertrain new 已经写好了这些文件;你只需填上服务器、域名和镜像仓库。
DockerfileUbuntu 构建阶段运行cybertrain setup和cybertrain build;同一发行版的精简阶段只获得dist/和它链接的库config/deploy.yml服务器、域名、带 TLS 并转发到 3000 端口的 kamal-proxy、健康检查以及数据库卷.kamal/secrets从你的环境变量读取KAMAL_REGISTRY_PASSWORD和CYBERTRAIN_SECRET_KEY_BASE,从不写入值本身public/up.htmlkamal-proxy 在把流量送到新容器之前轮询的页面
填好 config/deploy.yml 中标为 CHANGE ME 的值,提交,然后运行:
export KAMAL_REGISTRY_PASSWORD=...
export CYBERTRAIN_SECRET_KEY_BASE="$(openssl rand -hex 32)" # 只生成一次并妥善保存
kamal setup # 首次:在服务器上安装 Docker 和 kamal-proxy,构建、推送并启动
kamal deploy # 之后的每次部署
- 每次部署都会构建镜像(首次之后 Spinel 留在 Docker 的层缓存中)、推送它,并启动一个容器;容器先运行
./blog migrate,再以exec ./blog在0.0.0.0:3000上提供服务。 - kamal-proxy 等到
/up.html返回 200 后,把流量切换到新容器,然后停止旧容器;旧容器收到SIGTERM后会先处理完正在进行的请求。 - 数据库位于挂载在
/app/storage的卷blog_storage上,因此每次部署后都会保留。 - 把
builder.arch设为服务器的 CPU 架构。Spinel 在镜像内编译,用模拟方式为其他架构构建会很慢;这种情况下改用builder.remote在服务器上构建。
包括卷备份在内的分步指南见 docs/deploy-kamal.md。examples/blog 也带有同样的配置,CI 会构建它的镜像并做冒烟测试。
在 Caddy 之后,由 systemd 运行
为什么不使用 Docker:服务器从 git 构建应用,systemd 让它持续运行,Caddy 面向互联网。
- 应用只监听
127.0.0.1:3000,所以只能经由 Caddy 访问。public/和storage/相对于工作目录解析,因此 systemd 以dist/为工作目录启动它,并从仓库之外的环境变量文件读取密钥。 - 部署脚本把每个发布克隆到各自的目录,检查
gen/是否最新,运行cybertrain build和dist/blog migrate,然后把current链接指向它,重启服务并等待up.html响应;如果没有响应,脚本会切回之前的发布。切换之前的失败不会影响正在运行的发布。切回之前的发布不会撤销迁移,因此迁移必须与之前的发布兼容;首次部署失败时没有可切回的发布,脚本只会报错退出。 - Caddy 获取证书,把 HTTP 重定向到 HTTPS,自己提供
dist/public/中的文件,其余请求代理给应用。应用重启期间,它返回500.html。
blog.example.com {
encode zstd gzip
root * /srv/blog/current/dist/public
@static file
handle @static {
file_server
}
handle {
reverse_proxy 127.0.0.1:3000
}
handle_errors {
rewrite * /500.html
file_server
}
}
应用会给它返回的内容加上 X-Content-Type-Options 和 X-Frame-Options;Caddy 从 dist/public/ 直接提供的文件不经过应用,如果也想在那里加上,请在站点块中添加。nginx 的做法相同:终止 TLS,提供 dist/public/,其余的代理出去。
从一台全新的 Ubuntu 服务器到备份和回滚的完整流程见 docs/deploy.en.md(日文原文为 docs/deploy.md)。
配置
为什么生产环境的设置来自环境变量,所以同一个二进制文件可以在任何地方运行。
| 变量 | 在生产环境中 |
|---|---|
CYBERTRAIN_SECRET_KEY_BASE | 必需。为会话 cookie 签名;更改它会让所有人退出登录。用 openssl rand -hex 32 生成一次 |
CYBERTRAIN_DATABASE | SQLite 文件;默认是工作目录下的 storage/production.sqlite3。把它放在发布目录之外 |
CYBERTRAIN_HOST | 默认 127.0.0.1,适合同一台机器上的代理之后;容器中用 0.0.0.0 |
PORT | 默认 3000;代理转发的目标 |
CYBERTRAIN_ENV | 构建出的二进制文件中已经是 production;development 会启动文件监视和重建 |
教程中的为部署而构建一步列出了所有变量及其对应的 config/app.rb 属性。
日常运维
- 每次更改都是一次部署。视图在构建时嵌入,因此即使只改了视图也需要重新构建并重启。把
gen/和更改一起提交:部署脚本遇到过时的gen/会停止。 - 迁移在新代码开始提供服务之前运行:在部署脚本中,或在容器启动时。删除列要放到之后的另一次部署中,等运行中的代码不再读取它。
- 用 SQLite 的
.backup备份,而不是复制文件:即使应用正在写入,它也能得到一致的副本。 - 跨越迁移的回滚意味着数据库也要回滚:在切换之前,从较新的发布中运行
db rollback。 - 停止是平滑的:收到
SIGTERM后,服务器停止接受新连接,处理完正在进行的请求后退出,最多 10 秒。