在 Rails 项目中使用 Docker 和 GitLab CI 高效构建镜像(第一部分)

栏目: Ruby · 发布时间: 4年前

内容简介:Docker 是令人惊艳的 DevOps 工具,通过使用它可以节省大把时间,即使对个人项目来说也是如此,然而,把 Rails 项目有效地迁移到 Docker 技术栈上并不是一件容易的事情。通常我们会遇到如下问题:在这片文章里,你可以看到一个逐步 Docker 化 Rails 项目的比较完整流程,基于 GitLab CI。这个教程,对于 Rails 的 DevOps 来说,可能不是百分百完美。但是我认为这可以是一个不错的开始 :blush:。如果你非常熟悉 Rails 和 Docker,或者你甚至尝试了多次这

背景

Docker 是令人惊艳的 DevOps 工具,通过使用它可以节省大把时间,即使对个人项目来说也是如此,然而,把 Rails 项目有效地迁移到 Docker 技术栈上并不是一件容易的事情。通常我们会遇到如下问题:

  • 最终镜像体积过大。
  • 构建时间太长。安装项目依赖时会首先构建很多 工具 和库,这会消耗很多时间。
  • 不太容易复用 Docker 景象来进行测试。

在这片文章里,你可以看到一个逐步 Docker 化 Rails 项目的比较完整流程,基于 GitLab CI。这个教程,对于 Rails 的 DevOps 来说,可能不是百分百完美。但是我认为这可以是一个不错的开始 :blush:。

文章太长,不想看怎么办?

