Circle Image

Wen Lin Gang

Ruby & Go Full-stack programmer @ Lifelong learner.

使用 Docker 优化打包 Rails

依赖环境

ruby: 3.2.1

rails: 7.0.6

node: 18

webpack: 5.4

tailwindcss 3

docker: 23.0.5

目标

通过对打包过程的了解和优化,得到一个体积合适且可用的 Rails 镜像

PS: 想直接看结果的直接跳到最后即可

新建测试项目 myapp

rails new myapp -d postgresql --css tailwind

bundle add webpacker

bundle install

rails webpacker:install

rails tailwindcss:install

rails db:create

rails s

使用 ruby:3.2.1 构建

确保上方执行完成项目可访问

将下方代码复制并放在项目根目录(任意目录都可)的 Dockerfile

FROM ruby:3.2.1

RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak && \
    echo "deb https://mirrors.aliyun.com/debian/ bullseye main non-free contrib" >/etc/apt/sources.list && \
    echo "deb-src https://mirrors.aliyun.com/debian/ bullseye main non-free contrib" >>/etc/apt/sources.list && \
    echo "deb https://mirrors.aliyun.com/debian-security/ bullseye-security main" >>/etc/apt/sources.list && \
    echo "deb-src https://mirrors.aliyun.com/debian-security/ bullseye-security main" >>/etc/apt/sources.list && \
    echo "deb https://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib" >>/etc/apt/sources.list && \
    echo "deb-src https://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib" >>/etc/apt/sources.list && \
    echo "deb https://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib" >>/etc/apt/sources.list && \
    echo "deb-src https://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib" >>/etc/apt/sources.list

RUN apt-get update && apt-get install -y \
  curl -sL https://deb.nodesource.com/setup_18.x |  bash - && \
  curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
  echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
  apt-get update && apt-get install -y nodejs yarn postgresql-client net-tools

WORKDIR /app

ENV RAILS_ENV=production

ENV NODE_ENV=production

COPY Gemfile* package.json yarn.lock ./

RUN bundle install --verbose --jobs 20 --retry 5
RUN yarn --ignore-engines

COPY . .

RUN bin/rails assets:precompile

EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]

执行打包命令

# 如果 Dockerfile 在根目录
docker build . -t myapp

# 如果 Dockerfile 在其他目录
docker build . -f <Dockerfile Path> -t myapp

我们最终得到的 docker 镜像大小如下:

REPOSITORY   TAG     SIZE
myapp        latest  1.39GB

可以看到还没有任何业务代码的新项目打包都这么大,这是不可接受的,因为是用的 ruby:3.2.1 作为基础镜像的,我们看一下他的镜像大小

docker pull ruby:3.2.1
REPOSITORY   TAG     SIZE
ruby         3.2.1   844MB

看来我们需要寻找到一个可以替代并且体积更小的镜像,通过 ruby 官方镜像 能看到官方提供了 Alpine 镜像

docker pull ruby:3.2.1-alpine
REPOSITORY   TAG            SIZE
ruby         3.2.1-alpine   79.9MB

竟然少了 764MB!

使用 ruby:3.2.1-alpine 构建

https://www.alpinelinux.org/

使用 Alpine 镜像作为基础镜像可能有点复杂,需要手动运行一些命令并安装一些依赖包

FROM ruby:3.2.1-alpine

RUN apk update \
    && apk upgrade \
    && apk add --update --no-cache \
    build-base curl-dev git libc6-compat postgresql-dev \
    yaml-dev zlib-dev nodejs yarn tzdata

WORKDIR /app

ENV RAILS_ENV=production

ENV NODE_ENV=production

COPY Gemfile* package.json yarn.lock ./

RUN bundle install --verbose --jobs 20 --retry 5
RUN yarn --ignore-engines

COPY . .

RUN NODE_OPTIONS=--openssl-legacy-provider RAILS_ENV=production bundle exec rails assets:precompile

EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]

执行打包命令,我们得到的镜像大小如下:

REPOSITORY          TAG     SIZE
myapp-alpine        latest  1.16GB

对比 ruby:3.2.1ruby:3.2.1-alpine 的结果看是小了一些,但作为实际生产用仍然太大,我们还可以继续优化 docker 打包流程

REPOSITORY          TAG       SIZE
myapp-alpine        latest    1.16GB
myapp               latest    1.39GB

通过多步骤构建 docker 减少构建层数

