Egg 项目写完,接口在本地 7001 端口跑得好好的,接下来就卡在同一个地方,这玩意儿到底往哪儿放。有人说买台云服务器 pm2 起起来就行,有人说上 Docker,有人说直接 serverless 一把梭。我把这三条路都真跑了一遍,从本地起容器开始,一直到线上接口能被前端页面调通,中间该踩的坑基本都踩到了。
这篇是那次的完整记录,命令、配置文件、每一步该看到什么界面全在里面。你可以当手册翻,也可以顺着读一遍,理解为什么部署这件事会从「一条 docker run」慢慢演化成「一个 compose 文件」再到「一次 git push」。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- 本地用 Docker 把 node、MySQL、Redis、Nginx 四个环境一个个装起来,搞清楚每个参数在干嘛
- 用 docker-compose 把这四个服务编排成一组,靠自定义网桥让容器之间用容器名互相通信
- Nginx 容器里怎么放前端静态资源,怎么把接口反向代理到 Egg 服务
- 把整套 docker-compose 搬到云服务器上,包括 CentOS 装 Docker、放行端口、Redis 持久化配置
- 用微信云托管做流水线部署,push 代码自动构建
- 把 Egg 部署到腾讯云 serverless,包括 CLI 部署和控制台自定义部署两种姿势
- 这套 2022 年的流程,放到今天有哪些地方要改
# 一、本地 Docker 环境搭建
先说清楚,这一节手动做的所有事情,到了第二节都会被一条 docker-compose up -d 替代。那还有必要一个一个手动装吗?我的答案是有必要。只有自己敲过一遍 docker run,你才知道 compose 文件里的 ports、volumes、environment 分别对应命令行的哪个参数,线上出问题的时候才知道该去哪儿看。
mac 下装 Docker 用 brew install docker 就行。这里有个坑要注意,这条命令只装了 CLI,本机没有 daemon,光敲 docker ps 会报 Cannot connect to the Docker daemon。要跑容器还得装 Docker Desktop,也就是 brew install --cask docker,装完打开一次让它把守护进程拉起来。
https://hub.docker.com 拉取镜像速度比较慢,我们推荐使用国内的镜像源访问速度较快 https://hub.daocloud.io
# 1.1 设置国内镜像源
Docker Desktop 的偏好设置里有个 Docker Engine 面板,改的其实就是 daemon.json 这个文件,写完点 Apply & Restart 就生效。

registry-mirrors 是一个数组,Docker 拉镜像的时候会先走这里配的地址,拉不到才回源到官方仓库。配它的目的很单纯,就是让 docker pull 别卡在 0%。
{
"registry-mirrors": ["https://registry.docker-cn.com/"]
}
改完之后可以用 docker info 确认一下,输出最下面会多一段 Registry Mirrors,能看到你刚才填的地址才算配上了。至于镜像地址本身,这两年国内的公共加速源变动特别频繁,当年能用的今天不一定还在,遇到拉不动别急着怀疑网络,先确认这个源本身是不是已经废了,用你云厂商控制台给的专属加速地址通常最稳。
进入该网站https://hub.daocloud.io获取镜像的下载地址。
# 1.2 docker 命令基础
后面每一节都会反复用到这几条命令,先过一遍,混熟了排查问题会快很多。
docker images查看镜像docker ps查看启动的容器 (-a查看全部)docker rmi 镜像ID删除镜像docker rm 容器ID删除容器docker exec -it 1a8eca716169(容器ID:docker ps获取) sh进入容器内部docker inspect bf70019da487(容器ID)查看容器内的信息
这里面最值得单独说的是 docker inspect。它吐出来的是一大坨 JSON,包含容器的网络配置、挂载点、环境变量、启动命令,Node 连不上 MySQL 的时候我一般第一个看它,确认容器到底挂在哪个网络上、IP 是多少。docker exec -it 容器ID sh 则是进容器内部,注意有些精简镜像(比如 alpine)里没有 bash,只能用 sh。
镜像列表里那些 <none> 的悬空镜像,是重复构建留下的中间产物,占空间还看着乱。
删除none的镜像,要先删除镜像中的容器。要删除镜像中的容器,必须先停止容器。
顺序反了会报 image is being used by stopped container,所以下面这条得在容器清干净之后再跑。
$ docker rmi $(docker images | grep "none" | awk '{print $3}')
完整的清理三连是这样,停容器、删容器、删镜像,一步都不能跳。
$ docker stop $(docker ps -a | grep "Exited" | awk '{print $1 }') //停止容器
$ docker rm $(docker ps -a | grep "Exited" | awk '{print $1 }') //删除容器
$ docker rmi $(docker images | grep "none" | awk '{print $3}') //删除镜像
这三条都是靠 grep 匹配文字来筛 ID 的,属于比较糙的写法,好处是任何版本的 Docker 都能跑。现在的 Docker 其实自带了更安全的清理命令,docker image prune 专门清悬空镜像,docker system prune 连停掉的容器、没在用的网络一起清,用它们不会误伤名字里刚好带 none 的镜像。
# 1.3 环境准备
一个完整的 Egg 服务跑起来要四样东西,Node 运行时、MySQL、Redis、Nginx。这里拉取nginx、node、redis、mysql镜像,一个一个来。
# 1、安装 node 镜像
进入https://hub.daocloud.io 搜索node,切换到版本获取下载地址。
docker pull daocloud.io/library/node:12.18docker tag 28faf336034d node重命名镜像
docker tag 干的事情其实只是给同一个镜像挂一个新名字,不会复制一份数据,所以重命名镜像后IMAGE ID都是一样的。

