Docker:如何部署全栈应用程序且不灰显

“我们需要DevOps!”
(任何黑客马拉松比赛结束时最受欢迎的词组)

首先,一些歌词。

如果开发人员是出色的开发人员,并且可以在任何OC下在任何计算机上部署他的想法,那么这是一个加分。 但是,如果他对IDE的了解不多,那可不是一件容易的事-最终,他将获得代码的报酬,而不是部署它的能力。 市场上狭窄的深层专家的价值高于“万事通”的平均技能。 对于像我们这样的“ IDE用户”来说,好人想到了Docker。

Docker的原则如下:“它对我有用-它无处不在。” 在任何地方部署应用程序副本所需的唯一程序是Docker。 如果在计算机上的docker中运行应用程序,则可以确保在其他docker中成功运行。 除了泊坞窗外,不需要安装其他任何东西。 例如,我甚至在虚拟服务器上都没有Java。

码头工人如何工作?


Docker创建一个虚拟机的映像,其中安装了应用程序。 此外,该映像作为一个完全自主的虚拟机展开。 图像的运行副本称为“容器”。 您可以在服务器上运行任意数量的映像,每个映像都是具有自己环境的独立虚拟机。

什么是虚拟机? 这是服务器上装有应用程序的操作系统上的封装位置。 在任何操作系统中,通常都有大量的应用程序在旋转,而在我们的操作系统中,只有一个。

容器部署方案可以表示如下:



对于每个应用程序,我们创建自己的映像,然后分别部署每个容器。 另外,您可以将所有应用程序放在一个映像中,并作为一个容器进行部署。 此外,为了不单独部署每个容器,我们可以使用单独的docker-compose实用程序,该实用程序通过单独的文件配置容器及其之间的关系。 然后,整个应用程序的结构可能如下所示:



由于某些原因,我故意不将数据库提供给常规Docker程序集。 首先,数据库是完全独立于使用该数据库的应用程序的。 它可以远离一个应用程序,也可以是来自控制台的手动请求。 就个人而言,我认为没有理由使数据库依赖于其所在的Docker程序集。 因此,我忍受了。 但是,通常实践一种将数据库放置在单独的映像中并在单独的容器中启动的方法。 其次,我想展示Docker容器如何与容器外部的系统进行交互。

但是,相当歌词,让我们编写代码。 我们将在spring上编写最简单的应用程序并做出反应,它将对前端的调用记录到数据库中,并将所有这些工作通过Docker进行。 我们的应用程序的结构如下所示:



有很多方法可以实现这种结构。 我们正在实施其中之一。 我们将创建两个映像,从中启动两个容器,此外,后端将连接到数据库,该数据库安装在Internet上的特定服务器上(是的,对数据库的此类请求不会很快执行,但是我们并不需要进行优化,但是科学兴趣)。

该帖子将分为几部分:

0.安装Docker。
1.我们编写应用程序。
2.我们收集图像并启动容器。
3.收集映像并在远程服务器上启动容器。
4.解决网络问题。

0.安装Docker


为了安装Docker,您需要转到该站点并按照那里的内容进行操作。 在远程服务器上安装Docker时,请做好以下准备:Docker可能不适用于OpenVZ上的服务器。 如果没有启用iptables,可能还会有问题。 建议使用iptables在KVM上启动服务器。 但是这些是我的建议。 如果一切对您都有效,那么,我将很高兴您没有花很多时间弄清楚为什么它不起作用,以及我该怎么做。

1.我们编写应用程序


让我们用Spring Boot编写一个具有最原始后端的简单应用程序,在ReactJS和MySQL数据库上编写一个非常简单的前端。 该应用程序将具有一个带有单个按钮的“单页”,该按钮将记录在数据库中单击它的时间。

我依靠的事实是,您已经知道如何在启动时编写应用程序,但是如果不知道,则可以克隆完成的项目。 本文末尾的所有链接。

春季启动后端


build.gradle:

plugins { id 'org.springframework.boot' version '2.1.4.RELEASE' id 'java' } apply plugin: 'io.spring.dependency-management' group = 'ru.xpendence' version = '0.0.2' sourceCompatibility = '1.8' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'mysql:mysql-connector-java' testImplementation 'org.springframework.boot:spring-boot-starter-test' } 

