Reading view

There are new articles available, click to refresh the page.

GitHub 全家桶:Actions 自动构建多架构 Docker 镜像并上传至 Packages (ghcr.io)

前段时间把 GitHub 的用户名修改成了 @prinsss,准备把其他地方的账号也修改一下的时候,却发现 Docker Hub 的 username 不能改,只能砍掉重练(npm 也是)。

想想反正我 Docker Hub 上也没上传什么东西,不如就用 GitHub 自家的 Container registry 来托管镜像吧!

这里有个小插曲:其实我挺早之前就想要改名了,但当时在忙秋招,考虑到改名后可能会有一些后续要处理(擦屁股),所以只是创建了一个 organization 把名字占住,等有时间了再正式改名。然而后来我把组织删了,想要修改 GitHub 账户的用户名时,却提示 prinsss 这个名称 unavailable(我确定它是没被占用的,因为我还能再用这个名字创建组织),不知道是不是触发了内部的什么保留机制。

最后还是发工单找客服解决了,而且等了一个多星期才回复,也是挺无语的。原来的 printempw 这个名字我也保留了,所以 printempw.github.io 这个域名还是可以访问的,目前是两边同步更新,后续再慢慢迁移。

GitHub Packages 介绍

其实最开始知道这个还是因为 Homebrew,看它每次安装软件下载 bottle 时都会从 ghcr.io 这个域名下载。好奇去查了一下,发现原来 GitHub 自己也整了一个软件包仓库,颇有一统天下的味道。

GitHub Packages 支持托管 Docker、npm、Maven、NuGet、RubyGems 等软件包,用起来比较像私有库。比起官方 registry 的好处就是其与 GitHub 完全集成,可以把源代码和软件包整合在一起,包括权限管理都可以用 GitHub 的那一套。

GitHub Packages 对于开源项目完全免费,私有仓库也有一定的免费额度

手动上传镜像

基础用法和 Docker Hub 是一样的,只是 namespace 变为了 ghcr.io。

首先创建一个 PAT (Personal Access Token) 用于后续认证:

  • 打开 https://github.com/settings/tokens/new?scopes=write:packages
  • 创建一个 PAT,勾选 write:packages 权限

注意:如果是在 GitHub Actions 中访问 GitHub Packages,则应该使用 GITHUB_TOKEN 而非 PAT 以提升安全性。后续章节会说明如何在 Actions 中使用 GITHUB_TOKEN

然后我们就可以用这个 Token 登录镜像仓库了:

export CR_PAT=YOUR_TOKENecho $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin

尝试一下推送镜像:

docker tag hello-world:latest ghcr.io/prinsss/hello-world:latestdocker push ghcr.io/prinsss/hello-world:latest

可以看到已经出现在 GitHub 上了:

packages-hello-world

刚上传的镜像默认都是 private,可以在 Package Settings 下方的 Change package visibility 处修改为公开镜像。

自动构建并上传

连镜像都放 GitHub 上了,那怎么好意思不用 GitHub Actions 呢!

下面就使用 Actions 实现代码更新后自动构建多架构 Docker 镜像,打 tag 并发布。

废话不多说,直接贴配置:

