最近在维护一个多年前的 Ruby on Rails 项目,产品环境的操作系统是 Ubuntu 14.04,Ruby 是 2.7.4,部署工具是 Capistrano 3。为了在我现在的开发环境中搭建起来一个可用的稳定的开发环境花费了不少功夫,最终采用 Docker 来解决了这个问题。

问题

软件开发是一个不断进化的过程,同时软件开发是一个工程的问题,软件项目基于一个开发的环境,依赖多个第三方的软件项目,于是不可避免有了软件包冲突、版本适配、软件升级等各种各样的问题。

操作系统有多种类型,具体的一个操作系统还分为不同时期的版本。

编程语言有多种类型,具体的一个编程语言还分为不同的版本。

软件包有多种类型,具体的一个包还分为不同的版本,并且它还依赖其它的第三方软件包。

这其实真的是一个比较复杂的问题。

我现在就遇到这样的一个问题,这个项目是运行在 Ubuntu 14.04 的操作系统中,而我本地开发用的是 Ubuntu 21.04,项目跑不起来,项目依赖老版本的 MySQL 服务器,和老版本的 Node。

解决方案

我之前有使用 Docker 的经验,主要是用 Docker 作为项目的打包部署工具。这次尝试用 Docker 在本地构建一个 Ubuntu 14.04 系统中的开发环境。

Dockerfile 文件如下:

FROM ubuntu:14.04 as build
RUN apt-get update && apt-get install -y libmysqlclient-dev mysql-server wget
RUN apt-get install -y build-essential
RUN apt-get install -y curl git-core zlib1g-dev libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev libcurl4-openssl-dev libffi-dev

RUN wget https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.4.tar.gz && \
  tar zxvf ruby-2.7.4.tar.gz && cd ruby-2.7.4 && \
  ./configure --prefix=/usr/local/ruby2.7.4 --disable-install-doc && \
  make && make install

ENV PATH /usr/local/ruby2.7.4/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

RUN curl -fsSL https://deb.nodesource.com/setup_14.x | sudo -E bash -
RUN apt-get install -y --force-yes nodejs
RUN npm install -g yarn
RUN apt-get install -y python-pip

WORKDIR /www

COPY misc/id_rsa /root/.ssh/id_rsa
COPY misc/id_rsa.pub /root/.ssh/id_rsa.pub
RUN chmod 400 /root/.ssh/id_rsa

COPY Gemfile* /www/
RUN bundle config set --local path 'vendor/bundle'
RUN bundle install

COPY package*.json /www/
RUN yarn --check-files

ENV RAILS_ENV production

EXPOSE 3000

COPY . /www
RUN cp config/database.yml.local_docker config/database.yml


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

entrypoint.sh 文件的内容大致如下:

/etc/init.d/mysql start
bundle exec rails db:create
bundle exec rails db:migrate
bundle exec rails db:seed

bundle exec rails s -b 0.0.0.0

这里的 Dockerfile 文件中最主要操作的就是安装 Ruby、Node、MySQL。在编写 Dockerfile 中的步骤时,应该将最稳定不容易发生变更的步骤放在靠前面的位置。因为 Docker 在基于 Dockerfile 中的指令构建镜像时,是采用了一个链式的层次结构,通过这样的文件系统方式,可以有效利用缓存,来节省构建的时间。如果在某一个步骤中插入了一条新指令,那么下次构建镜像时,会从新的指令处构建,而新的指令后面的步骤都会被重新执行,从而耗费时间。

构建镜像

docker build -t localapp .

容器实例

启动容器实例:

docker run --name localapp_app -p 127.0.0.1:3000:3000 --rm -it localapp bash

进入容器实例:

docker exec -it localapp_app bash

进入了容器实例中,就拥有了开发环境,操作和本地没有两样。

总结

采用 Docker 来构建开发环境,软件包安装方便,与主机环境完全隔绝,并且容易清理。我后面调研 Node 中的各种技术时完全采用了 Docker 的方式,构建一个沙盒镜像:

FROM node:16.17-buster as build
RUN npm install -g npm@8.19.2
USER node
EXPOSE 3000
WORKDIR /app

构建镜像 docker build -t learnjs .

启动沙盒环境容器:

docker run --name learnjs_app -p 127.0.0.1:3000:3000 -v ~/.bash_history:/home/node/.bash_history -v $(pwd):/app --rm -it learnjs bash