docker-compose.yml文件的编写
Table of Contents
去年的时候写过一篇用Docker打包Tornado项目的文章,本来是打算很快补上后续文章的,不过那个时候学其他东西就忘记了.前一段时间家里的网络失常,笔记本的系统崩坏以及服务器的各种问题,为了以后可以快速恢复环境,因此要了解 docker 的更多用法,这文章就是给自己备忘的.
由于我本人也没有经常用 Docker,所以难免会有些遗漏,我自己也尽量避开这些点,给出相关连接.本文主要是讲述如何编写 docker-compose.yml 文件,从而使用 docker-compose 和 docker stack deploy 命令部署应用.
Docker Compose 和 Docker Swarm
官方文档介绍:
- https://docs.docker.com/compose/overview/
- https://docs.docker.com/engine/swarm/key-concepts/
- https://docs.docker.com/engine/swarm/stack-deploy/
Docker compose (docker-compose cli) 是一个用来定义以及运行多容器的 Docker 应用.该工具需要你编写 docker-compose.yml 文件,然后一条 docker-compose up 命令就可以启动了.
Docker swarm (docker swarm and docker stack cli) 是一个管理集群服务的工具,在 Docker 的世界里面,一个集群叫做 swarm,关于集群的知识就不多说了,该工具同样需要编写 docker-compose.yml,然后几条简单的命令就可以管理集群.部署到 swarm 需要先 docker swarm init 启动 swarm,然后再 docker stack deploy -c /path/to/docker-compose.yml 进行部署.
除了都要编写 docker-compose.yml 文件外,它们都适合部署多服务的应用,差别在于前者只适合用于单机,后者是集群.
现在清楚 Docker Compose, Docker Swarm 和 Docker Stack Deploy 三者的概念以及关系后,是时候讲一下 docker-compose.yml 文件的编写了.
docker-compose.yml
不论你是用 docker-compose 还是 docker swarm,你都要编写 docker-compose.yml 文件,值得注意的是 docker 的更新是很快的,不同版本的 docker-compose.yml 的能用指令是不一样的.这里是其中一个版本的reference,你可以在这里找到其他版本的参考文档,具体就不说了.
docker-compose.yml 有很多个指令,全部讲完又不太现实,所以我就抛砖引玉,刚好 Nginx 的 docker 官方参考只是讲了如何使用 Dockerfile 和命令行的使用,那么正好我就用它做为例子,补充一个 docker-compose.yml 的例子.
首先你要准备一个静态站点的目录以及它的 Nginx 配置文件和一个最新的 Nginx 镜像,其中静态站点目录的内容类似(请结合自己的实际情况来操作,这里为了演示就一切从简)如下:
site/ |- index.html |- css/ |- style/ |- js/
我相信大部份刚接触 docker 的人在过完 docker 的入门教程后都会想到在 Dockerfile 文件里面使用 COPY 命令来把静态目录复制到容器里面,然后 build 出新的镜像.不是不行,但是如果你的站点频繁更新,那么就需要频繁 build 镜像,因为你的站点是和你的镜像耦合在一起了.
很明显,这样会十分繁琐,也不符合 docker 用于打包环境的初衷.我们部署需要满足一个要求:可以实时更新静态文件,不用每次更新一次文件就要重新 build 一次服务或者镜像并且重新运行容器.
由于 docker-compose.yml 是使用 YAML 做为语言的,所以编写配置文件之前请确保自己 了解 YAML 是什么以及它的基本语法,不了解的话花几分钟了解一下.
根据 Nginx 的官方参考可以了解到 volumes 指令可以满足这一个要求,所以 docker-compose.yml 的配置如下:
version: "3.2"
services:
blog:
image: nginx:latest
volumes:
- ./site:/site
- ./conf.d:/etc/nginx/conf.d
ports:
- "80:80"
这里要求目录 conf.d 和 docker-compose.yml 与 site 是目录同级,其中 conf.d 目录是 Nginx 的配置目录,内容如下:
conf.d/ |- site.conf
site.conf 的配置如下,
server {
listen 80;
listen [::]:80;
root /site;
index index.html;
}
关于站点结构和 Nginx 的配置请自己到 Nginx 的使用文档进行阅读,不在本文的范围内.
到这里为止就可以开始选择你想要的方式(docker-compose 或者 docker swarm)进行部署了.
接下来针对 docker-compose.yml 的几个指令进行说明,
version,声明docker-compose.yml文件的版本,请一定要正确选择自己当前使用的版本,不同版本能够使用的命令是不一样的.关于版本的区别请看这里.services,定义多个服务,上面例子中的blog就是一个服务,如果项目复杂了还可以多定义一些服务,并且服务与服务之间是可以相互访问的,并且不要求服务本身就要与别的服务通信.更多具体用法请阅读Networking in Compose.image,指定该服务所使用的镜像,如果现成镜像不满足自己的需求,可以自己使用build命令指定Dockerfile来建立镜像.volumes,挂载文件,比如上述例子中,把宿主机的site挂载到容器的/site位置,你可以通过docker cp命令来把容器中的/site复制到宿主机上进行验证.当然,这个命令有更加复杂的写法,用于更复杂的情况,具体自行读文档.ports,有两种设置方法,1. 端口映射,格式为Host:Container; 2. 只指定容器端口,主机端口随机,如果这么分配的话就要通过docker ps命令来找主机端口了.
追加例子: MYSQL + Tornado
写于 2019/3/17
因为最近有好几个朋友问我一些关于 Docker 的一些使用问题,每次和这个说完另外一个就问起,所以我决定追加一篇例子来做为日后回答.
主要是关于如何组合两个或者两个以上的服务,也算是为了自己之前的偷懒而负责了,这里我只负责演示两个服务,最后会留下一个思考(都会再说).
项目内容
这个这是一个很简单的 API 服务,使用 Python 做为后端语言,实际上什么后端语言都无所为,根据你自己的实际情况来就好.
换句话说,这里的 Python 代码没有必要看懂,要求看懂的我会特意说明,该项目主要使用了 MYSQL 数据库, Tornado 框架以及一个加密算法库.
项目结构
server/ |- sql/ | |- init.sql |- data/ |- src/ | |- app.py | |- requirements.txt | |- Dockerfile |- docker-compose-yml
其中, src 就是后端程序的整个源代码,和我刚刚说的一样你可以用任何语言开发的后端程序,同样,源代码也可以直接类似的放这里面.
app.py 是程序代码的本身, requirements.txt 是该程序所需要的一些依赖记录,部署的时候会先安装好依赖再运行程序的.
其中 Dockerfile 是用来构建镜像的,不过值得一提的是我并不打算把源代码也 build 到镜像里面,我只会建立一个已经安装好依赖的环境镜像.
最后启动的时候挂载好代码再执行(注意,有些人不了解直译型和编译型语言的工作方式的区别,我这里简单提一下, Python 主要是直译运行,也就是说一个程序读取代码然后直接执行,
而 Java 这种主要是编译的,也就是把源代码翻译成另外一种语言,所以每次 Java 程序员都是写完代码需要 build 以下然后把生成的东西打包好发布,而 Python 是直接写完再整理一下文件目录就可以发布了).
这里我要展示一下 app.py 的源代码,
#! /usr/bin/python3 # -*- coding: utf-8 -*- import os import tornado.web import tornado.ioloop import tornado.httpserver import tornado.options from tornado.options import options, define import pymysql MYSQL_HOST = os.environ.get('MYSQL_HOST') define("port", default=8000, type=int, help="run server on the given port.") database = pymysql.connect( host=MYSQL_HOST, database='MUSICDB', port=3306, user='saltborn', password='saltborn', charset='utf-8') class Application(tornado.web.Application): def __init__(self): handlers = [ (r"/music/(?P<id>\d+)/?", MusicSrcHandler, dict(database=database)) ] settings = dict(debug=True) super().__init__(handlers, **settings) class MusicSrcHandler(tornado.web.RequestHandler): def initialize(self, database): self.database = database def get(self, id): # return music src self.set_header("Content-Type", "application/json") with self.database.cursor() as cursor: sql = "SELECT `msg` FROM MUSIC WHERE `id` = %s" cursor.execute(sql, (id,)) result = cursor.fetchone() # do something with result, I am not doing here self.write({"msg": id}) def main(): tornado.options.parse_command_line() app = Application() app.listen(options.port) tornado.ioloop.IOLoop.current().start() if __name__ == '__main__': main()
这里我们程序的 pymysql.connect 中的 host 参数表示 MYSQL 数据库的主机地址,我们这里 不需要写 MYSQL 数据库的地址,只需要写 MYSQL 服务名字就可以了.
我这里是通过环境变量 MYSQL_HOST 来获取服务名,因为日后服务名字可能会变,这样就可以防万变了,当然你也可以直接写死.这也是项目代码中唯二值得注意的地方了.
docker-compose.yml 配置
version: "3"
services:
mysql:
container_name: mysql
image: mysql
restart: on-failure
environment:
- MYSQL_USER=saltborn
- MYSQL_ROOT_PASSWORD=saltborn
- MYSQL_DATABASE=MUSICDB
ports:
- "3306:3306"
volumes:
- "./data:/var/lib/mysql"
- "./sql:/docker-entrypoint-initdb.d"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u","$MYSQL_USER", "-p$MYSQL_ROOT_PASSWORD"]
interval: 30s
timeout: 10s
retries: 5
music.api:
container_name: music_api
restart: on-failure
# build: ./src
image: saltborn/music
environment:
- MYSQL_HOST=mysql
depends_on:
- mysql
# links:
# - mysql
ports:
- "8000:8000"
volumes:
- "./src:/app"
command: sh -c "python3 /app/app.py"
这里如你所见定义了两个服务,分别是 mysql 和 music.api, 其中 music.api 需要连接 mysql 这个服务.
这里你会发现一些新选项,不用害怕我会尽量讲的简单点,
container_name: 运行时容器的名字;restart: 在什么时候重启容器,on-failure表示失败的时候重启;build: 建立镜像的目录/文件,服务就是基于这个新建立成的镜像生成容器,个人一般都是手动建立好镜像然后通过image选项来指定镜像,否则每次启动这些服务都会很耗时;environment: 设定环境变量,比如上面提到的MYSQL_HOST就是在这里设置的;depends_on: 告诉dockermusic.api服务基于mysql服务,还有一个作用就是确保先让mysql服务在music.api服务之前启动,但是这里有一些小问题,下面再说;links: 告诉dockermusic.api服务需要连接mysql服务,实际上该选项和depends_on比较相识,该选项可以不使用,直接用depends_on即可;command: 服务的入口,相当于Dockerfile的ENTRYPOINT,也就是启动服务时候执行的命令;healthcheck: 由于music.api服务是基于mysql服务的,所以我们需要检测mysql服务是否运行(防止mysql因为错误而无限重启);
关于如何确保 mysql 服务在 music.api 服务启动之前就绪,我在网上(官网)看到几种说法, wait-for-it 和 wait-for 等脚本,也有人说 healthcheck,但是我看了文档并没觉得这选项能干嘛(也有可能根据它的检测结果来决定是否启动 music.api 服务?).
上面的几种解决方法我都没有试过,但是很明显我目前这个配置已经可以确保 music.api 服务能够正常启动,为什么?因为 restart: on-failure,这会在 music.api 服务发生错误的时候重启,这样 music.api 会一直重启直到 mysql 服务就绪为止,当然这不是什么解决方法,但是对于简单的项目而言已经足够用了.
我个人还是推荐尝试上面的提到的 wait-for-it 和 wait-for.
关于 MYSQL Docker 的额外补充
也有一两个朋友不大了解如何使用 MYSQL 的镜像的,其实很简单(可能官方文档太多了大部份人都懒得看),这里我大概说一下,重点在于 mysql 服务的 volumes 选项那里,
你会发现主机上的 sql 挂载到容器的 /docker-entrypoint-initdb.d 目录上,这个容器的目录是 mysql 服务初次启动时候查找 sql 文件的位置,也就是说如果你想新建用户,数据库和表等等的东西都可以写个脚本挂载到这里.
比如例子中的 init.sql,
CREATE DATABASE IF NOT EXISTS MUSICDB CHARACTER SET utf8; USE MUSICDB; CREATE TABLE IF NOT EXISTS MUSIC( `ID` int(11) NOT NULL AUTO_INCREMENT, `NAME` varchar(50) NOT NULL, PRIMARY KEY (`ID`)) ENGINE=InnoDB; CREATE USER IF NOT EXISTS 'saltborn'@'%' IDENTIFIED BY 'saltborn'; CREATE USER IF NOT EXISTS 'saltborn'@'localhost' IDENTIFIED BY 'saltborn'; GRANT SELECT, INSERT, UPDATE, DELETE ON MUSICDB.* TO 'saltborn'@'%'; GRANT SELECT, INSERT, UPDATE, DELETE ON MUSICDB.* TO 'saltborn'@'localhost'; FLUSH PRIVILEGES;
而 /var/lib/mysql 则是容器中 MYSQL 保存数据的地方,我们应该挂载到这个位置来保存数据到主机上(除非你不在乎这些数据).
要注意的是 当 /var/lib/mysql 已经有数据的时候, init.sql 就不会在下一次服务启动的时候被执行了.
还有就是 MYSQL_USER, MYSQL_ROOT_PASSWORD, MYSQL_DATABASE 等等这些变量,分别是说启动镜像的时候创建出用户,设定 root 密码和创建数据库(尽管我的 init.sql 的工作内容就是和这里的一样,但这几个变量还是要提供的).
额外思考
假设你已经根据上面这些思路自己动手操作过一遍了(选自己熟悉的后端语言写一个简单的程序),那么再想一下 后端程序 + MYSQL + 前端 这样的组合该如何用 docker-compose 打包呢?配置文件该如何写呢?(所有的答案都在这文件里面了).
这里我给点提示,前端访问后端提供的接口时候是跨域的,并且前端不像后端那样简单设置的 depends_on: music.api 就通过 music.api:8000/api 这种形式获取响应,不过可以通过 Nginx 做到反向代理来解决跨域问题,提示已经给了,剩下就要自己动手验证了.
结语
由于官方文档的有效例子太少,所以我就自己写了这么一个简单的实验过程,内容虽少但五脏俱全,包含了基本概念以及基础用法.还有就是官方文档的结构实在是太乱了,新手看到估计会很迷茫,所以每篇文章下面都出现大量的 thumbs down,不过官方文档的内容其实质量很好,针对这个问题我在文章用合适的地方放上官方的参考连接.
这不是 docker-compose.yml 的全部用法,如果你要实现负载均衡,拓展服务等,那么请去阅读 docker-compose.yml 的 reference 文档,本文边幅有限,再加上如果你看懂这篇文章的内容,那么就问题不大了.