日志实体:

 package ru.xpendence.rebounder.entity; import com.fasterxml.jackson.annotation.JsonFormat; import javax.persistence.*; import java.io.Serializable; import java.time.LocalDateTime; import java.util.Objects; /** * Author: Vyacheslav Chernyshov * Date: 14.04.19 * Time: 21:20 * e-mail: 2262288@gmail.com */ @Entity @Table(name = "request_logs") public class Log implements Serializable { private Long id; private LocalDateTime created; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Long getId() { return id; } @Column @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS") public LocalDateTime getCreated() { return created; } @PrePersist public void prePersist() { this.created = LocalDateTime.now(); } //setters, toString, equals, hashcode, constructors 

LogController,它将使用简化的逻辑并立即写入数据库。 我们忽略了服务。

 package ru.xpendence.rebounder.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import ru.xpendence.rebounder.entity.Log; import ru.xpendence.rebounder.repository.LogRepository; import java.util.logging.Logger; /** * Author: Vyacheslav Chernyshov * Date: 14.04.19 * Time: 22:24 * e-mail: 2262288@gmail.com */ @RestController @RequestMapping("/log") public class LogController { private final static Logger LOG = Logger.getLogger(LogController.class.getName()); private final LogRepository repository; @Autowired public LogController(LogRepository repository) { this.repository = repository; } @GetMapping public ResponseEntity<Log> log() { Log log = repository.save(new Log()); LOG.info("saved new log: " + log.toString()); return ResponseEntity.ok(log); } } 

我们所看到的一切都很简单。 通过GET请求,我们写入数据库并返回结果。

我们将单独讨论应用程序设置文件。 有两个。

application.yml:

 spring: profiles: active: remote 

application-remote.yml:

 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://my-remote-server-database:3306/rebounder_database?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC username: admin password: 12345 jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate.dialect: org.hibernate.dialect.MySQL5Dialect server: port: 8099 

如何工作,您可能知道Spring首先扫描application.properties或application.yml文件-它找到了那个文件。 在其中,我们指示一个单一设置-我们将使用哪个配置文件。 通常,在开发过程中,我会积累多个配置文件,使用默认配置文件进行切换非常方便。 接下来,Spring查找具有所需后缀的application.yml并使用它。

我们指定了数据源,JPA设置,更重要的是,指定了后端的外部端口。

ReactJS前端


您还可以在git上的项目中看到前端,或者甚至可以不观看,但可以克隆并运行它。

您可以通过下载项目,转到终端中项目的根文件夹(package.json文件所在的文件夹)并依次执行两个命令来检查前端的各项工作:

 npm install //      ,  maven npm start //   

当然,为此,您需要安装的Node Package Manager(npm),这是我们避免使用Docker的最困难的方法。 如果您仍然启动项目,则将看到以下窗口:



哦,是时候看一下代码了。 我将仅指出涉及后端的部分。

 export default class Api { _apiPath = 'http://localhost:8099'; _logUrl = '/log'; getResource = async () => { const res = await fetch(`${this._apiPath}${this._logUrl}`); if (!res.ok) { throw new Error(`Could not fetch ${this._logUrl}` + `, received ${res.status}`) } return await res.json(); }; }; 

前端工作正常。 我们点击链接,等待答案并将其显示在屏幕上。



值得重点注意以下几点:

  1. 前面板通过端口3000向外界开放。这是React的默认端口。
  2. 背面在端口8099上打开。我们在应用程序设置中进行了设置。
  3. 背面是通过外部Internet敲数据库。

该应用程序已准备就绪。

2.收集图像并启动容器


我们组装的结构如下。 我们将创建两个图像-前端和后端,这两个图像将通过外部端口相互通信。 对于基础,我们将不会创建映像,而是将其单独安装。 为什么这样 我们为什么不为基础创建图像? 我们有两个不断变化的应用程序,它们不会自己存储数据。 数据库本身存储数据,这可能是应用程序运行数月的结果。 而且,该数据库不仅可以由我们的后端应用程序访问,而且可以由许多其他应用程序访问-因为它也是一个数据库,因此我们不会不断地对其进行重组。 再次,这是使用外部API(当然是连接到我们的数据库)的机会。

前端组装


要运行每个应用程序(无论是正面还是背面),您都需要执行一定的操作顺序。 要在React上运行应用程序,我们需要执行以下操作(前提是我们已经有Linux):

  1. 安装NodeJS。
  2. 将应用程序复制到特定文件夹。
  3. 安装所需的软件包(npm install命令)。
  4. 使用npm start命令启动应用程序。

这是我们必须在docker中执行的一系列操作。 为此,在项目的根目录中(与package.json位于同一位置),我们必须将Dockerfile放入以下内容:

 FROM node:alpine WORKDIR /usr/app/front EXPOSE 3000 COPY ./ ./ RUN npm install CMD ["npm", "start"] 

让我们看看每一行的含义。

 FROM node:alpine 

通过这一行,我们向docker明确说,当您启动容器时,您需要做的第一件事是从存储库下载Docker并安装NodeJS,而最轻的一个(docker中流行的框架和库的所有最轻的版本通常称为alpine)。

 WORKDIR /usr/app/front 

在Linux容器中,将创建与其他Linux文件夹相同的标准文件夹-/ opt,/ home,/ etc,/ usr等。 我们设置工作目录-/ usr / app / front。

 EXPOSE 3000 

我们打开端口3000。将通过此端口与容器中运行的应用程序进行进一步的通信。

 COPY ./ ./ 

将源项目的内容复制到容器的工作文件夹中。

 RUN npm install 

安装运行该应用程序所需的所有软件包。

 CMD ["npm", "start"] 

我们使用npm start命令启动应用程序。

容器启动时,将在我们的应用程序中执行此方案。

让我们保持一致。 为此,在终端的项目的根文件夹中(Dockerfile所在的位置),执行以下命令:

 docker build -t rebounder-chain-frontend . 

命令值:

docker是对docker应用程序的调用,您知道的。
建立-从目标材料建立图像。
-t <名称>-将来,该应用程序将通过此处指定的标记可用。 您可以忽略这一点,然后Docker将生成自己的标签,但是将其与其他标签区分开是不可能的。
。 -表示您需要从当前文件夹中收集项目。



因此,程序集应以以下文本结尾:

 Step 7/7 : CMD ["npm", "start"] ---> Running in ee0e8a9066dc Removing intermediate container ee0e8a9066dc ---> b208c4184766 Successfully built b208c4184766 Successfully tagged rebounder-chain-frontend:latest 

如果我们看到最后一步已经完成,并且一切都成功,那么我们将获得一个图像。 我们可以通过运行它来验证这一点:

 docker run -p 8080:3000 rebounder-chain-frontend 

我认为,除了-p 8080:3000条目以外,该命令的含义是众所周知的。
泊坞窗运行篮板链前端-意味着我们正在启动这样的docker映像,我们将其称为篮板链前端。 但是,这样的容器不会有外部出口,需要设置端口。 是下面的团队来设置它。 我们记得我们的React应用程序在端口3000上运行。-p 8080:3000命令告诉docker占用端口3000并将其转发到端口8080(将打开)。 因此,将在端口8080上打开在端口3000上运行的应用程序,并且该应用程序将在该端口上的本地计算机上可用。

 ,       : Mac-mini-Vaceslav:rebounder-chain-frontend xpendence$ docker run -p 8080:3000 rebounder-chain-frontend > rebounder-chain-frontend@0.1.0 start /usr/app/front > react-scripts start Starting the development server... Compiled successfully! You can now view rebounder-chain-frontend in the browser. Local: http://localhost:3000/ On Your Network: http://172.17.0.2:3000/ Note that the development build is not optimized. To create a production build, use npm run build. 

不要让记录打扰您

  Local: http://localhost:3000/ On Your Network: http://172.17.0.2:3000/ 

React这么认为。 它确实在端口3000的容器中可用,但我们将此端口转发到端口8080,并且应用程序从容器中在端口8080上运行。您可以在本地运行该应用程序并进行检查。

因此,我们有一个带有前端应用程序的现成容器,现在让我们收集后端。

构建后端。


用Java启动应用程序的脚本与以前的程序集有很大不同。 它包含以下各项:

  1. 安装JVM。
  2. 我们收集罐子档案。
  3. 我们启动它。

在Dockerfile中,此过程如下所示:

 # back #     JVM FROM openjdk:8-jdk-alpine #  . ,    .  . LABEL maintainer="2262288@gmail.com" #         (  ,  ) VOLUME /tmp #  ,        EXPOSE 8099 # ,       ARG JAR_FILE=build/libs/rebounder-chain-backend-0.0.2.jar #       rebounder-chain-backend.jar ADD ${JAR_FILE} rebounder-chain-backend.jar #    ENTRYPOINT ["java","-jar","/rebounder-chain-backend.jar"] 

在某些点上包括dzharnik在内的图像组装过程与我们前面的过程相似。

组装和启动第二个图像的过程与组装和启动第一个图像基本相同。

 docker build -t rebounder-chain-backend . docker run -p 8099:8099 rebounder-chain-backend 

现在,如果您同时运行了两个容器,并且后端已连接到数据库,则一切正常。 我提醒您,您必须自己从后端注册与数据库的连接,并且该连接必须通过外部网络工作。

3.收集映像并在远程服务器上运行容器


为了使所有内容都能在远程服务器上运行,我们需要在其上安装Docker,然后再运行映像。 我们将以正确的方式将映像提交到Docker云中的帐户,然后在世界任何地方都可以使用它们。 当然,此方法还有很多替代方法,以及后文中描述的所有内容,但让我们再加一点努力,做好我们的工作。 就像安德烈·米罗诺夫(Andrei Mironov)所说,糟糕,我们总是有时间这样做。

在Docker集线器上创建一个帐户


您要做的第一件事就是在Docker集线器上获得一个帐户。 为此,请转到集线器并注册。 这并不困难。

接下来,我们需要转到终端并登录Docker。

 docker login 

系统将要求您输入用户名和密码。 如果一切正常,将在终端中显示登录成功的通知。

将映像提交到Docker Hub


接下来,我们需要标记图像并将其提交到集线器。 这是由团队根据以下方案完成的:

 docker tag   /_: 

因此,我们需要指定映像的名称,登录名/存储库以及将映像提交到集线器的标记。

就我而言,它看起来像这样:



我们可以使用以下命令检查本地存储库中是否存在此映像:

 Mac-mini-Vaceslav:rebounder-chain-backend xpendence$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE xpendence/rebounder-chain-backend 0.0.2 c8f5b99e15a1 About an hour ago 141MB 

我们的形象已准备就绪。 提交:

 docker push xpendence/rebounder-chain-backend:0.0.2 

成功的提交记录应该出现。
对前端执行相同的操作:

 docker tag rebounder-chain-frontend xpendence/rebounder-chain-frontend:0.0.1 docker push xpendence/rebounder-chain-frontend:0.0.1 

现在,如果我们转到hub.docker.com,我们将看到两个锁定的图像。 随处可用。





恭喜啦 我们只需要继续进行最后的工作-在远程服务器上启动映像。

在远程服务器上运行图像


现在,我们只需在终端中完成一行即可在Docker上的任何机器上运行我们的映像(在我们的示例中,我们需要在不同的终端中顺序执行两行-每个映像一个)。

 docker run -p 8099:8099 xpendence/rebounder-chain-backend:0.0.2 docker run -p 8080:3000 xpendence/rebounder-chain-frontend:0.0.1 

但是,此发射只有一减。 当终端关闭时,该过程将结束并且该应用程序将停止工作。 为了避免这种情况,我们可以在“分离”模式下运行该应用程序:

 docker run -d -p 8099:8099 xpendence/rebounder-chain-backend:0.0.2 docker run -d -p 8080:3000 xpendence/rebounder-chain-frontend:0.0.1 

现在,应用程序不会向终端发出日志(可以再次单独配置日志),但是即使终端关闭,它也不会停止工作。

4.解决网络问题


如果您做对了所有事情,您可能会在跟随这篇文章的过程中感到最大的失望-可能没有结果。 例如,一切都对您来说完美无缺,并且可以在本地计算机上工作(例如,在我的Mac上),但是当部署在远程服务器上时,这些容器停止互相看见(例如,在我的Linux上的远程服务器上)。 怎么了 但是问题是这样的,一开始我就暗示了这一点。 如前所述,当容器启动时,Docker将创建一个单独的虚拟机,将Linux卷入其中,然后在该Linux上安装应用程序。 这意味着正在运行的容器的条件本地主机仅限于容器本身,并且应用程序不知道存在其他网络。 但是我们需要:

a)容器互相看见。
b)后端看到了数据库。