# yaml-language-server: $schema=https://json.schemastore.org/github-workflowname: Build Docker Image# 当 push 到 master 分支,或者创建以 v 开头的 tag 时触发,可根据需求修改on:  push:    branches:      - master    tags:      - v*env:  REGISTRY: ghcr.io  IMAGE: prinsss/ga-hit-counterjobs:  build-and-push:    runs-on: ubuntu-latest    # 这里用于定义 GITHUB_TOKEN 的权限    permissions:      packages: write      contents: read    steps:      - name: Checkout        uses: actions/checkout@v2      # 缓存 Docker 镜像以加速构建      - name: Cache Docker layers        uses: actions/cache@v2        with:          path: /tmp/.buildx-cache          key: ${{ runner.os }}-buildx-${{ github.sha }}          restore-keys: |            ${{ runner.os }}-buildx-      # 配置 QEMU 和 buildx 用于多架构镜像的构建      - name: Set up QEMU        uses: docker/setup-qemu-action@v1      - name: Set up Docker Buildx        id: buildx        uses: docker/setup-buildx-action@v1      - name: Inspect builder        run: |          echo "Name:      ${{ steps.buildx.outputs.name }}"          echo "Endpoint:  ${{ steps.buildx.outputs.endpoint }}"          echo "Status:    ${{ steps.buildx.outputs.status }}"          echo "Flags:     ${{ steps.buildx.outputs.flags }}"          echo "Platforms: ${{ steps.buildx.outputs.platforms }}"      # 登录到 GitHub Packages 容器仓库      # 注意 secrets.GITHUB_TOKEN 不需要手动添加,直接就可以用      - name: Log in to the Container registry        uses: docker/login-action@v1        with:          registry: ${{ env.REGISTRY }}          username: ${{ github.actor }}          password: ${{ secrets.GITHUB_TOKEN }}      # 根据输入自动生成 tag 和 label 等数据,说明见下      - name: Extract metadata for Docker        id: meta        uses: docker/metadata-action@v3        with:          images: ${{ env.REGISTRY }}/${{ env.IMAGE }}      # 构建并上传      - name: Build and push        uses: docker/build-push-action@v2        with:          context: .          file: ./Dockerfile          target: production          builder: ${{ steps.buildx.outputs.name }}          platforms: linux/amd64,linux/arm64          push: true          tags: ${{ steps.meta.outputs.tags }}          labels: ${{ steps.meta.outputs.labels }}          cache-from: type=local,src=/tmp/.buildx-cache          cache-to: type=local,dest=/tmp/.buildx-cache      - name: Inspect image        run: |          docker buildx imagetools inspect \          ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ steps.meta.outputs.version }}

自动构建的效果可以在我的 GitHub 上查看(其实就是之前写的那个 使用 Google Analytics API 实现博客阅读量统计)。

另外有几个需要注意的点:

上传时出现 400 Bad Request

这个昨天搞得我真是一脸懵逼,报错是这样的:

#16 exporting to image#16 pushing layers 0.5s done#16 ERROR: unexpected status: 400 Bad Request------ > exporting to image:------error: failed to solve: unexpected status: 400 Bad RequestError: buildx failed with: error: failed to solve: unexpected status: 400 Bad Request

排查了好久,最后发现是我打 tag 的时候忘记加上用户名了,原本是 ghcr.io/prinsss/ga-hit-counter 的,我给打成了 ghcr.io/ga-hit-counter,难怪推不上去(也要吐槽一下这个报错,就一个 400 鬼知道是什么啊)。

上传时出现 403 Forbidden

把上面那个解决了以后,心想这次总该成了吧,结果又来了个 403,我又一脸懵逼:

#16 exporting to image#16 pushing layers 0.7s done#16 ERROR: unexpected status: 403 Forbidden------ > exporting to image:------error: failed to solve: unexpected status: 403 ForbiddenError: buildx failed with: error: failed to solve: unexpected status: 403 Forbidden

再一番排查,发现是需要在 Package Settings 中的 Manage Actions access 处指定可以访问该软件包的源码仓库(也就是 Actions 所在的仓库)。好吧……

manage-actions-access

添加了仓库,这下确实可以了。

元数据自动生成

docker/metadata-action 这是一个比较有意思的 action,它可以从源码以及触发构建的 event 中获取数据,自动生成相应的 Docker 镜像 tag 以及 label。(在 GitHub 文档官方的示例中,这是由一段脚本完成的)

比如默认的效果就是:

EventRefDocker Tags
pull_requestrefs/pull/2/mergepr-2
pushrefs/heads/mastermaster
pushrefs/heads/releases/v1releases-v1
push tagrefs/tags/v1.2.3v1.2.3, latest
push tagrefs/tags/v2.0.8-beta.67v2.0.8-beta.67, latest