https://docs.docker.com/build/building/multi-stage/

就像 Git 仓库一样,随着提交/更改的 commits 数量增加项目体积会越来越大,同样 docker 镜像的大小也取决于构建层数,我们可以通过减少 RUN 和 COPY 语句的数量,或者通过类似于 git squash 的多步构建来减少层数

# 构建基础包
FROM ruby:3.2.1-alpine AS build-env

ARG RAILS_ROOT=/app
ARG BUILD_PACKAGES="build-base curl-dev git libc6-compat"
ARG DEV_PACKAGES="postgresql-dev yaml-dev zlib-dev nodejs yarn"
ARG RUBY_PACKAGES="tzdata"

ENV RAILS_ENV=production
ENV NODE_ENV=production
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"

WORKDIR $RAILS_ROOT

RUN apk update \
    && apk upgrade \
    && apk add --update --no-cache $BUILD_PACKAGES $DEV_PACKAGES \
       $RUBY_PACKAGES

COPY Gemfile* package.json yarn.lock ./

RUN bundle config set --local path vendor/bundle \
    && bundle config set --local without 'development:test:assets' \
    && bundle install -j4 --retry 3

RUN yarn install

COPY . .

RUN NODE_OPTIONS=--openssl-legacy-provider RAILS_ENV=production bundle exec rails assets:precompile

############### 引用构建好的基础包 ###############
FROM ruby:3.2.1-alpine

ARG RAILS_ROOT=/app

ARG PACKAGES="tzdata postgresql-client nodejs bash"

ENV RAILS_ENV=production

ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"

WORKDIR $RAILS_ROOT

RUN apk update \
    && apk upgrade \
    && apk add --update --no-cache $PACKAGES

COPY --from=build-env $RAILS_ROOT $RAILS_ROOT

EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]

这里打一个新的包作为对比

docker build . -t myapp-alpine-v2

对比与前者打包结果

REPOSITORY          TAG       SIZE
myapp-alpine-v2     latest    300MB
myapp-alpine        latest    1.16GB
myapp               latest    1.39GB

是的,相比较少了将近 4 倍,但我们其实还可以再优化

移除不必要的打包文件,即:最终生产使用的 Dockerfile

这里移除的文件都是生产中不需要的,不会影响系统运行的,比如:

# 构建基础包
FROM ruby:3.2.1-alpine AS build-env

ARG RAILS_ROOT=/app
ARG BUILD_PACKAGES="build-base curl-dev git libc6-compat"
ARG DEV_PACKAGES="postgresql-dev yaml-dev zlib-dev nodejs yarn"
ARG RUBY_PACKAGES="tzdata"

ENV RAILS_ENV=production
ENV NODE_ENV=production
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"

WORKDIR $RAILS_ROOT

RUN apk update \
    && apk upgrade \
    && apk add --update --no-cache $BUILD_PACKAGES $DEV_PACKAGES \
       $RUBY_PACKAGES

COPY Gemfile* package.json yarn.lock ./

RUN bundle config set --local path vendor/bundle \
    && bundle config set --local without 'development:test:assets' \
    && bundle install -j4 --retry 3 \
    && rm -rf vendor/bundle/ruby/3.2.0/cache/*.gem \
    && find vendor/bundle/ruby/3.2.0/gems/ -name "*.c" -delete \
    && find vendor/bundle/ruby/3.2.0/gems/ -name "*.o" -delete

RUN yarn install

COPY . .

RUN NODE_OPTIONS=--openssl-legacy-provider RAILS_ENV=production bundle exec rails assets:precompile && \
    rm -rf node_modules tmp/cache vendor/assets test

############### 引用构建好的基础包 ###############
FROM ruby:3.2.1-alpine

ARG RAILS_ROOT=/app

ARG PACKAGES="tzdata postgresql-client nodejs bash"

ENV RAILS_ENV=production

ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"

WORKDIR $RAILS_ROOT

RUN apk update \
    && apk upgrade \
    && apk add --update --no-cache $PACKAGES

COPY --from=build-env $RAILS_ROOT $RAILS_ROOT

EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]

这里打一个新的包作为对比

docker build . -t myapp-alpine-v3

对比与前者打包结果

REPOSITORY          TAG       SIZE
myapp-alpine-v3     latest    174MB
myapp-alpine-v2     latest    300MB
myapp-alpine        latest    1.16GB
myapp               latest    1.39GB

最终我们将一个 1.39GB 的镜像缩小到了 174MB