Docker新手入门之六:Docker在生产环境中的使用实践
2018-02-20T12:23:31    2022    0    0
#### 在之前的文章中,我们简要介绍的Docker的一些简单实践。 #### 接下来,我们将在本文中进行类似内容的讲解,不过相比而言,使用Docker部署的服务将会更加的复杂。 ## 搭建Jekyll框架的网站 本文中我们将要构建的第一个应用是一个使用Jekyll框架的自定义网站。 该网站会包括以下两个镜像: - 一个镜像安装了Jekyll及其相关的软件包 - 一个镜像安装了Apache服务用于使网站正常运行。 则以后开发过程可以转化如下: 1. 创建Jekyll基础镜像和Apache镜像 2. 利用Jekyll镜像创建一个容器并通过卷挂载网站的源代码。 3. 使用Apache镜像创建一个容器,这个容器利用包含编译后的网站的卷并为其服务。 4. 网站更新时跳转至步骤2重复执行。 ### Jekyll基础镜像 首先,我们需要创建第一个Jekyll基础镜像。 我们需要创建一个新的目录并创建该镜像所需要的Dockerfile文件。 ```bash mkdir jekyll cd jekyll touch Dockerfile ``` 修改`Dockerfile`文件如下: ``` FROM ubuntu:16.04 MAINTAINER nianshi ENV REFRESHED_AT 2018-02-17 RUN apt-get -qq update RUN apt-get -qq install ruby ruby-dev build-essential nodejs libffi-dev RUN gem install --no-rdoc --no-ri jekyll VOLUME /data VOLUME /var/www/html WORKDIR /data ENTRYPOINT [ "jekyll", "build", "--destination=/var/www/html" ] ``` 在该`Dockerfile`文件中,我们首先安装了Ruby和相关的包,此外,我们使用`VOLUME`指令创建了两个卷:`/data/`用于存放网站的源代码,`/var/www/html`用于存放编译后的Jekyll文件。 ### 构建Jekyll基础镜像 创建好`Dockerfile`文件后,我们可以执行如下命令来构建镜像: ``` docker build -t nianshi/jekyll . ``` 命令执行完成后,我们则创建完成了一个jekyll的基础镜像。 ### Apache基础镜像 接下来,我们需要继续创建一个新的文件夹用于构建Apache镜像。 ```bash mkdir apache cd apache touch Dockerfile ``` 修改`Dockerfile`文件如下: ``` FROM ubuntu:16.04 MAINTAINER nianshi RUN apt-get -qq update RUN apt-get -qq install apache2 VOLUME [ "/var/www/html" ] WORKDIR /var/www/html ENV APACHE_RUN_USER www-data ENV APACHE_RUN_GROUP www-data ENV APACHE_LOG_DIR /var/log/apache2 ENV APACHE_PID_FILE /var/run/apache2.pid ENV APACHE_RUN_DIR /var/run/apache2 ENV APACHE_LOCK_DIR /var/lock/apache2 RUN mkdir -p $APACHE_RUN_DIR $APACHE_LOCK_DIR $APACHE_LOG_DIR EXPOSE 80 ENTRYPOINT [ "/usr/sbin/apache2" ] CMD ["-D", "FOREGROUND"] ``` ### 构建Apache镜像 创建好`Dockerfile`文件后,我们可以执行如下命令来构建镜像: ``` docker build -t nianshi/apache . ``` 命令执行完成后,我们则创建完成了一个apache的基础镜像。 ### 启动Jekyll网站 现在,我们已经完成了两个基础镜像的构建了。 下面,我们需要启动容器来构建我们的网站了: 首先需要下载网站所需要的源代码:http://www.missshi.cn/#/adminBook?_k=95xjmw 访问资源下载页面,搜索`Jekyll示例服务依赖代码`下载并解压即可,此处我们假设解压目录是`/home/nianshi/blog_demo`。 此时,我们可以执行如下命令来启动Jekyll容器: ``` docker run -v /home/nianshi/blog_demo:/data/ --name blog_demo nianshi/jekyll ``` 此时,我们启动了一个`blog_demo`的容器,并对卷进行了挂载,其中,本地的`/home/nianshi/blog_demo`目录挂载到的容器中的`/data/`文件夹下。另外容器对源代码编译后生成的代码会存放在`/var/www/html/`卷下。 接下来,我们期望启动一个Apache容器,同时希望Apache容器可以连接到Jekyll容器中`/var/www/html/`卷下获取编译后的文件。 我们可以执行如下命令: ``` docker run -d -P --name apache --volumes-from blog_demo nianshi/apache ``` 在上述命令中,出现了一个新的参数`--volumes-from`,该参数用于连接一个卷,从而可以访问到指定容器中的所有的卷。 Ps:当所有使用某个卷的容器全部都被`rm`命令删除后,该卷会被自动释放。 下面我们来查看一下我们的容器映射到了机器的哪个端口吧: ```bash docker port apache 80 # 0.0.0.0:32782 ``` 接下来,我们访问32782端口,可以看到如下页面: ![title](/static/files/591/5989cee6e519f50ef7000031/26/images/156f96875182c7e6e2ee894c57bec7c4.png) ### 更新Jekyll网站 如果需要更新网站的数据,假设要修改Jekyll网站的博客名字,我们只需要通过编辑宿主机上`james_blog/_config.yml`文件:并将title域改为Nianshi Blog。 然后通过使用docker start命令启动Docker容器即可。 可以看到,Jekyll编译过程第二次被运行,并且往网站已经被更新。这次更新已经写入了对应的卷。现在浏览Jekyll网站,就能看到变化了。 由于共享的卷会自动更新,这一切都不要更新或者重启Apache容器。这个流程非常简单,可以将其扩展到更复杂的部署环境。 ### 备份Jekyll卷 如果担心一不小心删除卷。 由于卷的优点之一就是可以挂载到任意的容器,因此可以轻松备份它们。 现在创建一个新容器,用来备份`/var/www/html`卷。 ``` docker run --rm --volumes-from blog_demo -v $(pwd):/backup ubuntu tar cvf /backup/blog_demo_backup.tar /var/www/html ``` 上述代码描述了一个最简单的备份方式,基于基础的ubuntu镜像,通过执行`tar cvf`命令,将卷`/var/www/html`中的内容压缩为`blog_demo_backup.tar`文件,并存储到机器当前目录中。 ### 服务扩展方式 - 首先,我们可以通过运行多个Apache容器,这些容器同时共享blog_demo中共享的卷,同时在多个Apache容器前使用一个负载均衡器从而构成一个Web集群。 - 进一步构建一个镜像,用于把拉取源代码(如git clone)过程打包入卷中,在将该卷挂载到jekyll容器中,从而可以生成一个快速迁移的通用解决方案。 ## 使用Docker构建一个Java项目 接下来,我们考虑用Docker构建一个Java项目。 具体来说,主要分为两步: 1. 从指定URL中拉取指定的WAR文件并将其保存到卷中。 2. 利用一个含有Tomcat服务器的镜像运行拉取的WAR文件。 ### WAR文件获取器及其构建 首先创建一个新的目录用于准备构建WAR文件获取器镜像: ``` mkdir fetcher cd fetcher touch Dockerfile ``` 接下来,修改`Dockerfile`文件如下: ``` FROM ubuntu:16.04 MAINTAINER nianshi ENV REFRESHED_AT 2018-02-18 RUN apt-get -qq update RUN apt-get -qq install wget VOLUME [ "/var/lib/tomcat7/webapps/" ] WORKDIR /var/lib/tomcat7/webapps/ ENTRYPOINT [ "wget" ] CMD [ "--help" ] ``` 该镜像只完成一件非常简单的事,使用wget从指定的url中拉取文件并保存在`/var/lib/tomcat7/webapps/`卷中。后续我们会将该卷共享到Tomcat服务器中。 下面,我们可以执行如下命令来构建镜像: ``` docker build -t nianshi/fetcher . ``` ### 获取WAR文件 下面,我们以获取示例文件来启动我们刚才构建好的镜像: ```bash docker run -it --name fetcher nianshi/fetcher https://tomcat.apache.org/tomcat-7.0-doc/appdev/sample/sample.war # --2018-02-18 01:55:13-- https://tomcat.apache.org/tomcat-7.0-doc/appdev/sample/sample.war # Resolving tomcat.apache.org (tomcat.apache.org)... 140.211.11.105, 195.154.151.36, 2001:bc8:2142:300:: # Connecting to tomcat.apache.org (tomcat.apache.org)|140.211.11.105|:443... connected. # HTTP request sent, awaiting response... 200 OK # Length: 4606 (4.5K) # Saving to: 'sample.war' # # sample.war # 100%[===================================================================>] 4.50K --.-KB/s in 0s # # 2018-02-18 01:55:15 (217 MB/s) - 'sample.war' saved [4606/4606] ``` 我们可以在Docker容器中切换到`/var/lib/tomcat7/webapps/`目录下,我们可以找到刚刚下载得到的`sample.war`文件。 同时,我们也可以在宿主机上找到该文件: ``` docker inspect -f "{{ .Mounts }}" fetcher # [{642e56f6be0bb520661678cfa61f8c0182d3426a1b44092801bfcc20a154025f /mnt/docker_hub/volumes/642e56f6be0bb520661678cfa61f8c0182d3426a1b44092801bfcc20a154025f/_data /var/lib/tomcat7/webapps local true }] ``` 该命令可以查询fetcher容器的Mounts所在的文件夹。其中,`/mnt/docker_hub/volumes/642e56f6be0bb520661678cfa61f8c0182d3426a1b44092801bfcc20a154025f/_data`就是该卷在我们实际宿主机的位置。 ### Tomcat7服务器构建 下面,我们需要继续构建一个Tomcat7服务器镜像。 ``` mkdir tomcat7 cd tomcat7 touch Dockerfile ``` 修改`Dockerfile`如下: ``` FROM ubuntu:16.04 MAINTAINER nianshi ENV REFRESHED_AT 2018-02-18 RUN apt-get -qq update RUN apt-get -qq install tomcat7 default-jdk ENV CATALINA_HOME /usr/share/tomcat7 ENV CATALINA_BASE /var/lib/tomcat7 ENV CATALINA_PID /var/run/tomcat7.pid ENV CATALINA_SH /usr/share/tomcat7/bin/catalina.sh ENV CATALINA_TMPDIR /tmp/tomcat7-tomcat7-tmp RUN mkdir -p $CATALINA_TMPDIR VOLUME [ "/var/lib/tomcat7/webapps/" ] EXPOSE 8080 ENTRYPOINT [ "/usr/share/tomcat7/bin/catalina.sh", "run" ] ``` 下面,我们来构建一下该镜像: ``` docker build -t nianshi/tomcat7 . ``` ### 运行WAR文件 下面,我们来启动Tomcat容器来运行一下示例应用吧: ``` docker run --name tomcat --volumes-from fetcher -d -P nianshi/tomcat7 ``` 该命令将会创建一个`tomcat`容器,并关联`fetcher`容器中共享的卷。 下面我们来查看一下映射在宿主机的端口 ``` docker port tomcat 8080 # 0.0.0.0:32768 ``` 接下来,我们在浏览器中打开该页面,页面显示如下: ![title](/static/files/591/5989cee6e519f50ef7000031/59/images/85edeaaaf240e2b71c538521c3b2020a.png) ## 多容器应用栈 接下来,我们将分享一个使用Express框架的,带有Redis后端的Node.js的应用。 在本应用中,我们需要构建如下一系列镜像: 1. 一个Node容器,服务于Node应用 2. 一个Redis主容器,用于保存和集群化状态管理 3. 两个Redis备份容器,用于集群化应用状态 4. 一个日志容器,用于收集日志。 ### Node.js镜像 首先,同样是需要创建一个新的文件夹用于构建Node.js镜像。 ``` mkdir nodejs cd nodejs mkdir -p nodeapp touch Dockerfile ``` 修改`Dockerfile`文件如下: ``` FROM ubuntu:16.04 MAINTAINER nianshi ENV REFRESHED_AT 2018-02-18 RUN apt-get -qq update RUN apt-get -qq install nodejs npm RUN ln -s /usr/bin/nodejs /usr/bin/node RUN mkdir -p /var/log/nodeapp ADD nodeapp /opt/nodeapp/ WORKDIR /opt/nodeapp RUN npm install VOLUME [ "/var/log/nodeapp" ] EXPOSE 3000 ENTRYPOINT [ "nodejs", "server.js" ] ``` 在该`Dockerfile`文件中,我们使用`ADD`命令将`nodeapp`文件夹添加到了容器的`/usr/bin/node`目录下。其中`nodeapp`文件夹存储的是配置文件和实际代码。 Ps:代码可以访问http://www.missshi.cn/#/books?_k=xmjul9,搜索`Node.js示例服务依赖代码`,下载并解压至`nodeapp`文件夹即可。 其中,`server.js`文件如下: ```javascript var fs = require('fs'); var express = require('express'), app = express(), redis = require('redis'), RedisStore = require('connect-redis')(express), server = require('http').createServer(app); var logFile = fs.createWriteStream('/var/log/nodeapp/nodeapp.log', {flags: 'a'}); app.configure(function() { app.use(express.logger({stream: logFile})); app.use(express.cookieParser('keyboard-cat')); app.use(express.session({ store: new RedisStore({ host: process.env.REDIS_HOST || 'redis_primary', port: process.env.REDIS_PORT || 6379, db: process.env.REDIS_DB || 0 }), cookie: { expires: false, maxAge: 30 * 24 * 60 * 60 * 1000 } })); }); app.get('/', function(req, res) { res.json({ status: "ok" }); }); app.get('/hello/:name', function(req, res) { res.json({ hello: req.params.name }); }); var port = process.env.HTTP_PORT || 3000; server.listen(port); console.log('Listening on port ' + port); ``` 下面,我们来构建容器 ``` docker build -t nianshi/nodejs . ``` ### Redis基础镜像 下面,我们需要继续构建Redis基础镜像。后续会根据该基础镜像来构建主从镜像。 ``` mkdir redis_base cd redis_base touch Dockerfile ``` 编写`Dockerfile`文件如下: ``` FROM ubuntu:16.04 MAINTAINER nianshi ENV REFRESHED_AT 2018-02-18 RUN apt-get -qq update RUN apt-get install -qq software-properties-common python-software-properties RUN add-apt-repository ppa:chris-lea/redis-server RUN apt-get -qq update RUN apt-get -qq install redis-server redis-tools VOLUME [ "/var/lib/redis", "/var/log/redis" ] EXPOSE 6379 CMD [] ``` 下面我们来构建Redis基础镜像: ``` docker build -t nianshi/redis_base . ``` ### Redis主镜像 接下来,我们基于Redis基础镜像来构建我们的Redis主镜像。 ``` mkdir redis_primary cd redis_primary touch Dockerfile ``` 编辑`Dockerfile`文件如下: ``` FROM nianshi/redis_base MAINTAINER nianshi ENV REFRESHED_AT 2016-06-01 ENTRYPOINT [ "redis-server", "--protected-mode no", "--logfile /var/log/redis/redis-server.log" ] ``` 构建该镜像: ``` docker build -t nianshi/redis_primary . ``` ### Redis从镜像 同样,我们来继续构建构建从镜像: ``` mkdir redis_slave cd redis_slave touch Dockerfile ``` 编辑`Dockerfile`文件如下: ``` FROM nianshi/redis_base MAINTAINER nianshi ENV REFRESHED_AT 2018-02-18 ENTRYPOINT [ "redis-server", "--protected-mode no", "--logfile /var/log/redis/redis-replica.log", "--slaveof redis_primary 6379" ] ``` 构建该镜像: ``` docker build -t nianshi/redis_slave . ``` ### 启动Redis后端集群 现在Redis的主从镜像都已经准备好了,我们来启动Redis后端集群。 首先启动主容器: ``` docker run -d -h redis_primary --name redis_primary nianshi/redis_primary ``` 在该命令中,我们使用了`-h`命令,`-h`命令用于设置容器的主机名,从而保证本地DNS可以正确解析。 下面,我们来查看一下容器的日志吧,由于此处我们将容器日志定向到了指定的日志文件夹,而不是标准输出,因此我们不能通过之前的`docker logs`命令来查看。 而是可以通过如下命令来查看日志: ``` docker run -it --rm --volumes-from redis_primary ubuntu cat /var/log/redis/redis-server.log ``` 其中,我们启动了另外一个容器,该容器连接至Redis容器的卷用于查询日志,其中`--rm`命令表示该容器运行结束后自动删除。 日志观察到我们的Redis服务已经正常启动了,下面我们可以继续创建Redis从服务了。 ``` docker run -d -h redis_slave --name redis_slave --link redis_primary:redis_primary nianshi/redis_slave ``` 其中,`--link`参数用于将`redis_primary`容器以别名`redis_primary`连接到Redis从容器。 下面,我们来查看一下新容器的日志: ``` docker run -it --rm --volumes-from redis_slave ubuntu cat /var/log/redis/redis-replica.log ``` 日志显示我们新启动的容器已经成功连接至Redis主容器。 下面,我们可以再启动第二个Redis从容器来提高服务的可靠性: ``` docker run -d -h redis_slave2 --name redis_slave2 --link redis_primary:redis_primary nianshi/redis_slave ``` ### 启动Node容器 目前,我们的后台Redis集群已经可以正常运行了,下面,我们可以启动Node容器了: ``` docker run -d --name nodeapp -p 4000:3000 --link redis_primary:redis_primary nianshi/nodejs ``` 其中,`-p 4000:3000`表示将容器的3000端口映射宿主机的4000端口。 此时,使用浏览器打开该页面即可: ![title](/static/files/591/5989cee6e519f50ef7000031/18/images/87e7ef4b5ab56264c64d05e6fc967573.png) 输出为 ``` { "status": "ok" } ``` 表示服务目前正常工作,浏览器的会话状态被先记录到Redis的主容器后同步到了两个Redis从服务器上。 ### 捕获应用日志 在生产环境中,我们需要确保可以捕获日志并保存到日志服务器中。 此处,我们以Logstash为例进行讲解,首先需要创建一个Logstash镜像: ``` mkdir logstash cd logstash touch Dockerfile ``` 修改`Dockerfile`如下: ``` FROM ubuntu:16.04 MAINTAINER nianshi ENV REFRESHED_AT 2018-02-18 RUN apt-get -qq update RUN apt-get -qq install wget RUN wget -O - http://packages.elasticsearch.org/GPG-KEY-elasticsearch | apt-key add - RUN echo 'deb http://packages.elasticsearch.org/logstash/1.5/debian stable main' > /etc/apt/sources.list.d/logstash.list RUN apt-get -qq update RUN apt-get -qq install logstash default-jdk ADD logstash.conf /etc/ WORKDIR /opt/logstash ENTRYPOINT [ "bin/logstash" ] CMD [ "--config=/etc/logstash.conf" ] ``` 此时,创建镜像的过程中用到了一个配置文件`logstash.conf`,内容如下: ``` input { file { type => "syslog" path => ["/var/log/nodeapp/nodeapp.log", "/var/log/redis/redis-server.log"] } } output { stdout { codec => rubydebug } } ``` 该配置文件表示logstash会监控两个文件:`/var/log/nodeapp/nodeapp.log`和`/var/log/redis/redis-server.log`,将其中新的内容发送给logstash。 下面,我们来构建logstash镜像: ``` docker build -t nianshi/logstash . ``` 镜像构造完成后,我们可以执行如下命令来启动容器: ``` docker run -d --name logstash --volumes-from redis_primary --volumes-from nodeapp nianshi/logstash ``` 容器启动后,我们可以执行如下命令来查看logstash容器的输出日志: ``` docker logs -f logstash ``` 在之前的配置文件中,我们选择将日志进行标准输出,实际在生产环境中,我们更多的选择是会存储到Elasticsearch里。 ## 管理Docker容器的推荐方式 传统上,我们管理机器的方式通常是通过SSH登入运行环境中来管理服务。 但是对于Docker而言,大部分容器内部仅仅只是运行一个进程,因此不推荐这种方式。 如果其中在Docker容器中执行某些命令,更推荐的方式如下: ``` docker exec [参数] 容器名称 [执行命令] ```

上一篇: Docker新手入门之五:Docker在真实应用中的使用实践

下一篇: Docker新手入门之七:Docker编配工具介绍

0条评论