初始代码
如果买的服务器时按使用量计费的,用完记得停机并且销毁机器实例,单独停机还会扣费
复习前回内容:密钥管理
- 开发环境
master.key、credentials.yml.enc
- 生产环境
production.key、production.yml.enc
- 原则:机密不得提交到
git,不得复制给实习生
【山竹记账后端】6.部署到云服务器
大纲链接 §
[toc]
继续部署上线-云服务器
1. 一键部署到云服务器 ⇧
上云的前提条件,需要同时满足 ⇧
- 已经在本地成功部署
- 已经准备好
production.key 和 production.yml.enc
- 云服务器已经正确安装了
Docker
- 云服务器已经开通了
3000 端口
Docker 开发环境可以通过 ssh 命令登录云服务器(使用 mangosteen 用户)
容器项目登录终端
1
2
3
4
5
6
|
ssh mangosteen@server1
docker ps
docker run hello-world
docker network create network1
...
# mangosteen@server1:
|
映射云服务器 ip ⇧
云服务器部署 ⇧
回顾宿主机部署步骤
- 在
Docker 里运行 pack_for_host.sh
- 在
Windows/Mac 里运行 setup_host.sh
部署到宿主机 v.s. 部署到云机器
云服务器部署步骤
- 在
Docker 里运行 pack_for_remote.sh
- 没了
远程打包脚本 bin/pack_for_remote.sh ⇧
创建 bin/pack_for_remote.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
# 注意修改 user 和 ip(云服务器公网)
# user=mangosteen
# ip=121.196.236.94
# ip=server1 #已映射
# 带默认值的声明
user=${user:-mangosteen}
ip=${ip:-server1}
# 时间作为唯一的部署版本号
time=$(date +'%Y%m%d-%H%M%S')
# 容器部署临时缓存目录
cache_dir=tmp/deploy_cache
# 压缩包文件名:容器中压缩包的完整输出路径
dist=$cache_dir/mangosteen-$time.tar.gz
# bin 获取当前脚本所在的绝对或相对路径(通常是容器项目 bin 目录)
current_dir=$(dirname $0)
# 远程部署目标:容器中部署目录的完整路径,time 为版本号
deploy_dir=/home/$user/deploys/$time
# 依赖配置文件:容器中 Gemfile 的完整路径
gemfile=$current_dir/../Gemfile
gemfile_lock=$current_dir/../Gemfile.lock
# 本地缓存的依赖包目录:容器中 vendor/cache 目录的完整路径
vendor_cache_dir=$current_dir/../vendor/cache
function title {
echo
echo "###############################################################################"
echo "## $1"
echo "###############################################################################"
echo
}
title '打包源代码为压缩文件'
mkdir $cache_dir # 创建本地缓存目录
bundle cache # 将项目依赖的 Gem 包下载并缓存到 vendor/cache 目录,确保依赖不变时,远程机器无需再次联网下载
tar --exclude="tmp/cache/*" --exclude="tmp/deploy_cache/*" -czv -f $dist *
title '创建远程目录'
ssh $user@$ip "mkdir -p $deploy_dir/vendor/cache" # 在服务器上递归创建部署目录和依赖缓存目录
title '1.上传源码压缩包和依赖配置文件'
scp $dist $user@$ip:$deploy_dir/
yes | rm $dist # 强制删除本地生成的压缩包,释放空间
scp $gemfile $user@$ip:$deploy_dir/ # 上传 Gemfile
scp $gemfile_lock $user@$ip:$deploy_dir/ # 上传 Gemfile.lock
scp -r $vendor_cache_dir $user@$ip:$deploy_dir/vendor/ # 递归上传所有缓存的依赖包 #待改进为上传压缩包后再解压
title '2.上传 Dockerfile'
scp $current_dir/../config/host.Dockerfile $user@$ip:$deploy_dir/Dockerfile # 上传并重命名 Docker 配置文件
title '3.上传 setup 脚本'
scp $current_dir/setup_remote.sh $user@$ip:$deploy_dir/ # 上传在远程服务器执行的安装脚本
title '4.上传版本号'
ssh $user@$ip "echo $time > $deploy_dir/version" # 在远程目录中写入当前版本号文件
title '执行远程脚本'
ssh $user@$ip "export version=$time; /bin/bash $deploy_dir/setup_remote.sh"
|
解释命令:
title: 辅助函数。用于在终端输出显眼的分割线和大标题,提升日志可读性
scp 是基于 ssh 的拷贝
cp a.txt b.txt:本地复制文件
scp a.txt $user@$ip:/b.txt:上传文件到远程机器
scp -r a.txt $user@$ip:/b.txt:上传目录到远程机器
- 远程执行命令
ssh $user@$ip "echo xxx"
- 远程执行脚本
export version=$time; /bin/bash $deploy_dir/setup_remote.sh"
- 环境变量
export version=$time;:设置环境变量 version 为 time
/bin/bash 使用 bash 执行
- tar 命令参数解释:
--exclude:打包时排除本地的缓存和临时文件,减小压缩包体积。
-czv:create(创建压缩包)、zip(gzip压缩)、verbose(显示过程)。
-f $dist *:将当前目录下的所有内容打包到指定路径。
- 无需手动再登录远程机器去执行
setup_remote.sh 脚本命令了,比在宿主机中少一步
- 需要添加执行权限
chmod +x bin/pack_for_remote.sh
参考
远程部署脚本 setup_remote.sh ⇧
创建 bin/setup_remote.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
user=mangosteen
# 容器中部署目录 $version 由环境变量传递
root=/home/$user/deploys/$version
# 应用容器名称
container_name=mangosteen-prod-1
# 数据库容器名称
db_container_name=db-for-mangosteen
# 设置环境变量到 ~/.bashrc
function set_env {
name=$1
hint=$2
[[ ! -z "${!name}" ]] && return
while [ -z "${!name}" ]; do
[[ ! -z "$hint" ]] && echo "> 请输入 $name: $hint" || echo "> 请输入 $name:"
read $name
done
sed -i "1s/^/export $name=${!name}\n/" ~/.bashrc
echo "${name} 已保存至 ~/.bashrc"
}
function title {
echo
echo "###############################################################################"
echo "## $1"
echo "###############################################################################"
echo
}
title '设置远程机器的环境变量'
set_env DB_HOST
set_env DB_PASSWORD
set_env RAILS_MASTER_KEY
title '创建数据库'
if [ "$(docker ps -aq -f name=^${DB_HOST}$)" ]; then
echo '已存在数据库'
else
docker run -d --name $DB_HOST \
--network=network1 \
-e POSTGRES_USER=mangosteen \
-e POSTGRES_DB=mangosteen_production \
-e POSTGRES_PASSWORD=$DB_PASSWORD \
-e PGDATA=/var/lib/postgresql/data/pgdata \
-v mangosteen-data:/var/lib/postgresql/data \
postgres:14
echo '创建成功'
fi
title 'docker build'
docker build $root -t mangosteen:$version
# 判断是否已存在运行应用,存在就删除
if [ "$(docker ps -aq -f name=^mangosteen-prod-1$)" ]; then
title 'docker rm'
docker rm -f $container_name
fi
# 运行应用容器
title 'docker run'
docker run -d -p 3000:3000 \
--network=network1 \
--name=$container_name \
-e DB_HOST=$DB_HOST \
-e DB_PASSWORD=$DB_PASSWORD \
-e RAILS_MASTER_KEY=$RAILS_MASTER_KEY \
mangosteen:$version
echo
echo "是否要更新数据库?[y/N]"
read ans
case $ans in
y|Y|1 ) echo "yes"; title '更新数据库'; docker exec $container_name bin/rails db:create db:migrate ;;
n|N|2 ) echo "no" ;;
"" ) echo "no" ;;
esac
title '全部执行完毕'
|
- 核心功能函数 set_env: 交互式环境变量检查与保存
- 首先检查系统是否已存在该环境变量,若存在则直接跳过(
[[ ! -z "${!name}" ]] && return)
- 若不存在,则进入 while 循环,提示并强制要求用户手动输入
- 使用
sed -i "1s/^/.../" ~/.bashrc 将变量以 export KEY=VALUE 的形式插入到 ~/.bashrc 文件的第一行
- 确保持久化,下次登录依然有效
- 若用户输入不为空,则将输入保存至
~/.bashrc 中
第一步:配置环境变量
- 设置远程机器的环境变量,这些变量是不能写到 git 中的;部署时,添加到运行命令前的环境变量
set_env DB_HOST 数据库容器名或地址(后续创建数据库容器时,会直接以此变量名作为容器名)
set_env DB_PASSWORD 数据库密码
set_env RAILS_MASTER_KEY Rails 生产环境解密凭证所必需的密钥
第二步:创建并运行数据库容器
- 使用
docker ps -aq -f name=^${DB_HOST}$ 精确查找是否已存在同名的数据库容器
- 如果不存在:创建并后台运行一个基于
postgres:14 的容器
- 加入自定义网络
--network=network1,确保应用容器后续能通过 DB_HOST 域名直接访问数据库。
- 通过
-v 将数据挂载到外部数据卷 mangosteen-data,保证数据库重构时数据不丢失。
第三步:构建并运行应用容器
docker build $root -t mangosteen:$version:前往 $root 目录,读取那里的 Dockerfile,构建出带有版本标签的镜像 mangosteen:$version
- 清理旧容器:检查是否存在正在运行或残留的旧应用容器(
mangosteen-prod-1)
- 如果存在则强制删除(
docker rm -f),为新容器腾出端口和名字
docker run:启动新应用容器,指定端口映射、网络、容器名、环境变量、镜像标签
- 映射本地 3000 端口到容器 3000 端口。
- 加入相同的网络
network1。
- 将第一步获取的环境变量注入到应用容器中,供 Rails 程序读取
第四步:可选的数据库迁移
- 脚本使用
read ans 读取用户输入,提示用户是否需要更新数据库
- 如果用户输入 y、Y 或 1:脚本会通过
docker exec 进入刚刚启动的应用容器
- 执行 Rails 内置命令
bin/rails db:create db:migrate 来创建表结构或执行数据迁移
- 若输入其他内容或直接回车,则跳过此步
需要添加执行权限 chmod +x bin/setup_remote.sh
参考
执行部署脚本顺序 ⇧
确保脚本都已添加执行权限 chmod +x bin/*.sh
- 容器项目终端
bin/pack_for_remote.sh:打包应用代码到远程机器
- 远程服务器自动运行脚本
/home/$user/deploys/$time/setup_remote.sh:在远程机器上部署应用
运行结果
- 全部执行完毕
docker build 成功
docker rm 成功删除原 mangosteen-prod-1
docker run 成功显示一串哈希 58be87xxxxx...
验证
- 浏览器访问公网ip
http://8.155.132.238:3000,确认应用已启动
- 使用命令
curl --noproxy "*" http://localhost:3000 -v ,确认应用已启动
坑点1: 网络问题,可能报错 ⇧
1
2
|
Retrying fetcher due to error (2/4):Bundler:HTTPError Could not fetch specs from https://gems.ruby-china.com/
due to underlying error <timed out (https://gems.ruby-china.com/specs.4.8.gz)>
|
host.Dickerfile
1
2
3
|
#...
RUN bundle config mirror.https://rubygems.org https://mirrors.tuna.tsinghua.edu.cn/rubygems
#...
|
- 解决方法2,在容器项目中运行
bundle cache 缓存依赖
bundle cache 会将所有依赖缓存到自动生成的 vendor/cache 目录下,后续构建时会直接从缓存中读取,避免重复下载。
- 并且需要修改
host.Dickerfile 中的命令 RUN bundle install 改为 RUN bundle install --local
- 这样在后续构建时,会直接从缓存中读取依赖,避免重复下载。
- 修改
bin/pack_for_remote.sh,在 tar 压缩前添加 bundle cache
host.Dickerfile
1
2
3
4
5
6
7
8
9
10
|
FROM ruby:3.1.2
ENV RAILS_ENV=production
RUN mkdir /mangosteen
RUN bundle config mirror.https://rubygems.org https://mirrors.tuna.tsinghua.edu.cn/rubygems
WORKDIR /mangosteen
ADD mangosteen-*.tar.gz ./
RUN bundle config set --local without 'development test'
RUN bundle install --local
ENTRYPOINT ["bundle", "exec", "puma"]
|
坑点2:报错 EOF 脚本添加最后一行回车空行 ⇧
pack_for_remote.sh 报错 EOF
1
2
|
bin/pack_for_remote.sh:line 38:unexpected EOF while looking for matching
bin/pack_for_remote.sh:line 39:syntax error:unexpected end of file
|
2. 优化 Dockerfile 加速 bundle build ⇧
微调 host.Dockerfile
- 目的:如果依赖没变,就不需要重新安装依赖,复用之前的
- 步骤
- 先添加
Gemfile、Gemfile.lock、vendor/cache
- 再运行
bundle install --local
- 最后做其他
- 实际上改完代码后运行改的是
ADD mangosteen-*.tar.gz ./ 这行命令
- 调整顺序,先安装依赖,再去运行依赖
- 之前在
pack_for_remote.sh 脚本中已经上传 Gemfile 等文件
host.Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
FROM ruby:3.1.2
ENV RAILS_ENV=production
RUN mkdir /mangosteen
RUN bundle config mirror.https://rubygems.org https://mirrors.tuna.tsinghua.edu.cn/rubygems
WORKDIR /mangosteen
ADD Gemfile /mangosteen
ADD Gemfile.lock /mangosteen
ADD vendor/cache /mangosteen/vendor/cache
RUN bundle config set --local without 'development test'
RUN bundle install --local
ADD mangosteen-*.tar.gz ./
ENTRYPOINT ["bundle", "exec", "puma"]
|
- 这是
docker 的一个特性: layer 层复用
- 第n次运行镜像
ADD Gemfile /mangosteen、ADD Gemfile.lock /mangosteen、ADD vendor/cache /mangosteen/vendor/cache 等都不变
- 之后的
RUN bundle install --local 也不需要变化,直接使用上一次 docker build 的结果
已修改 pack_for_remote.sh
1
2
3
4
5
6
7
8
9
|
#...
# 本地缓存的依赖包目录:容器中 vendor/cache 目录的完整路径
vendor_cache_dir=$current_dir/../vendor/cache
#...
title '创建远程目录'
ssh $user@$ip "mkdir -p $deploy_dir/vendor/cache" # 在服务器上递归创建部署目录和依赖缓存目录
#...
scp -r $vendor_cache_dir $user@$ip:$deploy_dir/vendor/ # 递归上传所有缓存的依赖包 #待改进为上传压缩包后再解压
#...
|
重新再打包运行镜像时就走缓存逻辑了
第一次还需要安装,之后就直接使用缓存
1
2
3
4
|
...
Step 10/12 RUN bundle install --local
--->Using cache
...
|
提交代码忽略 vendor/cache
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# Temporary files generated by your text editor or operating system
# belong in git's global ignore instead:
# `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore`
# Ignore bundler config.
/.bundle
# Ignore all environment files (except templates).
/.env*
!/.env*.erb
# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
/tmp/*.gz
!/log/.keep
!/tmp/.keep
# Ignore pidfiles, but keep the directory.
/tmp/pids/*
!/tmp/pids/
!/tmp/pids/.keep
# Ignore storage (uploaded files in development and any SQLite databases).
/storage/*
!/storage/.keep
/tmp/storage/*
!/tmp/storage/
!/tmp/storage/.keep
# Ignore master key for decrypting credentials and more.
/config/master.key
# IDE
.idea
.vscode
# plugin
.ai
.junie
.rubymate
.ruby-lsp
/config/credentials/production.key
/vendor/cache
|
3.云服务器部署命令总结 ⇧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
# 【容器项目终端-云服务器】映射云服务器公网ip
ssh mangosteen@xxx.xxx.xxx.xx
vim /etc/hosts
echo "xxx.xxx.xxx.xx server1" >> /etc/hosts
cat /etc/hosts
# 【容器项目终端-云服务器】登录已经映射公网ip的云服务器
ssh mangosteen@server1
# 已添加脚本 pack_for_remote.sh 和 setup_remote.sh
# 【容器项目】添加可执行权限
chmod +x bin/*.sh
# 【容器项目】打包到云服务器
# bin/pack_for_remote.sh
# 【容器项目】打包到云服务器 - 传参公网ip
ip=xxx.xxx.xxx.xx bin/pack_for_remote.sh
# 云服务器自动运行 setup_remote.sh
# 创建网络
docker network create network1
# 启动数据库
docker start db-for-mangosteen
# 首次输入(不能提交到git中的内容)
# 请输入 DB_HOST:
db-for-mangosteen
# 请输入 DB_PASSWORD:
123456
# 请输入 RAILS_MASTER_KEY(在项目 production.key 中):
240c3dcce0e4e35f99e1c107042ec5a8
|
常见报错 ⇧
DEBUG 示例:部署时更新数据库失败

处理方法
- 可删除容器项目中的依赖缓存
tmp/deploy_cache
- 登录远程
ssh root@8.155.132.238
- 运行
docker ps -a 看看 db-for-mangosteen 容器是否在运行
- 启动数据库容器
docker start db-for-mangosteen
- 然后重新运行
ip=8.155.132.238 bin/pack_for_remote.sh
运行 bin/pack_for_remote.sh 报错:更新数据库失败
1
|
Error response from daemon: Container xxx is not running
|
- 出现这种报错一般是数据库容器启动失败,应该在远程机器运行
docker ps -a 看看 db-for-mangosteen 容器是否在运行
- 如果没有运行就用
docker logs db-for-mangosteen 看看数据库的报错信息,然后分析报错
运行 bin/pack_for_remote.sh 报错
1
2
3
4
|
> bin/pack_for_remote.sh
scp: realpath /home/mangosteen/deploys/20220906-210905/vendor/cache: No such file
scp: upload "/home/mangosteen/deploys/20220906-210905/vendor/cache": path canonicalization failed
scp: failed to upload directory bin/../vendor/cache to /home/mangosteen/deploys/20220906-210905/vendor/cache
|
- 出错原因是远程机器上的
vendor/cache 目录没有创建
解决办法是把 pack_for_remote.sh 中的
1
2
|
title '创建远程目录'
ssh $user@$ip "mkdir -p $deploy_dir/vendor"
|
改为
1
2
|
title '创建远程目录'
ssh $user@$ip "mkdir -p $deploy_dir/vendor/cache"
|
调整 config/host.Dockerfile 以适配本地宿主机部署
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
FROM ruby:3.1.2
ENV RAILS_ENV=production
# 2. 提前升级镜像中的 bundler 版本,使其与你的 Gemfile.lock (2.3.7) 一致
# RUN gem install bundler:2.3.7
RUN bundle config mirror.https://rubygems.org https://mirrors.tuna.tsinghua.edu.cn/rubygems
RUN mkdir /mangosteen
WORKDIR /mangosteen
ADD Gemfile /mangosteen
ADD Gemfile.lock /mangosteen
ADD vendor/cache /mangosteen/vendor/cache
RUN bundle config set --local without 'development test'
RUN bundle install --local
ADD mangosteen-*.tar.gz ./
ENTRYPOINT ["bundle", "exec", "puma"]
|
大坑:镜像源失效 ⇧
远程部署云服务器时报错:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
...
#2 [internal] load metadata for docker.io/library/ruby:3.1.2
#2 ERROR: failed to do request: Head "https://registry-1.docker.io/v2/library/ruby/manifests/3.1.2": dial tcp 118.193.240.37:443: i/o timeout
------
> [internal] load metadata for docker.io/library/ruby:3.1.2:
------
Dockerfile:1
--------------------
1 | >>> FROM ruby:3.1.2
2 |
3 | ENV RAILS_ENV=production
--------------------
ERROR: failed to build: failed to solve: DeadlineExceeded: ruby:3.1.2: failed to resolve source metadata for docker.io/library/ruby:3.1.2: failed to do request: Head "https://registry-1.docker.io/v2/library/ruby/manifests/3.1.2": dial tcp 118.193.240.37:443: i/o timeout
#... 或者报错
#2 [internal] load metadata for docker.io/library/ruby:3.1.2
#2 ERROR: failed to do request: Head "https://registry-1.docker.io/v2/library/ruby/manifests/3.1.2": dial tcp 118.193.240.37:443: i/o time
|
- Docker 无法在 Docker Hub 上找到或下载你指定的 ruby:3.1.2 镜像
- 常见的原因是网络连接限制、未登录 Docker Hub(或凭据过期)或者 Docker 守护进程的 DNS 配置故障
- 网络连接超时问题。因为移除了加速镜像,Docker 开始直接连接官方的 registry-1.docker.io,但在国内直连该地址目前几乎是 100% 被墙或严重丢包的
只能换镜像源
1
2
3
4
5
6
7
8
9
10
11
|
{
"registry-mirrors": [
"https://docker.xuanyuan.me",
"https://docker.1ms.run",
"https://docker.m.daocloud.io",
"https://docker.mirrors.ustc.edu.cn/",
"https://o3b9ba2s.mirror.aliyuncs.com",
"https://reg-mirror.qiniu.com",
"https://hub-mirror.c.163.com/"
]
}
|
参考
·未完待续·
参考文章
相关文章
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名