如果你非常熟悉 Rails 和 Docker,或者你甚至尝试了多次这个技术栈的话,请直接跳过第一和第二节,或者直接前往 GitHub ( https://github.com/imWildCat/rails-docker-demo ) 或者 GitLab 仓库 ( https://gitlab.com/imWildCat/docker-rails-demo )。如果你对 Docker 和 Gitlab CI 有丰富的经验,直接阅读问末尾总结部分也是一个不错的选择。

第 1 节: 开始使用 Rails 和 Docker

请注意,Rails 6.0.0 稳定版现在还没发布 (2019-06-29)。请使用这个命令 gem install --pre rails 安装 rc1 版本。

从一个简单而且干净的项目开始总是极好的。我们可以使用下面的命令来创建一个 Rails 项目:

rails new -d postgresql --webpack react rails_docker_demo

在这个阶段,我们不需要写 Rails 代码。直接进入 Docker 部分吧。

所以, Dockerfile 应该是什么样子呢?

FROM ruby:2.6.3-alpine3.8

# Install alpine packages
RUN apk add --no-cache \
  build-base \
  busybox \
  ca-certificates \
  cmake \
  curl \
  git \
  tzdata \
  gnupg1 \
  graphicsmagick \
  libffi-dev \
  libsodium-dev \
  nodejs \
  yarn \
  openssh-client \
  postgresql-dev \
  tzdata

# Define WORKDIR
WORKDIR /app

# Use bunlder to avoid exit with code 1 bugs while doing integration test
RUN gem install bundler -v 2 --no-doc

# Copy dependency manifest
COPY Gemfile Gemfile.lock /app/

# Install Ruby dependencies
RUN bundle update --bundler
RUN bundle install --jobs $(nproc) --retry 3 --without development test \
      && rm -rf /usr/local/bundle/bundler/gems/*/.git /usr/local/bundle/cache/

# Copy JavaScript dependencies
COPY package.json yarn.lock /app/

# Install JavaScript dependencies
RUN yarn install

# Define basic environment variables
ENV NODE_ENV production
ENV RAILS_ENV production
ENV RAILS_LOG_TO_STDOUT true

# Copy source code
COPY . /app/

# Build front-end assets
RUN bundle exec rails webpacker:verify_install
RUN SECRET_KEY_BASE=nein bundle exec rails assets:precompile

RUN chmod +x ./bin/entrypoint.sh

# Define entrypoint
ENTRYPOINT ["./bin/entrypoint.sh"]

你也可以在项目仓库的 v1-base-case 下( GitHub 或者 GitLab ),找到 bin/entrypoint.sh 的内容。

现在我们来看 .gitlab-ci.yml :

variables:
  # Prevent any locale errors
  LC_ALL: C.UTF-8
  LANG: en_US.UTF-8
  LANGUAGE: en_US.UTF-8

build_image:
  stage: build
  image: docker:stable
  services:
    - docker:dind
  variables:
    IMAGE_TAG: $CI_REGISTRY_IMAGE:latest
  before_script:
    - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
  script:
    - docker pull $IMAGE_TAG || echo "No pre-built image found."
    - docker build --cache-from $IMAGE_TAG -t $IMAGE_TAG . || docker build -t $IMAGE_TAG . # Use cache for building if possible
    - docker push $IMAGE_TAG

这个 CI 配置文件使用 --cahce-from 做了一些缓存,但是它没有特别大的作用。一旦我们修改了 alpine 的依赖库,整个流程都会被重新构建。在改进缓存之前,我们不如先部署这个 Rails 镜像试试。到目前位置,构建好的 Docker 镜像还不能被直接部署。

构建时间:

  • Rails 项目: 5 分 51 秒(从零开始,估计时间)

有关这部分的详细改动,请阅读这个 Merge Request: https://gitlab.com/imWildCat/docker-rails-demo/merge_requests/1 .

第 1.1 节:关于部署的改进

对于部署来说,我们都需要做哪些工作呢?

master.key

因为这个话题不是很契合这篇文章的主题,我想用一个 PR 来展示改动:

部署大概步骤如下:

  • 下载并修改 docker-compose.yml 到你的宿主机。
  • (可选)把 master.key 移动到宿主机的指定位置( docker-compose.yml 有设定)。
  • 拉取镜像: sudo docker-compose pull
  • 启动服务: sudo docker-compose up
  • (可选)在停止服务后,你可以执行 sudo docker-compose down 删除之前创建的所有资源。

我们也可以了解以下内容:

SECRET_KEY_BASE

构建时间:

  • Rails 项目: 5:09 (从零开始,估算时间),1:47(使用缓存,估算时间)
  • 反向代理:0:43 (估算时间)

在部署之后,你会看到 404 页面。因为我们没有写任何业务逻辑,所以这没问题。

第 2 节:Multi-stage 构建

在前一节构建的景象里,有一个非常明显的问题,就是镜像太大了(大概 617MB)。所以,我们可以做些什么来减小这个 Docker 镜像的体积呢?

首先,让我们来看看 Dockerfile 。这里大概有三部分:

  • 安装系统依赖
  • 根据 Gemfilepackage.json 安装项目依赖
  • 构建前端资源,设置基础环境

不错的事情是,我们可以利用 Docker 的 multi-stage builds 。在这个教程里,我不会太深入探讨这个特性,但是它有很大的潜力,值得探索。

在这个章节里,我们需要把原来的 Dockerfile 分为两部分:

Dockerfile.builder
Dockerfile

我们应该把 Dockerfile 里在最终构建 Rails 应用之前的代码移动到 Dockerfile.builder 里:

# Stage for dependencies installation
FROM ruby:2.6.3-alpine3.8 as builder

# Install alpine packages
RUN apk add --no-cache \
  build-base \
  busybox \
  ca-certificates \
  cmake \
  curl \
  git \
  tzdata \
  gnupg1 \
  graphicsmagick \
  libffi-dev \
  libsodium-dev \
  nodejs \
  yarn \
  openssh-client \
  postgresql-dev \
  tzdata

# Define WORKDIR
WORKDIR /app

# Use bunlder to avoid exit with code 1 bugs while doing integration test
RUN gem install bundler -v 2 --no-doc

# Copy dependency manifest
COPY Gemfile Gemfile.lock /app/

# Install Ruby dependencies
RUN bundle update --bundler
RUN bundle install --jobs $(nproc) --retry 3 --without development test

# Copy JavaScript dependencies
COPY package.json yarn.lock /app/

# Install JavaScript dependencies
RUN yarn install

我们也需要更新 Dockerfile ,使用两个 stages,由此我们可以减小最终的镜像体积。

# ARG: https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact
ARG BUILDER_IMAGE_TAG
FROM $BUILDER_IMAGE_TAG as builder

# Define basic environment variables
ENV NODE_ENV production
ENV RAILS_ENV production
ENV RAILS_LOG_TO_STDOUT true

# Copy source code
COPY . /app/

# Build front-end assets
RUN bundle exec rails webpacker:verify_install
RUN SECRET_KEY_BASE=nein bundle exec rails assets:precompile

RUN rm -rf node_modules

FROM ruby:2.6.3-alpine3.8 as deploy

RUN apk add --no-cache \
  ca-certificates \
  curl \
  tzdata \
  gnupg1 \
  graphicsmagick \
  libsodium-dev \
  nodejs \
  postgresql-dev \
  bash

# Define basic environment variables
ENV NODE_ENV production
ENV RAILS_ENV production
ENV RAILS_LOG_TO_STDOUT true
# Defined for future testing
ENV RAILS_SERVE_STATIC_FILES true

WORKDIR /var/www/app

COPY --from=builder /usr/local/bundle/ /usr/local/bundle/
COPY --from=builder /app/ /var/www/app/
# We will copy the files in to /app/public while app is starting.
# Otherwise, the asset files may not be updated if we use named volume.
COPY --from=builder /app/public /var/www/app/public_temp

RUN chmod +x ./bin/entrypoint.sh

# Define entrypoint
ENTRYPOINT ["./bin/entrypoint.sh"]

然后,我们也需要更新 CI 的配置 .gitlab-ci.yml

# ...
build_image:
  stage: build
  image: docker:stable
  services:
    - docker:dind
  variables:
    IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
    BUILDER_IMAGE_TAG: $CI_REGISTRY_IMAGE/builder:latest
  before_script:
    - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
  script:
    - docker pull $BUILDER_IMAGE_TAG || echo "No pre-built image found."
    - docker build --cache-from $BUILDER_IMAGE_TAG -t $BUILDER_IMAGE_TAG -f Dockerfile.builder . || docker build -t $BUILDER_IMAGE_TAG -f Dockerfile.builder . 
    - docker push $BUILDER_IMAGE_TAG
    - docker pull $IMAGE_TAG || echo "No pre-built image found."
    - docker build --cache-from $IMAGE_TAG --build-arg BUILDER_IMAGE_TAG=${BUILDER_IMAGE_TAG} -t $IMAGE_TAG . || docker build --build-arg BUILDER_IMAGE_TAG=${BUILDER_IMAGE_TAG} -t $IMAGE_TAG . # Use cache for building if possible
    - docker push $IMAGE_TAG
# ...

请注意我们在这里使用了: build-time variables (–build-arg)

另外,在 bin/entrypoint.sh 里也用了一点小技巧来处理静态资源:

rm -rf public/* # Remove assets in named volume
cp -r public_temp/* public/ # Copy new files from new image

通过这些改进,我们可以把最终镜像大小大概从 617MB 减小到 221MB ( 64.1% )。时间消耗大致相同。

关于细节,可以阅读这个 Merge Request: https://gitlab.com/imWildCat/docker-rails-demo/merge_requests/1 .

第 3 节:按需的 multi-stage 构建

在引入 multi-stage 构建后,你可能想知道,我们是否可以只在有必要的情况下构建 builder。因为对于绝大多数 commits 和 PRs 来说,我们不会改动依赖。只有业务逻辑是经常变动的。我们可以利用 GitLab CI 的 only 配置( https://docs.gitlab.com/ee/ci/yaml/#onlyexcept-basic )和 ‘stage’ 特性。他们非常好用。

首先,我们可以在 .gitlab-ci.yml 文件头部添加 stages 设定。

stages:
  - prebuild
  # - test # For future work
  - build
  # - deploy # For future work

然后,builder 的构建逻辑需要被移动到一个新的 stage prebuild 里:

construct_builder:
  stage: prebuild
  image: docker:stable
  services:
    - docker:dind
  only:
    changes:
      - Dockerfile
      - Dockerfile.builder
      - Gemfile
      - Gemfile.lock
      - package.json
      - yarn.lock
  variables:
    BUILDER_IMAGE_TAG: $CI_REGISTRY_IMAGE/builder:latest
  before_script:
    - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
  script:
    - docker pull $BUILDER_IMAGE_TAG || echo "No pre-built image found."
    - docker build --cache-from $BUILDER_IMAGE_TAG -t $BUILDER_IMAGE_TAG -f Dockerfile.builder . || docker build -t $BUILDER_IMAGE_TAG -f Dockerfile.builder . 
    - docker push $BUILDER_IMAGE_TAG

请注意,为了简单,我们这里继续使用 latest 标签。你可能需要根据需求改动它。

所以我们也可以继续简化 build_image

build_image:
  # ...
  script:
    - docker pull $BUILDER_IMAGE_TAG
    - docker build --build-arg BUILDER_IMAGE_TAG=${BUILDER_IMAGE_TAG} -t $IMAGE_TAG .
    - docker push $IMAGE_TAG

类似地,我们也需要在 build_reverse_proxy 里也使用这个方法。它的 stage 需要被改为 prebuild ,以备 construct_builder 构建失败。在这个特殊情况里,如果对反向代理做了任何改动, build_reverse_proxy 不会被触发了。

build_reverse_proxy:
  stage: prebuild
  # ...
  only:
    changes:
      - misc/reverse_proxy/**/*
      - misc/reverse_proxy/Dockerfile
  # ...

最后,我们终于可以只在必须的情况下,构建这些基础镜像了。构建 builder 这个基础镜像会花很多时间。通过这些工作,我们可以剩下很多 GitLab CI 月度配额。实际上,我们自己的时间也节约了很多。

构建时间

  • Builder(只会在依赖改变时触发): 4:38(估算)
  • Rails 应用:02:14 (估算),节约了 50% 的时间,相对于上一节。
  • 反向代理(只会在依赖改变时触发):没有变化。

详情可阅读这个 Merge Request: https://gitlab.com/imWildCat/docker-rails-demo/merge_requests/1 .

总结

实际上,这个教程用了如下的技术来加速构建和减少最终镜像大小:

通过使用这些技术,我们不仅可以节约构建时间,也可以降低最终构建镜像的大小,从而获得愉悦的开发和部署体验。

未来的话题

这里本来应该有两个或者更多关于测试 和 lint 的话题,不过我准备延后发布这些内容。因为前三节花费了我太多时间。实际上,在 Docker Swarm 上使用 TraefikPortainer webhooks 可以完全自动化这个 DevOps 流程。我想以后的空闲时间再来写这些。

小提示

  • 如果你不希望在你的开发机器上使用 Docker, VS Code Remote Development 是一个非常棒的工具,可以在远程机器上使用。

以上所述就是小编给大家介绍的《在 Rails 项目中使用 Docker 和 GitLab CI 高效构建镜像(第一部分)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

高性能JavaScript

高性能JavaScript

Nicholas C.Zakas / 丁琛、赵泽欣 / 电子工业出版社 / 2010-11 / 49.00元

如果你使用JavaScript构建交互丰富的Web应用,那么JavaScript代码可能是造成你的Web应用速度变慢的主要原因。《高性能JavaScript》揭示的技术和策略能帮助你在开发过程中消除性能瓶颈。你将会了解如何提升各方面的性能,包括代码的加载、运行、DOM交互、页面生存周期等。雅虎的前端工程师Nicholas C. Zakas和其他五位JavaScript专家介绍了页面代码加载的最佳方......一起来看看 《高性能JavaScript》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具