也就是我现在使用的:源码推送到 master 分支则自动构建并更新 master tag 的镜像;在 git 上创建以 v 开头的 tag,Docker 那边也会自动创建相应的 tag 并且更新 latest,不错不错。(不过想想我可能保留一个 tag 触发就够了)

比如我 push 了一个 v0.2.0 的 tag 上去,自动生成的元数据就是这样的:

Run docker/metadata-action@v3Context info  eventName: push  sha: 6071f564087d49be48dc318b89fc22ff96cf6a17  ref: refs/tags/v0.2.0  workflow: Build Docker Image  action: meta  actor: prinsss  runNumber: 11  runId: 1495122573Docker tags  ghcr.io/prinsss/ga-hit-counter:v0.2.0  ghcr.io/prinsss/ga-hit-counter:latestDocker labels  org.opencontainers.image.title=google-analytics-hit-counter  org.opencontainers.image.description=Page views counter that pulls data from Google Analytics API.  org.opencontainers.image.url=prinsss/google-analytics-hit-counter  org.opencontainers.image.source=prinsss/google-analytics-hit-counter  org.opencontainers.image.version=v0.2.0  org.opencontainers.image.created=2021-11-23T14:10:35.953Z  org.opencontainers.image.revision=6071f564087d49be48dc318b89fc22ff96cf6a17  org.opencontainers.image.licenses=MIT

如果想要修改为其他方案,action 也提供了丰富的配置项,可以自行修改。

最后

用 GitHub Packages 托管 Docker 镜像,体验还是挺不错的。硬要说有什么缺点的话就是不好配置国内镜像吧,毕竟大部分国内镜像都是对应 Docker Hub 的。

另外多架构镜像的这个构建时间也是挺感人,模拟 arm64 一次要六七分钟,哈人。(所以写 Dockerfile 还是挺讲究的,怎么让缓存效率最大化,这方面还得再学习)

参考链接:

题外话,秋招后这段时间我也折腾了一些东西,有空慢慢发出来吧。

使用国内镜像加速 Laravel Sail 构建

Laravel Sail 是什么?简单来说就是一个基于 Docker 的开发环境。其核心就是一个 docker-compose.yml 配置文件和 sail 脚本,定义了 PHP、MySQL、Redis 等一系列容器,然后把程序放里面跑。

至于好处嘛,主要就是使用方便、运行环境统一、不会弄乱系统。同样是 Laravel 开发,本机安装 LNMP、Valet、Homestead 这些方法我都用过,但现在我肯定首选 Laravel Sail(容器化是真滴爽)。

不过 Laravel Sail 好是挺好,想要在墙内顺利使用还是要费点功夫的。

创建项目

以下操作均在国内网络环境下运行。其实挂个全局代理这些都不是事啦

安装 Docker 和配置 Docker Hub 国内镜像的步骤可以参考这篇文章

创建项目时,直接用 Laravel 官方的安装脚本大概率会卡在 Updating dependencies 上,为什么你懂的。不过我们还是可以借鉴下官方脚本的内容:

curl -s https://laravel.build/example-app -o install.sh

临时新建一个 composer 容器:

docker run -it --rm -v $(pwd):/opt -w /opt \  laravelsail/php80-composer:latest bash

在容器内配置 composer 镜像,创建 Laravel 项目:

composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/laravel new example-app

安装 Laravel Sail 相关文件(这里可自定义需要使用的服务):

cd example-appphp ./artisan sail:install --with=mysql,redis,meilisearch,mailhog,selenium

导出底层 Dockerfile,方便后续定制:

php ./artisan sail:publish

完成后,可以看到项目下多出了一个 docker 目录和 docker-compose.yml 文件。

定制 Dockerfile

Laravel Sail 有一个主要的容器,用于运行 PHP 主程序。其他比如 composer, artisan, node, npm 命令也是在这个容器里运行的。