拉一次镜像动辄几百兆,网络不好的时候拉一次要好几分钟。所以我习惯把常用镜像导出到本地存着,换机器或者重装 Docker 直接导回来。也可以导出镜像到本地备份 docker save -o node.image(导出镜像要起的名称) 28faf336034d(要导出的镜像的ID)。

导完验证一下确实能用,我们先删除之前的镜像 docker rmi 28faf336034d -f 强制删除。加 -f 是因为这个镜像上还挂着 tag 和容器引用,不强制删会被拒绝。

再次导入本地镜像
docker load -i node.image(导入的镜像名称)
导入完你会发现镜像的 REPOSITORY 和 TAG 都是 <none>,这是正常的,docker save 存的是镜像层数据,标签信息没跟着回来。

然后再次重命名镜像即可
docker tag 28faf336034d node:v1.0(版本v1.0)

补一句时效性的话,Node 12 这条线早就结束维护了,当年写这篇的时候它还是可用的 LTS,今天再起新项目不要再挑这个版本,直接用当前的 LTS 大版本,安全补丁和 npm 生态兼容性都会好很多。下面所有出现 node:12.18 的地方,思路照搬,版本号换成你项目实际用的就行。
# 2、安装 MySQL 镜像
进入https://hub.daocloud.io 搜索mysql,切换到版本获取下载地址。

docker pull daocloud.io/library/mysql:8.0.20
拉取过程会一层一层往下走,每一层对应 Dockerfile 里的一条指令,已经存在的层会直接显示 Already exists 跳过。

启动MySQL镜像
这条命令里参数不少,逐个看一下就明白了。-p 3307:3306 是把宿主机的 3307 映射到容器里的 3306,之所以不用 3306 对 3306,是因为很多人本机早就装过一个 MySQL 占着 3306,撞端口会直接启动失败。-e 注入的环境变量是官方镜像约定好的,容器第一次启动时会读它来初始化 root 密码。
docker run -d(后台运行) -p 3307:3306(本机端口:MySQL运行端口) --name mysql(容器名称) -e MYSQL_ROOT_PASSWORD=123456(设置mysql密码) be0dbf01a0f3(mysql镜像ID)
这里有个坑要注意,MYSQL_ROOT_PASSWORD 只在数据目录为空、也就是容器第一次初始化的时候生效。如果你挂载了 volume 之后再改这个变量,密码是不会变的,重来一次得先把挂载出来的数据目录删干净。
查看当前正在运行的镜像
docker ps -a(正在运行和停止的镜像-a都可见)
不带 -a 只能看到运行中的容器,容器起不来直接挂掉的时候你会看到一片空白,误以为命令没执行。加上 -a 才能看到那条 Exited (1) 的记录,再去 docker logs 里翻原因。

删除容器
删除之前需要 stop,也就是 docker stop bac2692e2b9a(容器ID),运行中的容器是删不掉的。
docker rm bac2692e2b9a(容器ID:docker ps获取)
进入容器内部
docker exec -it bac2692e2b9a(容器ID) sh(指定进入方式)
-it 这两个参数经常被一起写,-i 保持标准输入打开,-t 分配一个伪终端,少了任意一个你都敲不了交互式命令。进去之后就是一个普通的 shell,可以直接 mysql -uroot -p 登进数据库确认服务是活的。

容器内部能连上只说明服务起来了,还得验证端口映射有没有真的通到宿主机。我们使用Navicat新建一个连接测试一下,主机填 127.0.0.1,端口填映射出来的 3307。

能连上,说明我们使用docker安装MySQL的方式是没问题的。
查看MySQL容器日志
docker logs -f(持续跟随输出) bac2692e2b9a(容器ID)
-f 是 follow,效果类似 tail -f,会挂在那里持续打印新日志,按 Ctrl + C 退出。如果你只想看最后几条,用的是 --tail 100 这个参数,两个别记混了。容器起不来的时候,答案九成写在这里面。

重启容器
如果修改了容器配置,我们需要重新启动容器。
docker restart bac2692e2b9a(容器ID)
设置MySQL权限
mysql8.0后,需要设置,否则node连接不上