有两种解决方案。

1.创建一个内部网络。
2.将容器带到主机级别。

1.在Docker级别,您可以创建网络,并且默认情况下在其中创建三个网络-bridge, nonehost

Bridge是与主机网络隔离的Docker内部网络。 您只能通过以-p命令启动容器时打开的端口来访问容器。 您可以创建任意数量的网络,例如bridge



没有一个是针对特定容器的单独网络。

主机是主机网络。 选择此网络时,可以通过主机完全访问您的容器--p命令在这里完全不起作用,并且如果您将容器部署到该网络,则无需指定外部端口-容器可以通过其内部端口访问。 例如,如果Dockerfile EXPOSE设置为8090,则应用程序将通过此端口可用。



由于我们需要访问服务器数据库,因此我们将使用后一种方法,并在远程服务器网络上布置容器。

这非常简单,我们从容器启动命令中删除端口的提及,并指定主机网络:

 docker run --net=host xpendence/rebounder-chain-frontend:0.0.8 

连接到我指示的底座

 localhost:3306 

必须从外部完全指定正面和背面的连接:

 http://<__:__> 

如果将内部端口转发到外部端口(对于远程服务器通常是这样),则需要指定数据库的内部端口和容器的外部端口。

如果要试验连接,则可以下载并构建一个我专门编写的项目,以测试容器之间的连接。 只需输入所需的地址,按发送,然后在调试模式下,查看返回的内容。

该项目位于此处

结论


有很多方法可以构建和运行Docker映像。 对于那些感兴趣的人,我建议您学习docker-compose。 在这里,我们仅研究了使用docker的一种方法。 当然,起初这种方法似乎并不那么简单。 但这是一个示例-在撰写文章期间,我在远程服务器上拥有传出连接。 在调试过程中,我不得不多次更改数据库连接设置。 完整的组装和部署适合我的4行,输入后,我在远程服务器上看到了结果。 在极限编程模式下,Docker是必不可少的。

按照承诺,我发布了应用程序源:

后端
前端

Source: https://habr.com/ru/post/zh-CN448094/


All Articles