一、compose介紹
在先前介紹Docker的內(nèi)容中,我們從代碼到部署容器需要經(jīng)過兩個步驟:
- 打包鏡像
docker build
洋侨; - 部署啟動容器
docker run
;
在真實的企業(yè)級應用中倦蚪,一個服務往往需要和很多個其它的服務進行關(guān)聯(lián)希坚,單個服務也有可能有多個容器實例,如果需要發(fā)布陵且,很可能會需要人員手動對每一個容器進行打包和啟動的操作裁僧,非常繁瑣,容易出錯。
在這種背景下聊疲,Docker Compose就有了用武之地茬底。簡單來說,Docker Compose是一個用于定義和運行多個容器的工具获洲,通過docker-compose.yml
來實現(xiàn)對容器集群的編排工作桩警。
Docker Compose管理著如下三個內(nèi)容:
- 工程,docker-compose運行的目錄即為一個工程昌妹,在微服務場景下,我們往往都是使用git submodule的方式組建工程的握截,因此父項目就可以成為一個docker compose的工程飞崖;
- 服務,對應子項目谨胞,一個工程可以包含多個子項目固歪;
- 容器,對應服務的實例胯努,一個服務可以有多個實例牢裳;
Docker Compose當然也存在不足的地方,就是它只能用在單一host上進行容器編排叶沛,無法跨節(jié)點host對容器進行編排蒲讯,那是Docker Swarm和K8s的范疇了,后續(xù)再討論灰署。
二判帮、compose使用
2.1 單服務單容器使用
我們新建一個SpringBoot應用,僅僅包含一個Controller:
@Slf4j
@RestController
public class HelloController {
@GetMapping("/getHello")
public String getHello(){
log.info("myapp works!");
return "myapp is running ok!!!";
}
}
請務必保證程序能正常運行溉箕,再進行如下操作晦墙。并進行package,打成jar包肴茄。
編寫Dockerfile:
FROM openjdk:8
EXPOSE 8080
ADD target/myapp-0.0.1-SNAPSHOT.jar /demo.jar
ENTRYPOINT ["java", "-jar", "demo.jar"]
編寫docker-compose.yml文件:
# 使用的yml版本
version: "3.9"
services:
# 服務名稱晌畅,可以自定義
myapp:
# 容器名稱,可以自定義
container_name: myapp
# 指定Dockerfile所在的目錄
build: .
ports:
- "8080:8080"
然后執(zhí)行docker-compose up
即可寡痰,主要完成以下的兩步操作:
- 鏡像構(gòu)建
docker build
抗楔; - 啟動yml中的所有容器
docker run
;
執(zhí)行過程如下:
E:\myapp>docker-compose up
# 創(chuàng)建了默認類型的自定義網(wǎng)絡氓癌,即bridge類型網(wǎng)絡谓谦,而非使用默認的docker0橋接網(wǎng)絡,擁有自己的獨立網(wǎng)段贪婉,可以通過docker network ls及docker network inspect查看具體的網(wǎng)絡信息
Creating network "myapp_default" with the default driver
Building myapp
[+] Building 0.5s (7/7) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 153B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/openjdk:8 0.0s
=> [internal] load build context 0.2s
=> => transferring context: 17.62MB 0.1s
=> CACHED [1/2] FROM docker.io/library/openjdk:8 0.0s
=> [2/2] ADD target/myapp-0.0.1-SNAPSHOT.jar /demo.jar 0.1s
=> exporting to image 0.1s
=> => exporting layers 0.1s
# 將鏡像寫入本地的鏡像倉庫反粥,并以項目名稱_服務名稱命名鏡像
=> => writing image sha256:c387978706931f09fa16a737704f2c1047e8f632de192a25b0dc42dc151ac4c7 0.0s
=> => naming to docker.io/library/myapp_myapp 0.0s
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
WARNING: Image for service myapp was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating myapp ... done
Attaching to myapp
myapp |
myapp | . ____ _ __ _ _
myapp | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
myapp | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
myapp | \\/ ___)| |_)| | | | | || (_| | ) ) ) )
myapp | ' |____| .__|_| |_|_| |_\__, | / / / /
myapp | =========|_|==============|___/=/_/_/_/
myapp | :: Spring Boot :: (v2.6.0)
......
到此,我們的單容器使用方式完成了。
如下是一些常見的docker-compose操作(需要在工程目錄下執(zhí)行命令):
docker-compose up才顿,構(gòu)建鏡像并啟動容器莫湘;
-
docker-compose down,停止容器郑气,刪除容器幅垮,移除自定義網(wǎng)絡;
E:\myapp>docker-compose down Stopping myapp ... done Removing myapp ... done Removing network myapp_default
-
docker-compose ls尾组,查看所有運行的容器忙芒;
E:\myapp>docker-compose ps Name Command State Ports ----------------------------------------------------------- myapp java -jar demo.jar Up 0.0.0.0:8080->8080/tcp
docker-compose logs -f container_name,查看具體容器的日志讳侨,-f參數(shù)表示實時日志輸出呵萨;
docker-compose port container_name container_port,查看和容器端口綁定的主機端口跨跨;
docker-compose stop container_name潮峦,停止指定的容器,如果不指定則停止所有的容器勇婴;
docker-compose start container_name忱嘹,啟動指定的容器,如果不指定則停止所有的容器耕渴;
docker-compose rm container_name拘悦,刪除指定的已停止容器,如果不指定則刪除所有已停止容器橱脸;
docker-compose build窄做,構(gòu)建或者重新構(gòu)建服務的鏡像,但不會創(chuàng)建和啟動容器慰技;
2.2 多服務多容器依賴使用
假設我們的應用需要依賴其它服務椭盏,比如需要使用redis,mysql等吻商,那么這種場景下掏颊,就需要被依賴的容器先啟動。
首先艾帐,我們改造上述例子中的myapp代碼乌叶,需要引入redis的支持依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
然后增加redis的配置,容器啟動的默認redis是沒有密碼的柒爸,所以不用配置password准浴。
server:
port: 8080
spring:
redis:
host: 127.0.0.1
port: 6379
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
再增加redis的序列化和反序列化的配置:
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {
/**
* 配置自定義redisTemplate
* @return
*/
@Bean
RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
//使用StringRedisSerializer來序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
再后,我們需要修改Controller的邏輯捎稚,使得返回的結(jié)果依賴redis:
@Slf4j
@RestController
public class HelloController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/getHello")
public String getHello(){
log.info("myapp works!");
Long counter = redisTemplate.opsForValue().increment("counter");
return "myapp is running " + counter + "times!";
}
}
如此乐横,每次訪問該接口都會使得計數(shù)器加1并返回結(jié)果求橄。
最后,我們只需要修改docker-compose.yml
:
version: "3.9"
services:
myapp:
container_name: myapp
build: .
ports:
- "8080:8080"
depends_on:
- myredis
myredis:
image: "redis:latest"
其它內(nèi)容不變葡公,如此配置就全部完成了罐农,注意在執(zhí)行如下操作之前,先確保程序能夠正常運行催什,可以先自行運行一個redis容器做下實驗涵亏。
docker-compose up
啟動工程,過程如下:
......
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
WARNING: Image for service myapp was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating myapp ... done
Creating myapp_myredis_1 ... done
Attaching to myapp_myredis_1, myapp
myredis_1 | 1:C 21 Nov 2021 03:19:06.934 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
myredis_1 | 1:C 21 Nov 2021 03:19:06.934 # Redis version=6.2.6, bits=64, commit=00000000, modified=0, pid=1, just started
myredis_1 | 1:C 21 Nov 2021 03:19:06.934 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
myredis_1 | 1:M 21 Nov 2021 03:19:06.935 * monotonic clock: POSIX clock_gettime
myredis_1 | 1:M 21 Nov 2021 03:19:06.936 * Running mode=standalone, port=6379.
myredis_1 | 1:M 21 Nov 2021 03:19:06.936 # Server initialized
myredis_1 | 1:M 21 Nov 2021 03:19:06.936 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the
command 'sysctl vm.overcommit_memory=1' for this to take effect.
myredis_1 | 1:M 21 Nov 2021 03:19:06.936 * Ready to accept connections
myapp |
myapp | . ____ _ __ _ _
myapp | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
myapp | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
myapp | \\/ ___)| |_)| | | | | || (_| | ) ) ) )
myapp | ' |____| .__|_| |_|_| |_\__, | / / / /
myapp | =========|_|==============|___/=/_/_/_/
myapp | :: Spring Boot :: (v2.6.0)
......
此處的redis是使用的已有鏡像蒲凶,所以不會再創(chuàng)建redis的鏡像气筋,但是myapp是需要build構(gòu)建的,所以需要創(chuàng)建myapp的鏡像旋圆,然后再基于這倆個鏡像分別創(chuàng)建兩個容器裆悄,這兩個容器都屬于myapp這個工程下面。
2.3 多服務多容器獨立使用
除了如上依賴容器的使用臂聋,日常開發(fā)中,我們都是使用git submodule的方式組織父工程和多個子工程或南,那么部署的時候就需要同時部署多個微服務子工程孩等。
我們重新新建一個SpringBoot的項目,名稱為demo采够,然后將工程下面的src刪除肄方,因為它將是一個父工程,然后新建兩個模塊service1和service2蹬癌,這兩個服務分別對外提供getHello的服務权她,service1端口設置8080,service2端口設置8081逝薪。
@Slf4j
@RestController
public class HelloRest {
@GetMapping("/service1/getHello")
public String getHello(){
return "hello from service1";
}
}
@Slf4j
@RestController
public class HelloRest {
@GetMapping("/service2/getHello")
public String getHello(){
return "hello from service2";
}
}
確保兩個子項目都能正常運行后再進行下面的步驟隅要。
執(zhí)行maven的package命令,確保兩個服務都生成了各自的jar董济,然后在各自的目錄內(nèi)新建Dockerfile:
FROM openjdk:8
EXPOSE 8080
ADD target/service1-0.0.1-SNAPSHOT.jar /demo.jar
ENTRYPOINT ["java", "-jar", "demo.jar"]
FROM openjdk:8
EXPOSE 8081
ADD target/service2-0.0.1-SNAPSHOT.jar /demo.jar
ENTRYPOINT ["java", "-jar", "demo.jar"]
然后在父工程目錄下新建docker-compose.yml
version: "3.9"
services:
service1:
container_name: service1
# 指定Dockerfile的目錄
build: ./service1
ports:
- "8080:8080"
service2:
container_name: service2
# 指定Dockerfile的目錄
build: ./service2
ports:
- "8081:8081"
然后可以執(zhí)行docker-compose up
了步清,發(fā)現(xiàn)會新構(gòu)建兩個鏡像demo_service1和demo_service2,同時創(chuàng)建兩個容器并啟動虏肾。
Creating service1 ... done
Creating service2 ... done
Attaching to service1, service2
2.4 單服務多容器使用
我們在一開始講解docker-compose概念的時候廓啊,有提到過服務和容器之間的關(guān)系,即一個服務可以有多個容器封豪,但是在上面的例子中谴轮,我們都是一個服務一個容器的,那么想要實現(xiàn)一個服務啟動多個容器該怎么操作呢吹埠?
我們還是拿2.1節(jié)的例子作為演示第步,只要修改docker-compose.yml文件的內(nèi)容:
version: "3.9"
services:
myapp:
build: .
ports:
- "8080"
我們把container_name: myapp
去掉了疮装,因為容器的名稱要求是唯一的,如果指定了名字雌续,那么哪個容器叫這個名字呢斩个?就不好區(qū)分了,去掉后驯杜,多個容器會使用工程名+服務名+數(shù)字進行自動命名受啥。
還有,需要把端口也改造為只指定容器的端口鸽心,不要指定host的端口滚局,這樣會自動綁定host上未使用的隨機端口。其實如果Dockerfile中指定了暴露的端口顽频,此處也可以不需要ports設置了藤肢。
到此,設置完畢糯景,執(zhí)行啟動命令myapp>docker-compose up --scale myapp=2
嘁圈,就會啟動一個服務的兩個容器實例。
E:\myapp>docker-compose up --scale myapp=2
Creating network "myapp_default" with the default driver
Creating myapp_myapp_1 ... done
Creating myapp_myapp_2 ... done
Attaching to myapp_myapp_2, myapp_myapp_1
...
E:\myapp>docker-compose ps
Name Command State Ports
--------------------------------------------------------------------
myapp_myapp_1 java -jar demo.jar Up 0.0.0.0:53425->8080/tcp
myapp_myapp_2 java -jar demo.jar Up 0.0.0.0:53424->8080/tcp
當然蟀淮,這兩個容器沒有實現(xiàn)負載均衡最住,這個不在本文討論范圍內(nèi)了,可以參考另外一篇文章《Nginx使用入門及實例演示》怠惶。
三涨缚、參考文檔
使用Docker compose發(fā)布SpringBoot項目-阿里云開發(fā)者社區(qū) (aliyun.com)