后续你可以通过 sail shell 来访问这个容器。

这个主容器的 Dockerfile 就是我们上面导出的那个。可以看到它主要就是在 Ubuntu 的基础上安装了一些软件包,大部分的网络问题都是这里造成的。

使用国内镜像替换之,主要需要替换的软件源有:

  • Ubuntu
  • PPA
  • Composer
  • Node.js
  • Npm

修改后的 Dockerfile 如下:

FROM ubuntu:21.04LABEL maintainer="Taylor Otwell"ARG WWWGROUPWORKDIR /var/www/htmlENV DEBIAN_FRONTEND noninteractiveENV TZ=Asia/ShanghaiENV APT_MIRROR http://mirrors.ustc.edu.cnENV NVM_DIR /usr/local/nvmENV NODE_VERSION 16.9.1ENV NVM_NODEJS_ORG_MIRROR https://mirrors.ustc.edu.cn/nodeENV NVM_SOURCE https://hub.fastgit.org/nvm-sh/nvm.gitSHELL ["/bin/bash", "-o", "pipefail", "-c"]RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezoneRUN echo 'APT::Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries \  && sed -i "s|http://archive.ubuntu.com|$APT_MIRROR|g; s|http://security.ubuntu.com|$APT_MIRROR|g" /etc/apt/sources.list \  && sed -i "s|http://ports.ubuntu.com|$APT_MIRROR|g" /etc/apt/sources.list \  && apt-get update \  && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 \  && mkdir -p ~/.gnupg \  && chmod 600 ~/.gnupg \  && echo "disable-ipv6" >> ~/.gnupg/dirmngr.conf \  && apt-key adv --homedir ~/.gnupg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys E5267A6C \  && apt-key adv --homedir ~/.gnupg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C300EE8C \  # PHP  && echo "deb https://launchpad.proxy.ustclug.org/ondrej/php/ubuntu hirsute main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \  && apt-get update \  && apt-get install -y php8.0-cli php8.0-dev \    php8.0-pgsql php8.0-sqlite3 php8.0-gd \    php8.0-curl php8.0-memcached \    php8.0-imap php8.0-mysql php8.0-mbstring \    php8.0-xml php8.0-zip php8.0-bcmath php8.0-soap \    php8.0-intl php8.0-readline php8.0-pcov \    php8.0-msgpack php8.0-igbinary php8.0-ldap \    php8.0-redis php8.0-swoole php8.0-xdebug \  # Composer  && curl -so /usr/bin/composer https://mirrors.aliyun.com/composer/composer.phar \  && chmod a+x /usr/bin/composer \  && composer --version \  && composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/ \  # Node.js  && mkdir -p $NVM_DIR \  && curl -so- https://raw.fastgit.org/nvm-sh/nvm/v0.38.0/install.sh | bash \  && source $NVM_DIR/nvm.sh \  && nvm install $NODE_VERSION \  && nvm use $NODE_VERSION \  && node -v && npm -v \  && npm config set registry https://registry.npm.taobao.org \  # Yarn  && npm install --global yarn \  && yarn config set registry https://registry.npm.taobao.org \  && apt-get install -y mysql-client \  && apt-get install -y postgresql-client \  && apt-get -y autoremove \  && apt-get clean \  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modulesENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATHRUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.0RUN groupadd --force -g $WWWGROUP sailRUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sailCOPY start-container /usr/local/bin/start-containerCOPY supervisord.conf /etc/supervisor/conf.d/supervisord.confCOPY php.ini /etc/php/8.0/cli/conf.d/99-sail.iniRUN chmod +x /usr/local/bin/start-containerEXPOSE 8000ENTRYPOINT ["start-container"]

目前有个不足之处就是 Node.js 的安装,原来用的 NodeSource 现在没有可用的国内镜像源,只能改用 nvm 安装。但是 nvm 在 PATH 的处理上有些问题(它是通过一个脚本修改环境变量,把当前启用的 Node 版本添加到 PATH 里去,但是 Dockerfile 里不能动态设置 ENV),目前只能手动指定要安装的 Node 版本了。

