超过 Docker 中的“Hello World”:在 Docker 中构建 Rails + Sidekiq Web 应用程序
这是 Docker _More 不仅仅是“Hello World”系列的第一篇文章。该系列将帮助您准备好您的应用程序:从在本地设置到在 AWS 中将其部署为生产级工作负载。 在 Docker 中构建 Rails + Sidekiq Web 应用程序 - 我们在这里 在 Amazon ECS 中部署 Rails *概念 *将图像推送到 ECR *创建RDS数据库、任务定义和负载均衡器 *创建ECS集
这是 Docker _More 不仅仅是“Hello World”系列的第一篇文章。该系列将帮助您准备好您的应用程序:从在本地设置到在 AWS 中将其部署为生产级工作负载。
-
在 Docker 中构建 Rails + Sidekiq Web 应用程序 - 我们在这里
-
在 Amazon ECS 中部署 Rails
*概念
*将图像推送到 ECR
*创建RDS数据库、任务定义和负载均衡器
*创建ECS集群并连接在一起
*Tsonfigure Zidekia
- 使用 AWS CodeDeploy 自动部署
*深入了解 AWS CodeBuild
-
高级 ECS 概念:
-
服务发现和 Auto Scaling - 即将推出
-
Fargate - 即将推出
不乏关于如何在 Docker 中显示“Hello World”的网络教程。问题在于,从让您的应用程序在 Docker 中说“Hello World”到在 AWS 中运行生产级应用程序的路径通常并不那么简单。即使您的应用程序不是 Rails / Ruby,阅读本系列文章也会帮助您了解在您决定在生产中使用 Docker 后面临的挑战。
我已经在生产环境中使用了 7 个月的 Docker,并且在此之前已经研究了 4 个月。在简单的设置中,做 Docker 非常简单。然而,在生产中使用它会增加复杂性,因为需要考虑很多其他事情。这篇文章将帮助您管理这种复杂性。
对于这篇文章,我们将:
-
了解基本的 Docker 概念以及使用 Docker 的原因。
-
将 Rails 应用程序连接到 PostgreSQL 服务器。
-
运行 Sidekiq 服务器以从 Redis 数据库获取作业。
-
通过在 Redis 中添加作业将简单任务提供给 Sidekiq 服务器。
-
使用 Docker compose 运行 Rails 应用程序
这是一个基础职位。我们将在此处构建的 Rails 应用程序将是我们将在以后的文章中使用的示例应用程序。如果您刚开始使用 Docker,我建议您阅读这篇文章。
如果您之前有使用 Docker 的经验,我建议您阅读本系列的下一篇文章。本教程的源代码可在GitHub上获得,版本为v0.0.1。
0 |概念
在本节中,我将简要介绍 Docker 背后的基本概念。对于 Docker 更深入的概念理解,我推荐Chris Noring 的介绍。
从本质上讲,Docker 允许我们在主机操作系统(如 Ubuntu、Mac 或 Windows)之上生成容器。我们可以在单个主机操作系统中运行多个容器。
Container
- 这是在 Linux 内核之上运行的进程。它被分配了自己的资源:CPU、内存和文件系统。通过拥有自己的资源,容器与在您的实例中运行的其他容器隔离开来。当一个容器由于某种原因失败时,它不会影响在同一主机操作系统中运行的其他容器/进程。
Image
- 这是一个包含执行代码所需的所有内容的文件:依赖项、二进制文件、源代码等。它们是通过在名为 Dockerfile 的类似脚本的文件中执行命令来构建的。图像用于生成多个相同的容器。容器是这些图像的“实例化”——并且是为您的客户流量提供服务的容器。
Dockerfile
- 这是一个包含构建映像所需指令的文件。您在此处输入的命令应指定如何将应用程序所需的所有内容放入映像中。
如果这一切看起来都不清楚,那么这篇文章中的实践将在实践中说明这些概念。
1 |为什么选择 Docker?
更低的花费
传统上,您配置一个实例并且只有一个应用程序服务器在那里运行。如果您需要扩展您的应用程序,您可以添加更多实例,并在每个实例上运行一个应用程序服务器。您通常不会通过在一个实例中添加更多应用程序服务器来进行扩展——因为它很难管理。但这也会导致您的大多数实例都有未使用的容量。
在 Docker 中,您可以在一个实例中运行多个容器。由于您可以将实例的资源分配给容器,因此只要实例上有足够的资源,您就可以在实例中添加任意数量的容器。例如,如果您有一个内存为 8GB 的实例,而您的容器每个只需要 1GB,那么您可以在该单个实例中拥有多达 8 个容器。
生产环境中的另一个著名问题是内存膨胀。当您的应用程序消耗的内存超出预期时,就会发生内存膨胀。常见的解决方法是增加实例的大小,这当然会增加成本。
在 Ruby 中,内存膨胀问题在诸如Sidekiq等大量多线程应用程序中是臭名昭著的。在 Docker 中,您可以为容器设置硬内存分配。如果应用程序消耗的内存超过硬分配(比如 1GB),Docker 会终止容器并启动一个新容器。这可以防止内存膨胀并帮助您更有效地使用资源。
这样,您就可以充分利用实例的资源。您不必像传统方式那样预置尽可能多的实例。
标准化
您的应用在本地运行的环境将与生产环境完全相同。不再有“它在我的机器上有效,但在产品中无效”。
新开发人员的入职也变得容易:您所要做的就是让他们安装 Docker 并让他们在本地机器上运行您的 Docker 映像。没有更多关于如何在他们的机器上安装您的应用程序及其所有依赖项的 7 页文档。
更快的部署
Docker 之前的部署如下所示:AWS 检测到您的流量激增,它预置一个新实例,安装所有依赖项,测试您的实例是否显示流量,然后新实例接收流量。该过程可能需要几分钟,因为它每次都会加载整个操作系统。
Docker 中的部署要快得多。由于容器只是进程,因此启动它是加载整个操作系统的轻量级。
其他
还是不服气?您可以在此处阅读其他好处。
2 |安装
在本教程中,我使用了 Rails 5.2.3 和 Ruby 2.5.1。您可以从此教程安装 Ruby 和 Rails。确保在下拉列表中选择 Ruby 2.5.x 和 Rails 5.2.x,然后选择您要使用的操作系统。
我们还将使用 Docker。如果您像我一样是 Mac 用户,请使用此教程。对于 Windows 用户,您可以使用这个。
3 |创建 Rails 应用程序
(3.0) 转到您要创建 Rails 应用程序的文件夹并执行:
rails new rails_docker_app
cd rails_docker_app
git init && git add .
git commit -m "Initialize Rails project"
然后,让我们创建在主页上显示消息所需的路由、控制器和视图。
(3.1) 打开config/routes.rb
并路由到主页。我们将在接下来的步骤中创建主页。
root to: "home#index"
(3.2) 在app/controllers
文件夹中创建文件home_controller.rb
。在变量@message
中放置一条基本消息。由于变量名称前面带有 @ 符号,因此可以从视图中访问它。
class HomeController < ApplicationController
def index
@message = "static page"
end
end
(3.3) 创建app/views/home
文件夹。在该文件夹中,创建一个名为index.html.erb
的文件。请注意,我们引用了在上一步中创建的@message
变量。这应该显示消息“静态页面”。
Home Page:
<%= @message %>
(3.4) 现在,启动 rails 服务器,您应该会看到我们刚刚创建的简单主页。
rails server -p 3000
您应该会看到一个简单的页面,如下所示:
[](https://res.cloudinary.com/practicaldev/image/fetch/s--V-lwcRaB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3。 amazonaws.com/i/b2rx0qwh8dtpj3fv7snk.png)
(3.5) 当你满意时,承诺你的进步
git add .
git commit -m "Add basic routes, views, controller"
4 |为 Rails 设置 Docker
(4.1) 使用touch Dockerfile
命令在应用根目录下创建 Dockerfile。
# gets the docker image of ruby 2.5 and lets us build on top of that
FROM ruby:2.5.1-slim
# install rails dependencies
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs libsqlite3-dev
# create a folder /myapp in the docker container and go into that folder
RUN mkdir /myapp
WORKDIR /myapp
# Copy the Gemfile and Gemfile.lock from app root directory into the /myapp/ folder in the docker container
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
# Run bundle install to install gems inside the gemfile
RUN bundle install
# Copy the whole app
COPY . /myapp
本质上,Dockerfile 只是一堆人类可读的 shell 命令放在一个文件中。第一行从Docker Hub获取一个名为“ruby:2.5.1-slim”的 Docker 镜像。此图像称为_base image_。它有一个名为“slim”的轻量级操作系统,预装了 ruby 2.5.1。我们将使用下一个命令在此基础映像上进行构建。
下一个命令安装 Rails 的依赖项。
图像内部有自己的“文件夹结构”。使用接下来的命令,我们创建/myapp
文件夹并将其设为“默认”文件夹。
我们复制Gemfile
和Gemfile.lock
,然后运行bundle install
。这会在映像中安装所有必需的 gem。然后我们继续将整个应用程序复制到图像中。
(4.2) 现在我们了解了它的作用,让我们执行docker build .
让 Docker 执行指令并为我们创建 Docker 映像。
你应该看到 Docker 在你的 Dockerfile 中运行命令:
[](https://res.cloudinary.com/practicaldev/image/fetch/s--b2m3qVMp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws。 com/i/bluw9ftlbea0dxnfy2xq.png)
(4.3) 执行完成后,运行docker images
即可查看刚刚构建的 docker 镜像:
[](https://res.cloudinary.com/practicaldev/image/fetch/s--3iIiFczo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws。 com/i/5i4geje7qf9m6o5wwjab.png)
您可以看到它是一个非常大的文件(~641MB)。那是因为它_字面上_拥有运行应用程序所需的一切。
(4.4) 从docker images
中,查找新创建的图像的 image_id。我的看起来像这样:“2f1a0fabe8c6”。让我们标记我们的图像,以便它有一个我们可以轻松记住的漂亮名称。
# for me
docker tag 2f1a0fabe8c6 rails-docker-app:v0.0.1
# for you: replace --chash-- with your image_id
docker tag --chash-- rails-docker-app:v0.0.1
再次运行docker images
,您会看到具有正确名称的图像。
[](https://res.cloudinary.com/practicaldev/image/fetch/s--e4nX2N8v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws。 com/i/h28r6jaejehk6h6g81y4.png)
(4.5) 现在我们有了一个 Docker 镜像,让我们运行它。
docker run -it -p 3000:3000 rails-docker-app:v0.0.1 bundle exec rails server -b 0.0.0.0 -p 3000
-it
标志允许您拥有一个终端,您可以在其中查看来自 Rails 服务器的日志。-p 3000:3000
标志将容器的端口 3000 暴露给本地计算机的端口 3000。然后,我指定了我们之前刚刚标记的 Docker 映像和运行 Rails 服务器的命令。
你应该再次看到这个简单的页面,这次是在 Docker 之上运行:
[](https://res.cloudinary.com/practicaldev/image/fetch/s--MX5X0RaT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws。 com/i/l9f4w5p055jzjr3ec29h.png)
(4.6) 如果您对输出感到满意,请提交您的进度。
git add .
git commit -m "Add Dockerfile and run via docker run"
5 |使用 docker-compose 轻松运行 Docker
现在,我们在 Docker 🎉 🎉 🎉 之上有了一个正常运行的 Ruby on Rails 应用程序。
但是,通常的生产工作负载并非如此简单。我们通常会与 Rails 应用程序一起运行其他服务:数据库、Redis、Sidekiq 等。因此,本地设置通常会有 5 个不同的终端窗口和 5 个单独的 docker run 命令。现在有很多事情要跟踪。 Docker 想到了这一点,并引入了 docker-compose。
服务 docker-compose 允许您在一个 YAML 文件中整齐地声明所有这些服务。它还使您能够在一个窗口中启动所有这些服务。
让我们深入了解:
(5.1) 在应用的根目录下创建一个 docker 文件夹,并在其中创建一个 development 文件夹:mkdir docker && mkdir docker/development
。然后,创建一个名为 docker-compose.yml 的文件并输入以下内容:
version: '3'
services:
db:
image: postgres
volumes:
- ../../tmp/db:/var/lib/postgresql/data
web:
build: ../../
command: bundle exec rails s -p 3000 -b '0.0.0.0'
volumes:
- ../../:/myapp
ports:
- "3000:3000"
depends_on:
- db
这个 docker-compose 文件夹定义了 2 个服务:db 和 web。 “db”服务是一个 PostgreSQL 容器,它将作为我们的数据库服务。 “web”服务是我们的 Rails 应用程序的应用服务器。
“web”服务中的内容是我们通常放在docker run
命令中的内容 - 但这样更容易阅读! 📖
(5.2) 现在,我们使用docker-compose build
命令来构建运行此设置所需的 docker 映像。然后,我们使用docker-compose up
来同时运行这些服务。
cd docker/development
docker-compose build
docker-compose up
您将看到来自这两个服务的日志:
[](https://res.cloudinary.com/practicaldev/image/fetch/s--1jJahBXZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws。 com/i/jsds03lb7m8s6fvoen20.png)
(5.3) 如果您对输出感到满意,请提交您的进度。
git add .
git commit -m "Add docker-compose.yml for easier docker runs"
6 |更进一步:集成 PostgreSQL
现在,大多数网络教程都停留在第 5 节,然后就结束了。这篇文章旨在尽可能地帮助您满足生产需求。连接 PostgreSQL 数据库和通过 Redis 集成 Sidekiq 是两个常见的生产任务,在博客文章中没有给出太多的介绍。
(6.1) 是时候创建我们的模型了。运行以下 2 个命令以生成 Author 和 Post 的模型和迁移。
rails generate model Author name:string kind:string
rails generate model Post title:string body:text author:references likes_count:integer
(6.2) 打开您的 Gemfile 并将gem 'sqlite3'
替换为下面的行。我们将使用 PostgreSQL 作为我们的数据库,并删除对 SQLite3 的引用。
gem 'pg', '>= 0.18', '< 2.0'
然后执行bundle install
。
(6.3) 为了能够连接到我们的数据库,请将config/database.yml
的内容替换为以下内容(如果该文件不存在,请自行创建)。
development:
adapter: postgresql
encoding: unicode
host: db
username: postgres
password:
pool: 5
database: sample_app_docker_development
然后,将种子添加到db/seeds.rb
文件中,这样我们就可以在数据库中拥有示例数据。
author = Author.create(name: "Raphael Jambalos", kind: "Programmer")
post = Post.create(title: "Redis", body: "This is a in-memory database often used for caching.", author_id: author.id)
post = Post.create(title: "PostgreSQL", body: "This is a transactional database used for transactions", author_id: author.id)
post = Post.create(title: "DynamoDB", body: "This is a NoSQL database used for concurrent workloads.", author_id: author.id)
(6.4) 我们希望将示例数据显示到视图中。为此,让我们将app/controllers/home_controller.rb
文件的内容更改为:
class HomeController < ApplicationController
def index
@message = "Dynamic"
@posts = Post.all
end
end
然后,让我们更改app/views/home/index.html.erb
的内容,以便我们可以将帖子整齐地呈现到视图中。
Home Page:
<%= @message %>
<% @posts.each do |post| %>
<h1> <%= post.title %> </h1>
<h2> <%= post.author.name %> </h2>
<p> <%= post.body %>
<% end %>
(6.5) 现在,让我们再次运行docker-compose
。
cd docker/development/
docker-compose build
docker-compose up
在单独的窗口中,依次运行这些命令
docker-compose run web rake db:create
docker-compose run web rake db:migrate
docker-compose run web rake db:seed
(6.6) 如果您对自己的进度感到满意,请提交:
git add .
git commit -m "Add models, migration, seeds; Modify views to show seeded data."
7 |更进一步:整合 Sidekiq
(7.1) 将gem 'sidekiq'
添加到 Gemfile 并运行bundle
。
Sidekiq 用于后台处理。想象一个包含表单的网站。填写表格中所需的所有信息后,按提交。浏览器将您写入的所有信息发送到 Web 服务器。默认情况下,Web 服务器有 60 秒的时间来处理请求;否则,会导致请求超时。这种类型的处理是前台处理。
现在,我们不想让我们的客户在处理信息时等待(更不用说向他们显示超时页面了)。因此,我们将处理信息的工作交给了后台处理器(“Sidekiq”)。一旦我们将作业分配给 Sidekiq,客户将看到一个带有“请稍候”文本的页面。 Sidekiq 执行请求,几分钟后,客户可以在刷新时看到结果。
(7.2) 运行rails g sidekiq:worker IncrementCount
创建工作文件。我们转到 Sidekiq 刚刚创建的文件app/workers/increment_count_worker.rb
并将其内容替换为下面的代码段。
本质上,我们获取一个帖子并将其 like_count 属性加 1。
class IncrementCountWorker
include Sidekiq::Worker
def perform(post_id)
post = Post.find(post_id)
post.likes_count ||= 0
post.likes_count += 1
post.save
end
end
(7.3) 默认情况下,Rails 不会读取app/workers/
文件夹中的文件。您必须转到config/application.rb
并将此行插入class Application
块内。
config.autoload_paths += %W(#{config.root}/app/workers)
(7.4) 要显示每个帖子的点赞数,并为每个帖子添加一个点赞按钮,让我们更改app/views/home/index.html.erb
的内容
Home Page:
<%= @message %>
<% @posts.each do |post| %>
<h1> <%= post.title %> </h1>
<h2> <%= post.author.name %> </h2>
<p> <%= post.body %>
<br>
<p>
<%= link_to "Like", increment_async_path(post_id: post.id), method: :post %>
Likes:
<%= post.likes_count %>
</p>
<% end %>
(7.5) 然后,我们在config/routes.rb
中添加一个路由,在app/controllers/home_controller.rb
中添加一个控制器方法来适应这个请求。
post "/increment_async", to: "home#increment_async"
控制器方法调用我们的 Sidekiq 服务并调用工作程序“IncrementCountWorker”。
class HomeController < ApplicationController
def index
@message = "Dynamic"
@posts = Post.all
end
def increment_async
::IncrementCountWorker.perform_async(params[:post_id])
redirect_to root_path
end
end
(7.6) 我们的新设置需要 Sidekiq 服务和 Redis 服务。
Redis 是一个内存数据库。每次客户按下我们在 7.4 中制作的按钮时,都会在 Redis 数据库中添加一个条目。 Sidekiq 服务轮询 Redis 服务以获取其数据库中的新条目。如果有,它会执行它。
version: '3'
services:
db:
image: postgres
volumes:
- ../../tmp/db:/var/lib/postgresql/data
web:
build: ../../
command: bundle exec rails s -p 3000 -b '0.0.0.0'
volumes:
- ../../:/myapp
environment:
RAILS_ENV: "development"
REDIS_URL: "redis://redis:6379/12"
ports:
- "3000:3000"
depends_on:
- db
redis:
image: redis
volumes:
- ../../tmp/db:/var/lib/redis/data
sidekiq:
build: ../../
command: 'bundle exec sidekiq'
volumes:
- ../../:/myapp
environment:
RAILS_ENV: "development"
REDIS_URL: "redis://redis:6379/12"
depends_on:
- redis
(7.7) 现在,让我们再次运行 docker-compose。
cd docker/development/
docker-compose build
docker-compose up
在浏览器上返回localhost:3000
,您应该能够看到:
[](https://res.cloudinary.com/practicaldev/image/fetch/s--zOL0j9TZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws。 com/i/hkgq8ie3y9meh1kuyldh.png)
按下链接按钮,您将能够增加每个帖子的点赞数。当 Rails 收到请求时,它会在 Redis 上放置一个任务,Sidekiq 获取它,将“1”添加到指定的帖子并保存它。
8 |下一步是什么?
现在,您已经在 Docker 上运行了一个成熟的 Rails 5 应用程序。这并不多,但拥有 Sidekiq 和 PostgreSQL 是您在其上构建的良好基础。您甚至可以使用此模板将现有 Rails 应用程序迁移到 Docker。
开发完 Rails 应用程序后,下一步是使用云提供商部署它,以便您的客户可以看到您的应用程序。在下一篇文章中,我们将把这个项目部署到AWS Elastic Container Service。敬请关注!
致谢
特别感谢我的编辑艾伦帮助这篇文章变得更加连贯。
致 Patrick Brinksma 的封面照片。
我很高兴收到您对这篇文章的评论/反馈。只需在下方发表评论,或给我留言!
更多推荐
所有评论(0)