Docker Compose介紹及使用入門

一、compose介紹

在先前介紹Docker的內(nèi)容中,我們從代碼到部署容器需要經(jīng)過兩個步驟:

  • 打包鏡像docker build;
  • 部署啟動容器docker run;

在真實的企業(yè)級應(yīng)用中,一個服務(wù)往往需要和很多個其它的服務(wù)進(jìn)行關(guān)聯(lián),單個服務(wù)也有可能有多個容器實例,如果需要發(fā)布,很可能會需要人員手動對每一個容器進(jìn)行打包和啟動的操作,非常繁瑣,容易出錯。

在這種背景下,Docker Compose就有了用武之地。簡單來說,Docker Compose是一個用于定義和運行多個容器的工具,通過docker-compose.yml來實現(xiàn)對容器集群的編排工作。

Docker Compose管理著如下三個內(nèi)容:

  • 工程,docker-compose運行的目錄即為一個工程,在微服務(wù)場景下,我們往往都是使用git submodule的方式組建工程的,因此父項目就可以成為一個docker compose的工程;
  • 服務(wù),對應(yīng)子項目,一個工程可以包含多個子項目;
  • 容器,對應(yīng)服務(wù)的實例,一個服務(wù)可以有多個實例;

Docker Compose當(dāng)然也存在不足的地方,就是它只能用在單一host上進(jìn)行容器編排,無法跨節(jié)點host對容器進(jìn)行編排,那是Docker Swarm和K8s的范疇了,后續(xù)再討論。

二、compose使用

2.1 單服務(wù)單容器使用

我們新建一個SpringBoot應(yīng)用,僅僅包含一個Controller:

@Slf4j
@RestController
public class HelloController {

    @GetMapping("/getHello")
    public String getHello(){
        log.info("myapp works!");
        return "myapp is running ok!!!";
    }

}

請務(wù)必保證程序能正常運行,再進(jìn)行如下操作。并進(jìn)行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:
  # 服務(wù)名稱,可以自定義
  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)建了默認(rèn)類型的自定義網(wǎng)絡(luò),即bridge類型網(wǎng)絡(luò),而非使用默認(rèn)的docker0橋接網(wǎng)絡(luò),擁有自己的獨立網(wǎng)段,可以通過docker network ls及docker network inspect查看具體的網(wǎng)絡(luò)信息
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
 # 將鏡像寫入本地的鏡像倉庫,并以項目名稱_服務(wù)名稱命名鏡像
 => => 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)絡(luò);

    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)建服務(wù)的鏡像,但不會創(chuàng)建和啟動容器;

2.2 多服務(wù)多容器依賴使用

假設(shè)我們的應(yīng)用需要依賴其它服務(wù),比如需要使用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的配置,容器啟動的默認(rèn)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 多服務(wù)多容器獨立使用

除了如上依賴容器的使用,日常開發(fā)中,我們都是使用git submodule的方式組織父工程和多個子工程,那么部署的時候就需要同時部署多個微服務(wù)子工程。

我們重新新建一個SpringBoot的項目,名稱為demo,然后將工程下面的src刪除,因為它將是一個父工程,然后新建兩個模塊service1和service2,這兩個服務(wù)分別對外提供getHello的服務(wù),service1端口設(shè)置8080,service2端口設(shè)置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";
    }

}

確保兩個子項目都能正常運行后再進(jìn)行下面的步驟。

執(zhí)行maven的package命令,確保兩個服務(wù)都生成了各自的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 單服務(wù)多容器使用

我們在一開始講解docker-compose概念的時候,有提到過服務(wù)和容器之間的關(guān)系,即一個服務(wù)可以有多個容器,但是在上面的例子中,我們都是一個服務(wù)一個容器的,那么想要實現(xiàn)一個服務(wù)啟動多個容器該怎么操作呢?

我們還是拿2.1節(jié)的例子作為演示,只要修改docker-compose.yml文件的內(nèi)容:

version: "3.9"
services:
  myapp:
    build: .
    ports:
      - "8080"

我們把container_name: myapp去掉了,因為容器的名稱要求是唯一的,如果指定了名字,那么哪個容器叫這個名字呢?就不好區(qū)分了,去掉后,多個容器會使用工程名+服務(wù)名+數(shù)字進(jìn)行自動命名。

還有,需要把端口也改造為只指定容器的端口,不要指定host的端口,這樣會自動綁定host上未使用的隨機端口。其實如果Dockerfile中指定了暴露的端口,此處也可以不需要ports設(shè)置了。

到此,設(shè)置完畢,執(zhí)行啟動命令myapp>docker-compose up --scale myapp=2,就會啟動一個服務(wù)的兩個容器實例。

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

當(dāng)然,這兩個容器沒有實現(xiàn)負(fù)載均衡,這個不在本文討論范圍內(nèi)了,可以參考另外一篇文章《Nginx使用入門及實例演示》

三、參考文檔

使用Docker compose發(fā)布SpringBoot項目-阿里云開發(fā)者社區(qū) (aliyun.com)

Overview of Docker Compose | Docker Documentation

Compose file version 3 reference | Docker Documentation

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容