docker_postgresql.png

Dockerfile 多阶段构建

最近在手撕Gitea源码的时候,发现项目的Dockerfile里有这样的片段:

FROM golang:1.17-alpine3.15 AS build-env
#...
RUN go build contrib/environment-to-ini/environment-to-ini.go

FROM alpine:3.15
#...
COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
COPY --from=build-env /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini

经过一些搜索,发现docker支持多阶段构建。 所谓的多阶段构建就是在一个Dockerfile里面,可以通过构建中间镜像,方便的利用各种现成的集成环境来编译最终运行环境需要的文件。 最终环境里面可以做到非常干净,只有最终运行需要的文件,而不需要安装编译环境。

Docker版本过低和CentOS系统停止维护

我修改完我的Dockerfile,同时重构了一下我的网站项目结构,把前端和后端放进一个代码仓库里。 在推送到服务器后,意外的发现由于我的Docker版本过低,导致我的项目无法正常构建。 而且从我的版本更新到最新的Docker版本需要卸载就版本再装新版本,我的网站的数据都在Docker数据卷里,直接重装不太可能会有好结果。 正好最近CentOS 系统宣布停止维护。一个停止维护的系统bug一定会越来越多,软件支持率也会越来越低。 所以我决定不如换一个Ubuntu服务器吧。

野生运维之路

更换服务器意味着数据备份与恢复,不像在普通的系统里,将数据拖进拖出U盘里就叫备份和恢复。 在Docker里,数据是放在Docker数据卷里,并在容器启动时动态挂载到指定的容器上。 如何将它从数据卷里取出来再放回去,是一门艺术。 大致的过程可以分为,先将静态的文件打包并压缩,

backup.sh

VOLUME_NAME=("volume1" "volume2" "volume3")
for VOLUME in "${VOLUME_NAME[@]}"
  do
    docker run --rm \
      # 创建数据卷,将项目的命名数据卷挂载到本中间容器上
      --volume "${VOLUME}:${TEMPORARY_DIRECTORY_TO_STORE_VOLUME_DATA}" \
      # 创建第二个数据卷,将主机上一个路径与容器内的备份数据所在目录进行映射
      --volume "${BACKUP_DATA_DIRECTORY}:${TEMPORARY_DIRECTORY_TO_STORE_BACKUP_FILE}" \
      # 用最小巧的alpine镜像来处理文件的压缩。
      alpine \
      # 容器内执行压缩命令
      tar cvf "${TEMPORARY_DIRECTORY_TO_STORE_BACKUP_FILE}/${VOLUME}.tar" ${TEMPORARY_DIRECTORY_TO_STORE_VOLUME_DATA}
      # 压缩完之后我们就可以在 BACKUP_DATA_DIRECTORY 里找到对应数据了
done

对于数据库文件,不要直接拷贝,而是利用用数据库对应的备份恢复命令。我使用的是PostgreSQL。

docker run -it --rm \
      # 在跑服务的时候最好自己设定网络名称,限制变化,这里将项目的网络与这个中间容器关联
      --network=${DOCKER_COMPOSE_PREFIX}_${NETWORK_NAME} \
      # 将项目的命名数据卷挂载到本中间容器
      --volume "${DOCKER_COMPOSE_PREFIX}_${DB_VOLUME_NAME}:${TEMPORARY_DIRECTORY_TO_STORE_VOLUME_DATA}" \
      # 可以在环境变量中指定数据库的密码
      --env PGPASSWORD="${PGPASSWORD}" \
      # 这里我直接使用与数据库相同的镜像,
      postgres:13.3-alpine \
      # pg_dump 命令输出数据库备份文件
      pg_dump \
      # 这里的host一般就是数据库容器名,因为与项目网络相连,所以才访问得到
      --host=${DB_HOST} \
      --username=${DB_USERNAME} \
      --dbname=${DB_NAME} \
      --format=p > "${BACKUP_DATA_DIRECTORY}/db_data/backup.sql"

备份完了文件,可以使用scp命令直接将文件传输到新服务器

# 在旧服务器运行
scp -r /path/to/backup/file/directory username@new_server:/path/to/new/directory

不放心也可以先拷到自己电脑里一份

# 在本地运行
scp -r username@old_server:/path/to/backup/file/directory /path/to/my/computer/directory

当然要运行这两条命令,你需要先设置好登陆密钥对。

传输好了备份文件,终于可以在新服务器上恢复数据了。 在新服务器上构建好容器之后,先不急启动容器。 容器启动之后难免有新数据产生,特别是数据库,容器启动之后再恢复很有可能起冲突。

restore.sh

for VOLUME in "${VOLUME_NAME[@]}"
  do
    echo "${DOCKER_COMPOSE_PREFIX}_${VOLUME}"
    docker run --rm \
      --volume "${DOCKER_COMPOSE_PREFIX}_${VOLUME}:${TEMPORARY_DIRECTORY_TO_STORE_VOLUME_DATA}" \
      --volume "${BACKUP_DATA_DIRECTORY}:${TEMPORARY_DIRECTORY_TO_STORE_BACKUP_FILE}" \
      alpine \
      # 对应的解压命令
      tar xvf "${TEMPORARY_DIRECTORY_TO_STORE_BACKUP_FILE}/${VOLUME}.tar" -C ${TEMPORARY_DIRECTORY_TO_STORE_VOLUME_DATA} --strip 1
done

docker run -it --rm \
      --network=${DOCKER_COMPOSE_PREFIX}_${NETWORK_NAME} \
      --volume "${BACKUP_DATA_DIRECTORY}:${TEMPORARY_DIRECTORY_TO_STORE_BACKUP_FILE}" \
      --env PGPASSWORD="${PGPASSWORD}" \
      postgres:13.3-alpine \
      # 使用psql命令读取备份文件并恢复
      psql \
      --host=${DB_HOST} \
      --username=${DB_USERNAME} \
      --dbname=${DB_NAME} \
      --file="${TEMPORARY_DIRECTORY_TO_STORE_BACKUP_FILE}/db_data/backup.sql"

这时候再启动容器,就跟在旧服务器上感觉一模一样啦!

优化过程,享受生活

在实际情况下,这些脚本都保存在文件里,可以直接执行,也可以利用GNUMake的Makefile编写脚本的具体行逻辑。 有人会说Makefile又是什么东西?如果说.sh文件的作用是把linux shell命令扎堆执行,那么Makefile就是更加花里胡哨的shell命令执行器。 你可以编写文件让一系列又臭又长的shell命令只用一两个单词就按你的要求执行。这时候终于可以喝杯咖啡了。