不知不觉,docker相关的文章已连载好几篇了,今天我们来介绍下docker三剑客之compose,用一个实操型的案例来为该系列文章收尾。
之所以要以compose来做该系列文章的收尾的原因很简单,因为docker-compose确实给我们在实际的工作中带来了很大的便捷性。根据我所在的小组定位,旨在开发一些基础组件给各个业务系统使用,前段时间一系列的升级给不少domain集成带来不小的工作量,无论是本地联调还是集成测试。在没有下沉到mesh之前绝大部分组件不可避免的要各个业务方来一起来升级发布。幸好,我们从VM时代演进下来的一套体系,大部分业务系统都是nginx + service + mysql + redis作为标配,这就非常适合用compose来一键拉起开发/测试环境。
docker compose简介
Docker-Compose项目是Docker官方的开源项目,负责实现对Docker容器集群的快速编排。在实际环境中,一个应用往往由许多服务构成,往往需要运行多个容器。多个容器协同工作需要一个有效的工具来管理他们,定义这些容器如何相互关联,docker compose 应运而生。Docker-Compose将所管理的容器分为三层,分别是工程(project),服务(service)以及容器(container),一个工程当中可包含多个服务,每个服务中定义了一个或者多个容器运行的镜像,参数,依赖。Docker-Compose允许用户通过一个单独的docker-compose.yml模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。
Compose模板文件是一个定义服务、网络和卷的YAML文件。Compose模板文件默认路径是当前目录下的docker-compose.yml,可以使用.yml或.yaml作为文件扩展名。Docker-Compose标准模板文件包含version、services、networks 三大部分,最关键的是services和networks两个部分,例如本文实例最后的compose文件长这样:
version: '3'
services:
nginx:
container_name: demo_nginx
image: nginx:latest
restart: always
depends_on:
- api
ports:
- 80:80
- 443:443
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
database:
container_name: database
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost","--password=123456"]
timeout: 20s
retries: 10
build: ./mysql
environment:
MYSQL_DATABASE: t_order
MYSQL_ROOT_PASSWORD: 123456
MYSQL_ROOT_HOST: '%'
ports:
- 3306:3306
api:
build: ./api
ports:
- 10001
depends_on:
database:
condition: service_healthy
简单说明:
-
version:指定当前使用的一个版本(一般是2或者3),例中我们使用version 3
-
services:定义服务,例中我们定义了nginx,api,mysql三个服务
-
container_name: 容器名,可不指定让docker生成一个. 当容器需要有多个副本的时候不要去指定他,这会导致scale up 失败
-
image: 指定服务的镜像名或者ID
-
build:服务除了可以基于指定的镜像,还可以基于一份Dockerfile,在使用up启动时执行构建任务,构建标签是build,可以指定Dockerfile所在文件夹的路径。Compose将会利用Dockerfile自动构建镜像,然后使用镜像启动服务容器。
-
depend_on: 定义服务的依赖关系,这会决定启动先后顺序,但这仅仅是服务启动开始的顺序,并保证被依赖服务启动完毕,后面例子会有详细的解释.
-
ports: 指定容器的端口,以及宿主机的映射端口的映射.
-
volumes: 定义数据卷.
-
envrionment: 定义环境变量.
-
healthcheck/condition: 定义健康检查和依赖条件,保证依赖的服务启动完毕.
当然,除了例子中用到的这些并不是compose的全部能力,其他像net/host/links/dns等等在Dockfile里介绍过的几乎都可以使用,具体可以去翻阅官方文档.
有了docker-compose.yaml, compose命令就可以上场了!
docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...]
选项包括:
-f --file FILE指定Compose模板文件,默认为docker-compose.yml
-p --project-name NAME 指定项目名称,默认使用当前所在目录为项目名
--verbose 输出更多调试信息
-v,-version 打印版本并退出
--log-level LEVEL 定义日志等级(DEBUG, INFO, WARNING, ERROR, CRITICAL)
1)docker-compose up [options] [--scale SERVICE=NUM...] [SERVICE...]
用途:很常用的命令,用来启动服务
选项包括:
-d 在后台运行服务容器
-no-color 不是有颜色来区分不同的服务的控制输出
-no-deps 不启动服务所链接的容器
--force-recreate
强制重新创建容器,不能与-no-recreate同时使用
–no-recreate
如果容器已经存在,则不重新创建,不能与–force-recreate同时使用
–no-build 不自动构建缺失的服务镜像
–build 在启动容器前构建服务镜像
–abort-on-container-exit
停止所有容器,如果任何一个容器被停止,不能与-d同时使用
-t, –timeout TIMEOUT
停止容器时候的超时(默认为10秒)
–remove-orphans
删除服务中没有在compose文件中定义的容器
3)docker-compose ps [options] [SERVICE...]
列出项目中所有的容器
4)docker-compose stop [options] [SERVICE...]
用途:停止正在运行的容器,可以通过docker-compose start 再次启动
选项包括-t, –timeout TIMEOUT 停止容器时候的超时(默认为10秒)
docker-compose stop
5)docker-compose down [options]
用途:停止和删除容器、网络、卷、镜像。
选项包括:–rmi type,删除镜像,类型必须是:all,删除compose文件中定义的所有镜像;local,删除镜像名为空的镜像
-v, –volumes,删除已经在compose文件中定义的和匿名的附在容器上的数据卷
–remove-orphans,删除服务中没有在compose中定义的容器
6)docker-compose logs [options] [SERVICE...]
用途:查看服务容器的输出。默认情况下,docker-compose将对不同的服务输出使用不同的颜色来区分。
可以通过–no-color来关闭颜色。
-f 跟踪日志输出
7)docker-compose bulid [--build-arg key=val...] [SERVICE...]
用途:构建(重新构建)项目中的服务容器。
选项包括:–compress 通过gzip压缩构建上下环境
–force-rm 删除构建过程中的临时容器
–no-cache 构建镜像过程中不使用缓存
–pull 始终尝试通过拉取操作来获取更新版本的镜像
-m, –memory MEM为构建的容器设置内存大小
–build-arg key=val为服务设置build-time变量
8)docker-compose pull [options] [SERVICE...]
用途:拉取服务依赖的镜像。
选项包括:
–ignore-pull-failures,忽略拉取镜像过程中的错误
–parallel,多个镜像同时拉取
–quiet,拉取镜像过程中不打印进度信息
9)docker-compose restart [options] [SERVICE...]
重启项目中的服务。
选项包括:
-t, –timeout TIMEOUT,指定重启前停止容器的超时(默认为10秒)
10)docker-compose rm [options] [SERVICE...]
删除所有(停止状态的)服务容器。
选项包括:–f, –force,强制直接删除,包括非停止状态的容器
-v,删除容器所挂载的数据卷
另外,start/run/scale/pause/kill/config/create/exec等命令不一一列举了,有兴趣或者需要用到的时候去查阅吧!
docker compose项目实战
虽然本文的初衷是说利用compose在多个不同业务系统(每个系统都有类似的体系)之间来回联调和测试如何提高效率,但为了更加深刻,我们从零开始手把手来介绍如何利用docker compose来一键拉起开发环境。我们从下面最简单也是最典型的一个微服务出发,前置一个nginx,然后有一个api服务(有些功能需要多副本才能验证,所以我们需要三个副本),存储一个mysql容器(Redis也是类似的,而且比mysql更简单一些,所以这里就没有纳入进来).
1) nginx
稍微了解nginx都知道其配置是在/etc/nginx下,所以我们利用volume先搞出配置
# 1. 启动同时把配置文件所在的目录作为挂载了一个volume
docker run -dit --name nginx_test -p 8080:80 -v /etc/nginx nginx
# 2. 启动一个容器把配置文件搞下来
docker run --rm --volumes-from nginx_test -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /etc/nginx/conf.d
然后我们根们上面整体的图知道nginx需要监听80端口并转发到10001端的api服务,所以修改刚刚搞下来的default配置文件:
server {
listen 80;
charset utf-8;
access_log off;
location / {
proxy_pass http://api:10001;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /static {
access_log off;
expires 30d;
alias /app/static;
}
}
这个常规的简单配置就多废话了,主要是要注意proxy_pass写的容器服务名,原因是nginx和api服务不是同一个容器无法用localhost,而docker-compose可以通过服务名做容器之间的通信。
2)api
api是我们的微服务,也是业务系统的主体,这里假设是一个价格查询服务,给下单等上游系统提供API。我们从start.spring.io开始创建:
a) 添加一个coffee查询服务
model:
@Entity@Table(name = "T_COFFEE")
@Builder
@Data
@ToString(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper=false)
public class Coffee extends BaseEntity implements Serializable {
private String name;
private long price;
}
@MappedSuperclass
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BaseEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(updatable = false)
@CreationTimestamp
private Date createTime;
@UpdateTimestamp
private Date updateTime;
}
service & repository:
@Service
public class CoffeeService {
@Autowired
private CoffeeRepository coffeeRepository;
public List<Coffee> list(){
return coffeeRepository.findAll();
}
}
public interface CoffeeRepository extends JpaRepository<Coffee, Long> {}
应用启动&API
@Slf4j
@EnableTransactionManagement
@SpringBootApplication
@EnableJpaRepositories
@RestController
public class PriceApiApplication {
public static void main(String[] args) {
SpringApplication.run(PriceApiApplication.class, args);
}
@Autowired
private CoffeeService coffeeService;
@RequestMapping("coffee/list")
public List<Coffee> getAllCoffee(){
return coffeeService.list();
}
}
b) application.yml
server:
port: 10001
spring:
datasource:
url: jdbc:mysql://database:3306/t_order
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
jpa:
hibernate:
ddl-auto: none
show-sql: true
database-platform: org.hibernate.dialect.MySQLDialect
database: mysql
这里也要注意一下,数据库url使用的mysql容器的服务名,而不是localhost或者某个ip
c) Dockfile
FROM java:8
VOLUME /tmp ADD build/libs/api-service-0.0.1-SNAPSHOT.jar price-api.jar
EXPOSE 10001 ENTRYPOINT ["java", "-jar", "price-api.jar"]
3)mysql
mysql容器有一个小的要求,就是我们必须启动容器之后需要自动做好数据库和表的创建,以及初始数据的插入。所以我们不能直接使用官方的镜像,而是需要基于官方镜像把我们初始化要执行的sql脚本注入到/docker-entrypoint-initdb.d。
Dockfile
FROM mysql:8
ENV AUTO_RUN_DIR /docker-entrypoint-initdb.d
ENV INSTALL_DB_SQL init_database.sql
COPY ./$INSTALL_DB_SQL $AUTO_RUN_DIR/
EXPOSE 3306
RUN chmod a+x $AUTO_RUN_DIR/$INSTALL_DB_SQL
而init_database.sql就是我们要自动执行的脚本,虽然这里只有一个表来演示
CREATE DATABASE IF NOT EXISTS t_order default charset utf8 COLLATE utf8_general_ci;
USE t_order;
create table if not exists t_coffee(
id bigint auto_increment,
create_time timestamp,
update_time timestamp,
name varchar(255),
price bigint,
primary key (id)
);
insert into t_coffee (name, price, create_time, update_time)
values ('espresso', 2000, now(), now());
insert into t_coffee (name, price, create_time, update_time)
values ('latte', 3200, now(), now());
insert into t_coffee (name, price, create_time, update_time)
values ('capuccino', 2800, now(), now());
insert into t_coffee (name, price, create_time, update_time)
values ('mocha', 2500, now(), now());
4**)docker-compose.yml**
需要解释三点:
1)api不能指定container_name,避免扩容到3个副本时候容器名冲突
2)api 只定义host端口,映射的容器端口随机
3)为了保证在api启动时,Mysql已经启动好并且执行了sql脚本,我们需要加入condition和healthcheck
现在,我们可以用docker-compose up -d来启动:
省略了第一次pull mysql和nginx镜像部分日志,总之一切正常:
$ curl http://localhost/coffee/list
[{"id":1,"createTime":"2021-12-29T16:57:42.000+00:00","updateTime":"2021-12-29T16:57:42.000+00:00","name":"espresso","price":2000},{"id":2,"createTime":"2021-12-29T16:57:42.000+00:00","updateTime":"2021-12-29T16:57:42.000+00:00","name":"latte","price":3200},{"id":3,"createTime":"2021-12-29T16:57:42.000+00:00","updateTime":"2021-12-29T16:57:42.000+00:00","name":"capuccino","price":2800},{"id":4,"createTime":"2021-12-29T16:57:42.000+00:00","updateTime":"2021-12-29T16:57:42.000+00:00","name":"mocha","price":2500}]
最后,我们用docker-compose up --scale api=3 -d来做一下扩容
有了这个成功运行的案例,我们就可以作为模板套用到其他各个业务里,同样的模式一键拉起本地开发环境,或者拉起一台测试机不费吹灰之力!
最后的最后,再讲一个提高效率的小窍门,我在个人本地电脑实操demo时候发现好几个网络问题,一是gradle下载jar很慢,二是docker拉mysql:8镜像很慢,大中午的实在是等不起,所以可以通过添加国内mirror来加速:
gradle.build:
mac docker engine: