CyberTrain部署

部署到生产环境

CyberTrain 应用以一个原生二进制文件和它旁边的一个目录交付。它只说普通的 HTTP,所以要由前端的某个组件负责 HTTPS:用 Kamal 部署时是 kamal-proxy,自己管理的服务器上则是 Caddy。本页说明各个部分如何组合,以及两种方式有何不同;每一步链接的指南里有全部命令。

你需要
一台可以 SSH 登录的 Linux 服务器,以及一个域名
运行的是
反向代理后面的一个二进制文件,旁边是 SQLite
两种方式
Kamal 加 Docker,或 systemd 加 Caddy

当前位置:运行时 dist/

生产环境中运行的是什么

为什么弄清二进制文件自己做什么、把什么交给别人,其余的部署方式也就随之确定。

cybertrain build 把框架、你的控制器和模型以及模板解释器编译进 dist/blog,嵌入所有视图,并在旁边复制 public/。运行它的地方无需安装其他任何东西:不需要 Ruby,也不需要 gem。

生产环境text
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;区别在于在哪里构建,以及部署时如何替换。

Kamalsystemd 加 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 的值,提交,然后运行:

终端 — blog/shell
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。
/etc/caddy/Caddyfilecaddyfile
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_DATABASESQLite 文件;默认是工作目录下的 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 秒。

下一步