内容简介: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
。这里大概有三部分:
- 安装系统依赖
-
根据
Gemfile
和package.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 .
总结
实际上,这个教程用了如下的技术来加速构建和减少最终镜像大小:
-
--cahce-from
option: https://docs.docker.com/engine/reference/commandline/build/ - Multi-stage build: https://docs.docker.com/develop/develop-images/multistage-build/
-
Build-time variables (
--build-arg
): https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables—build-arg -
GitLab CI:
only
: https://docs.gitlab.com/ee/ci/yaml/#onlyexcept-basic
通过使用这些技术,我们不仅可以节约构建时间,也可以降低最终构建镜像的大小,从而获得愉悦的开发和部署体验。
未来的话题
这里本来应该有两个或者更多关于测试 和 lint 的话题,不过我准备延后发布这些内容。因为前三节花费了我太多时间。实际上,在 Docker Swarm 上使用 Traefik 和 Portainer webhooks 可以完全自动化这个 DevOps 流程。我想以后的空闲时间再来写这些。
小提示
- 如果你不希望在你的开发机器上使用 Docker, VS Code Remote Development 是一个非常棒的工具,可以在远程机器上使用。
以上所述就是小编给大家介绍的《在 Rails 项目中使用 Docker 和 GitLab CI 高效构建镜像(第一部分)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
快速转行做产品经理
李三科 / 华中科技大学出版社 / 2018-6-1 / 39.90
互联网已经进入以产品为中心的时代,不懂技术一样做高薪产品经理。本书将满足你转行、就业、加薪的愿望。 . 作者李三科,互联网资深产品经理。2011年离开传统销售行业进入互联网行业工作,从对产品经理的工作一无所知,到成长为一名年薪几十万的资深产品经理,他对产品经理职业有着深刻的理解,也积累了丰富的学习、工作经验。本书以作者亲身经历为线索,讲解学习产品经理相关知识和工作方法的经验,同时介绍求......一起来看看 《快速转行做产品经理》 这本书的介绍吧!