运行容器

使用 Laravel Sail 提供的脚本运行容器:

./vendor/bin/sail up

这个脚本主要的工作就是读取 .env 里配置的环境变量,然后通过 docker-compose 在容器里执行相应命令,所以基本用法和 docker-compose 是一致的:

sail up -d  # 后台运行sail down   # 停止运行

耐心等待镜像构建完成,Laravel Sail 就可以正常运行啦。

Creating network "example-app_sail" with driver "bridge"Creating example-app_redis_1       ... doneCreating example-app_selenium_1    ... doneCreating example-app_meilisearch_1 ... doneCreating example-app_mailhog_1      ... doneCreating example-app_mysql_1       ... doneCreating example-app_laravel.test_1 ... done

laravel-sail-app

参考链接

在 M1 Mac 上构建 x86 Docker 镜像

今天闲来无事,数了一下服务器上在跑的东西,打算把它们都扔到 Docker 里面去。第一个开刀的就是之前写的 Google Analytics 博客阅读量统计,很简单的一个 Node.js + Express 程序。

写完 Dockerfile 测试好,正准备 push 上去时,我才突然想起来:

我现在用的是 M1 MacBook,丫的默认 build 出来的镜像是 arm64 架构的呀!

好在 M1 Mac 上的 Docker Tech Preview 也支持使用 buildx 构建多架构的镜像,稍微设置一下就可以了。

题外话,M1 MacBook Air 真的很好用,建议早买早享受(

启用实验性功能

Docker 的 buildx 还是实验性功能,需要在 Docker Desktop 设置中开启,具体位于 Preferences > Experimental Features > Enable CLI experimental features

新建 builder 实例

Docker 默认的 builder 不支持同时指定多个架构,所以要新建一个:

docker buildx create --use --name m1_builder

查看并启动 builder 实例:

docker buildx inspect --bootstrap
Name:   m1_builderDriver: docker-containerNodes:Name:      m1_builder0Endpoint:  unix:///var/run/docker.sockStatus:    runningPlatforms: linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/arm/v7, linux/arm/v6

其中 platforms 就是支持的架构,跨平台构建的底层是用 QEMU 实现的。

构建多架构 Docker 镜像

使用 buildx 构建:

docker buildx build \  --platform linux/amd64,linux/arm64  --push -t prinsss/google-analytics-hit-counter .

其中 -t 参数指定远程仓库,--push 表示将构建好的镜像推送到 Docker 仓库。如果不想直接推送,也可以改成 --load,即将构建结果加载到镜像列表中。

--platform 参数就是要构建的目标平台,这里我就选了本机的 arm64 和服务器用的 amd64。最后的 .(构建路径)注意不要忘了加。

构建完 push 上去以后,可以查看远程仓库的 manifest:

docker buildx imagetools inspect prinsss/google-analytics-hit-counter
Name:      docker.io/prinsss/google-analytics-hit-counter:latestMediaType: application/vnd.docker.distribution.manifest.list.v2+jsonDigest:    sha256:a9a8d097abb4fce257ae065365be19accebce7d95df58142d6332270cb3e3478Manifests:  Name:      docker.io/prinsss/google-analytics-hit-counter:latest@sha256:bb7f3a996b66a1038de77db9289215ef01b18e685587e2ec4bb0a6403cc7ce78  MediaType: application/vnd.docker.distribution.manifest.v2+json  Platform:  linux/amd64  Name:      docker.io/prinsss/google-analytics-hit-counter:latest@sha256:94ea08ac45f38860254e5de2bae77dee6288dd7c9404d8da8a3578d6912e68e7  MediaType: application/vnd.docker.distribution.manifest.v2+json  Platform:  linux/arm64

可以看到已经是支持多架构的镜像了。

参考链接

❌