容器化软件是将其打包成标准化的单元,以方便开发和部署。容器将你的应用程序的代码和它的所有依赖关系捆绑在一起。容器可以完全独立存在;它包含一个包含你的软件、运行环境和系统库的包。容器帮助开发者和运营团队确保软件在任何环境下都能正常运行。通过将代码与基础设施分开,被 "容器化 "的应用程序在本地环境、测试环境和生产环境中的运行是一样的。
Docker是开发和部署软件的最流行的平台之一。Docker将软件打包成一个 "镜像",在Docker镜像上执行时,会在运行时变成一个容器。这种隔离允许开发者在一台主机上同时运行许多容器。
在对现有应用进行容器化时,Rails开发者面临一系列独特的挑战。本文将提供对一个功能性Rails应用进行容器化的演练,并解释其中的重要概念和陷阱。本文不是对容器或Docker的基本描述;相反,它是对开发人员在对生产应用进行容器化时面临的问题的解释。
前提条件
如果你正在关注,那么你显然需要一个尚未被docker化的Rails应用程序(这是docker对 "容器化 "的专门术语)。 我将使用RailsWork,这是我刚刚启动的一个功能齐全的副业项目。它是一个用Rails编写的工作板,并部署在Heroku上,但它并没有被容器化。
除此之外,你还需要安装Docker。一种流行的安装方式是使用Docker Desktop,它可以通过官方网站下载。
一旦应用程序被下载,运行安装程序。运行后,它会提示你将该应用程序拖到你的应用程序文件夹。然后,你必须从你的应用程序文件夹启动该应用程序,并授予它所要求的特权权限。作为确保Docker正确安装的最后一次检查,尝试通过运行以下程序从终端列出在你的机器上运行的容器。
docker ps
如果Docker已经安装了(而且你没有运行任何容器),你会得到一个空的列表,其中只有标题,看起来像这样。
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Docker文件
在我们深入研究之前,首先要明确术语,这很重要。
在你的Rails应用程序被 "Docker化 "之后,它将在一个容器中运行。容器是独立的,可以被替换,并且经常被重建。
容器是由一个镜像构建的。一个镜像是一个文件系统的虚拟快照,与元数据配对。
Dockerfile是描述如何创建镜像的源代码。Docker文件通常包含在Docker化应用的资源库中,并与应用的其他部分一起在版本控制中被追踪。
创建Docker文件比听起来要容易得多!Docker为我们提供了特殊的语法,抽象化了容器化的艰苦工作。首先,让你的方式到你想容器化的应用程序的根目录。现在你已经准备好开始工作了,如果你使用的是git,最好是创建一个新的分支。你可以通过运行以下程序,轻松创建一个名称为dockerize-this-app 的新分支。
git checkout -b dockerize-this-app
接下来,创建一个Docker文件,指示它建立一个基于Ruby应用程序的镜像。这可以通过运行以下命令行来完成。
echo "FROM ruby:3.0.0" > Dockerfile
在这里,我们只是创建Dockerfile并添加一行,指定在哪里找到Ruby容器镜像。我的项目使用Ruby 3.0.0,所以我使用了合适的镜像。如果你用的是不同版本的Ruby,那也没问题。Docker有一个所有支持镜像的列表。
接下来,手动指示Docker创建一个Docker镜像。
docker build -t rails_work .
在这里,你可以用你想要的镜像的任何名字来代替rails_work 。另外,一定要在结尾处加上句号!
如果你想看到镜像已经被创建,你可以用下面的方法列出你系统上的镜像。
docker image list
不过这个图像大部分是空的,它目前不包含我们的应用程序。我们可以通过在Dockerfile中添加以下内容来指示它添加我们应用程序的代码。
ADD . /rails_work
WORKDIR /rails_work
RUN bundle install
这将复制你的应用程序中的文件,并安装应用程序的依赖性。(在这里,你将用你的应用程序的名称替换rails_work )。
在这一点上,你应该重新运行命令来创建镜像。
docker image list
这里有可能出现一个问题,特别是当你在对一个现有的生产应用程序做这件事时。Bundler可能会抱怨说,镜像试图使用的Bundler版本与创建Gemfile.lock 文件的版本不同。如果发生这种情况,你有两个明确的选择。
- 改变镜像所使用的版本。
- 完全删除
Gemfile.lock。**如果你这样做,请确保将你需要的任何版本的Gems钉在特定的版本上,因为锁文件将被完全重新生成。
如果你的bundle安装仍然失败,那么你可能需要在你的Dockerfile中进行一些额外的安装。
RUN apt-get update && apt-get install -y shared-mime-info
如果你仍然遇到问题,你可能选择了错误的Ruby镜像作为基础,所以值得从那里开始调查。
这里是一个设置环境变量的好机会。
ENV RAILS_ENV production
ENV RAILS_SERVE_STATIC_FILES true
接下来,添加一行来暴露端口3000,这是Rails默认运行的地方。
EXPOSE 3000
最后,指示容器在启动时打开一个bash shell。
CMD ["bash"]
总的来说,你的Docker文件应该是这样的(将rails_work的名字替换掉)。
FROM ruby:3.0.0
ADD . /rails_work
WORKDIR /rails_work
RUN bundle install
ENV RAILS_ENV production
ENV RAILS_SERVE_STATIC_FILES true
EXPOSE 3000
CMD ["bash"]
Docker命令详解
对一些最常见的Dockerfile命令有充分的了解肯定会有帮助。
- FROM -- 这定义了要以什么图像为基础。
- RUN -- 这是在容器内执行的命令。
- ENV -- 这定义了环境变量。
- WORKDIR -- 这将改变容器正在使用的目录。
- CMD -> 指定容器启动时要运行的程序。
Docker Compose
根据Docker的文档,"Compose "是他们用于创建(和启动)具有多个Docker容器的应用程序的工具。在YAML中列出了启动应用程序所需的容器的一切。当有人运行docker-compose up ,这些容器就被创建了。Docker-compose让我们声明性地描述我们的容器配置。
在创建Docker Compose文件之前,必须向Docker说明哪些文件应该从被构建的镜像中排除。创建一个名为.dockerignore 的文件。(注意句号!)在这个文件中,粘贴以下内容。
.git
.dockerignore
.env
如果你的Gemfile是由构建过程维护的,那么一定要在上面的忽略中加入Gemfile.lock 。
接下来,创建一个名为docker-compose.yml 的文件。这就是我们要描述我们的容器配置的地方。我们将以这个文件的内容为开端。
version: '3.8'
services:
db:
image: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
volumes:
- postgres:/var/lib/postgresql/data
web:
build: .
command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
volumes:
- .:/Rails-Docker
ports:
- "3000:3000"
depends_on:
- db
volumes:
postgres:
这个文件创建了两个服务:一个叫db ,另一个叫web 。db容器将从一个为postgres准备的预制镜像中构建,你应该把POSTGRES_USER 和POSTGRES_PASSWORD 的相关值替换掉。你应该注意不要把生产机密放在这个文件中--见下面的 "管理机密 "部分以了解更多信息。
Web容器是从我们的Docker文件中构建的,然后在IP地址0.0.0.0的3000端口上启动一个Rails服务器。
最后,我们有一个Postgres卷来为我们保存数据。
管理秘密
构建时的认证对于生产应用来说可能是一个问题。也许你的应用程序从一个私人仓库中寻找Gems,或者你只需要存储数据库凭证。
任何直接存在于Docker文件中的信息都会被永远植入容器镜像,这是一个常见的安全隐患。
如果你使用Rails的凭证管理器,那么给Docker(或任何主机)的访问权就相对简单了。在Docker Compose文件中,你只需提供RAILS_MASTER_KEY 环境变量。对于给定的编译目标,你在environment 头部下指定密钥,如果你还没有创建,你需要创建。上面的docker-compose文件就会变成下面的样子。
version: '3.8'
services:
db:
image: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
volumes:
- postgres:/var/lib/postgresql/data
web:
build: .
command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
volumes:
- .:/Rails-Docker
ports:
- "3000:3000"
depends_on:
- db
environment:
- RAILS_MASTER_KEY=this_would_be_the_key
volumes:
postgres:
现在,这让你处于一个十字路口。你可能想把这个文件提交到源码控制,但你肯定不希望你的主密钥甚至数据库密码被源码控制跟踪,因为这将是另一个危险的安全问题。到目前为止,最好的解决方案是利用dotenv gem,这样你就可以通过代理访问这些证书,将它们存储在一个不被源码控制跟踪的不同文件中。
运行Docker化的应用程序
最后,你可以用以下命令运行docker化的应用程序。
docker compose up
信不信由你,这就是了!Docker-compose让启动容器变得很容易,尤其是在涉及到命令行参数的时候。
如果你想要一个正在运行的容器列表,只需运行以下命令。
docker ps
如果你的Rails容器的名字是web ,你可以用一种相当直接的方式在上面执行命令。例如,如果你想运行一个Rails控制台,你所需要做的就是运行下面的命令。
docker exec -it web rails console
如果你只想在容器内使用bash shell,你可以运行下面的命令。
docker exec -it web bash
还有一些类似这里列出的陷阱
在生产中使用docker化Rails应用程序的一个常见问题是处理日志。它们不应该长期存在于容器系统中。Docker建议将日志简单地重定向到STDOUT。这可以在config/application.rb 中明确配置。
另一个常见的问题是关于邮件的问题。如果你的应用程序使用邮件,你必须明确地定义连接设置。SMTP是一种非常好的交付方式,通常在默认情况下工作得很好,但我们必须小心地设置服务器的位置和其他设置,以匹配我们的容器配置。
如果你有工作者或后台工作,比如sidekiq,那么你必须在它自己的容器中运行它。
总结
容器化生产型Rails应用程序会带来一系列的挑战,这一点你肯定已经看到了。随着你的应用程序的发展,它可能已经积累了许多依赖关系,这使得像这样的迁移具有挑战性。无论是后台工作者、邮件发送者还是秘密,都有既定的模式来处理大多数陷阱。一旦完成了让生产应用与Docker一起工作的初始工作,未来的变化和部署的便利性将使投资物有所值。