软件开发中最为麻烦的事情可能就是配置环境了。由于用户使用的操作系统具有多样性,即便使用跨平台的开发语言(如Java和Python)都不能保证代码能够在各种平台下都可以正常的运转,而且在不同的环境下我们安装的软件需要依赖的软件包也是不一样的。
那么问题来了,我们安装软件的时候可不可以把软件运行的环境一并安装?我们是不是可以把原始环境一模一样地复制过来呢?
虚拟机(virtual machine)就是带环境安装的一种解决方案,它可以在一种操作系统里面运行另一种操作系统,比如在Windows系统里面运行Linux系统,在macOS上运行Windows,而应用程序对此毫无感知。使用过虚拟机的人都知道,虚拟机用起来跟真实系统一模一样,而对于虚拟机的宿主系统来说,虚拟机就是一个普通文件,不需要了就删掉,对宿主系统或者其他的程序并没有影响。但是虚拟机通常会占用较多的系统资源,启动和关闭也非常的缓慢,总之用户体验并没有想象中的那么好。
Docker属于对Linux容器技术的一种封装(利用了Linux的namespace和cgroup技术),它提供了简单易用的容器使用接口,是目前最流行的 Linux 容器解决方案。Docker将应用程序与该程序的依赖打包在一个文件里面,运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。下图是虚拟机和容器的对比,左边是传统的虚拟机,右边是Docker。
目前,Docker主要用于几下几个方面:
- 提供一次性的环境。
- 提供弹性的云服务(利用Docker很容易实现扩容和收缩)。
- 实践微服务架构(隔离真实环境在容器中运行多个服务)。
下面以CentOS为例讲解如何安装Docker,使用Ubuntu、macOS或Windows的用户可以通过点击对应的链接了解这些平台下如何进行安装。
- 确定操作系统内核版本(CentOS 7要求64位,内核版本3.10+;CentOS 6要求64位,内核版本2.6+),可以通过下面的命令确定Linux系统内核版本。
uname -r
- 在CentOS下使用yum安装Docker并启动。
yum -y install docker
systemctl start docker
- 查看Docker的信息和版本。
docker version
docker info
接下来可以通过下载镜像和创建容器来看看Docker是否可以运转起来。可以使用下面的命令从Docker的镜像仓库下载名为hello-world的镜像文件。
docker pull hello-world
查看所有镜像文件。
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/hello-world latest fce289e99eb9 7 months ago 1.84 kB
通过镜像文件创建并运行容器。
docker container run --name mycontainer hello-world
说明:其中
mycontainer
是我们给容器起的名字,跟在--name
参数之后;hello-world
就是我们刚才下载的镜像文件。
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
如果要删除这个容器,可以使用下面的命令。
docker container rm mycontainer
在删除容器之后,我们还可以删除刚才下载的镜像文件。
docker rmi hello-world
说明:如果要在Ubuntu(内核版本3.10+)下面安装和启动Docker,可以按照如下的步骤进行。
apt update apt install docker-ce service docker start国内用户可以通过更换Ubuntu软件下载源来提升下载速度,具体请参照清华大学开源软件镜像站上的《Ubuntu镜像使用帮助》。
安装Docker后,由于直接访问dockerhub下载镜像会非常缓慢,建议将服务器更换为国内镜像,可以通过修改 /etc/docker/daemon.js
文件来做到。一般的云服务器会有自己专属的镜像,就不需要手动修改了。
{
"registry-mirrors": [
"http://hub-mirror.c.163.com",
"https://registry.docker-cn.com"
]
}
想要玩转Docker,最简单的办法就是马上用Docker创建一些自己学习和工作中需要用到的容器,接下来我们就带着大家一起来创建这些容器。
Nginx是高性能的Web服务器,同时也是做反向代理服务器的上佳选择。使用Docker可以非常简单的创建一个运行Nginx的容器,命令如下所示。
docker container run -d -p 80:80 --rm --name mynginx nginx
说明:上面的参数
-d
表示容器在后台运行(不产生输出到Shell)并显示容器的ID;-p
是用来映射容器的端口到宿主机的端口,冒号前面是宿主机的端口,冒号后面是容器内部使用的端口;--rm
表示容器停止后自动删除容器,例如执行命令docker container stop mynginx
后,容器就不复存在了;--name
后面的mynginx是自定义的容器名字;在创建容器的过程中,需要用到nginx的镜像文件,镜像文件的下载是自动完成的,如果没有指定版本号,默认是最新版本(latest)。
如果需要将自己的Web项目(页面)部署到Nginx上,可以使用容器拷贝命令将指定路径下所有的文件和文件夹拷贝到容器的指定目录中。
docker container cp /root/web/index.html mynginx:/usr/share/nginx/html
如果不愿意拷贝文件也可以在创建容器时通过数据卷操作--volume
将指定的文件夹映射到容器的某个目录中,例如将Web项目的文件夹直接映射到/usr/share/nginx/html
目录。我们先通过下面的命令让刚才创建的容器停止运行。
docker container stop mynginx
然后用下面的命令重新创建容器。
docker container run -d -p 80:80 --rm --name mynginx --volume $PWD/html:/usr/share/nginx/html nginx
说明:上面创建容器和拷贝文件的命令中,
container
是可以省略的,也就是说docker container run
和docker run
是一样的,而docker container cp
和docker cp
是一样的。此外,命令中的--volume
也可以缩写为-v
,就如同-d
是--detach
的缩写,-p
是--publish
的缩写。$PWD
代表宿主系统当前文件夹,这些对于使用过Unix或者Linux系统的人来说,应该是很容易理解的。
要查看运行中的容器,可以使用下面的命令。
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3c38d2476384 nginx "nginx -g 'daemon ..." 4 seconds ago Up 4 seconds 0.0.0.0:80->80/tcp mynginx
要启动和停止容器,可以使用下面的命令。
docker start mynginx
docker stop mynginx
由于在创建容器时使用了--rm
选项,容器在停止时会被移除,当我们使用下面的命令查看所有容器时,应该已经看不到刚才的mynginx
容器了。
docker container ls -a
如果在创建容器时没有指定--rm
选项,那么也可以使用下面的命令来删除容器。
docker rm mynginx
要删除正在运行中的容器,需要使用-f
选项。
docker rm -f mynginx
我们再来尝试用Docker安装一台MySQL服务器,首先可以先检查一下有没有MySQL的镜像文件。
docker search mysql
INDEX NAME DESCRIPTION STARS OFFICIAL AUTOMATED
docker.io docker.io/mysql MySQL is a ... 8486 [OK]
...
说明:上面查询结果的列依次代表索引、镜像名、镜像描述、用户评价、是否官方镜像、自动构建。
下载MySQL镜像并指定镜像的版本号。
docker pull mysql:5.7
如果需要查看已经下载的镜像文件,可以使用下面的命令。
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/nginx latest e445ab08b2be 2 weeks ago 126 MB
docker.io/mysql 5.7 f6509bac4980 3 weeks ago 373 MB
创建并运行MySQL容器。
docker run -d -p 3306:3306 --name mysql57 -v $PWD/mysql/conf:/etc/mysql/mysql.cnf.d -v $PWD/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7
注意:上面创建容器时我们又一次使用了数据卷操作,那是因为通常容器是随时创建随时删除的,而数据库中的数据却是需要保留下来的。
上面的两个数据卷操作一个是映射了MySQL配置文件所在的文件夹,一个是映射了MySQL数据所在的文件夹,这两个数据卷操作非常重要。我们可以将MySQL的配置文件放在$PWD/mysql/conf
目录下,配置文件的具体内容如下所示:
[mysqld]
pid-file=/var/run/mysqld/mysqld.pid
socket=/var/run/mysqld/mysqld.sock
datadir=/var/lib/mysql
log-error=/var/log/mysql/error.log
server-id=1
log-bin=/var/log/mysql/mysql-bin.log
expire_logs_days=30
max_binlog_size=256M
symbolic-links=0
如果安装了MySQL 8.x版本(目前的最新版本),在使用客户端工具连接服务器时可能会遇到error 2059: Authentication plugin 'caching_sha2_password' cannot be loaded
的问题,这是因为MySQL 8.x默认使用了名为“caching_sha2_password”的机制对用户口令进行了更好的保护,但是如果客户端工具不支持新的认证方式,连接就会失败。解决这个问题有两种方式:一是升级客户端工具来支持MySQL 8.x的认证方式;二是进入容器,修改MySQL的用户口令认证方式。下面是具体的步骤,我们先用docker exec
命令进入容器的交互式环境,假设运行MySQL 8.x的容器名字叫mysql8x
。
docker exec -it mysql8x /bin/bash
进入容器的交互式Shell之后,可以首先利用MySQL的客户端工具连接MySQL服务器。
mysql -u root -p
Enter password:
Your MySQL connection id is 16
Server version: 8.0.12 MySQL Community Server - GPL
Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
接下来通过SQL来修改用户口令就可以了。
alter user 'root'@'%' identified with mysql_native_password by '123456' password expire never;
当然,如果愿意你也可以查看一下用户表检查是否修改成功。
use mysql;
select user, host, plugin, authentication_string from user where user='root';
+------+-----------+-----------------------+-------------------------------------------+
| user | host | plugin | authentication_string |
+------+-----------+-----------------------+-------------------------------------------+
| root | % | mysql_native_password | *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 |
| root | localhost | mysql_native_password | *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 |
+------+-----------+-----------------------+-------------------------------------------+
2 rows in set (0.00 sec)
在完成上面的步骤后,现在即便不更新客户端工具也可以连接MySQL 8.x了。
接下来我们试一试运行多个容器并让多个容器之间通过网络通信。我们创建4个Redis容器来实现一主三从的主从复制结构。
docker run -d -p 6379:6379 --name redis-master redis redis-server
docker run -d -p 6380:6379 --name redis-slave-1 --link redis-master:redis-master redis redis-server --replicaof redis-master 6379
docker run -d -p 6381:6379 --name redis-slave-2 --link redis-master:redis-master redis redis-server --replicaof redis-master 6379
docker run -d -p 6382:6379 --name redis-slave-3 --link redis-master:redis-master redis redis-server --replicaof redis-master 6379
上面的命令中,--link
参数用于给容器创建网络别名,因为三台从机(slave)需要通过网络连接自己的主机(master)。虽然,我们可以通过docker inspect --format '{{ .NetworkSettings.IPAddress }}' <container-ID>
命令来查看到容器的IP地址,但是由于容器的即装即用性,容器的IP地址有可能会发生变化,如果直接使用IP地址,在容器重启后就可能会因为IP地址的变化导致从机无法连接到主机。使用--link
参数创建网络别名就是为了在启动Redis服务器时在redis-server
后面的--replicaof
参数后使用这个别名而不是IP地址。
接下来我们进入名为redis-master
的容器,看看主从复制的配置是否成功。
docker exec -it redis-master /bin/bash
通过redis-cli
启动命令行工具。
redis-cli
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:3
slave0:ip=172.17.0.4,port=6379,state=online,offset=1988,lag=0
slave1:ip=172.17.0.5,port=6379,state=online,offset=1988,lag=1
slave2:ip=172.17.0.6,port=6379,state=online,offset=1988,lag=1
master_replid:94703cfa03c3ddc7decc74ca5b8dd13cb8b113ea
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1988
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1988
通过上面的讲解,我们已经掌握了如何通过官方提供的镜像来创建容器。当然如果愿意,我们也可以用配置好的容器来生成镜像。简而言之,Docker镜像是由文件系统叠加而成的,系统的最底层是bootfs,相当于就是Linux内核的引导文件系统;接下来第二层是rootfs,这一层可以是一种或多种操作系统(如Debian或Ubuntu文件系统),Docker中的rootfs是只读状态的;Docker利用联合挂载技术将各层文件系统叠加到一起,最终的文件系统会包含有底层的文件和目录,这样的文件系统就是一个镜像,如下图所示。
之前我们讲过了如何查找、列出镜像和拉取(下载)镜像,接下来看看构建镜像的两种方式:
- 使用
docker commit
命令。(不推荐) - 使用
docker build
命令和Dockerfile文件。
为了演示如何构建镜像,我们先使用Ubuntu镜像来定制一个容器,命令如下所示。
docker run --name myubuntu -it ubuntu /bin/bash
在容器中执行下面的命令来安装Apache服务器并退出容器。
apt -y upgrade
apt -y install apache2
exit
我们将这个容器作为一个定制的Web服务器保存起来,当需要这样一台Web服务器的时候,就没有必要重新创建容器并安装Apache了。
首先我们通过下面的命令查看容器的ID。
docker container ls -a
docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
014bdb321612 ubuntu "/bin/bash" 5 minutes ago Exited (0) myubuntu
提交定制的容器。
docker commit 014bdb321612 jackfrued/mywebserver
查看镜像文件。
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
jackfrued/mywebserver latest 795b294d265a 14 seconds ago 189 MB
生成镜像文件以后,后面就可以利用刚才创建的镜像文件来创建新的容器。
Dockerfile使用DSL(Domain Specific Language)来构建一个Docker镜像,只要编辑好了Dockerfile文件,就可以使用docker build
命令来构建一个新的镜像。
我们先创建一个空文件夹并在文件夹下创建名为Dockerfile的文件。
touch Dockerfile
编辑这个Dockerfile文件添加如下所示的内容。
vim Dockerfile
# version: 0.0.1
FROM ubuntu:14.04
MAINTAINER jackfrued "[email protected]"
RUN apt-get update && apt-get install -y nginx
RUN echo 'hello, world!' > /usr/share/nginx/html/index.html
EXPOSE 80
我们来解释一下上面的Dockerfile文件。Dockerfile文件通过特殊的指令来指定基础镜像(FROM指令)、创建容器后需要指定的命令(RUN指令)以及需要暴露的端口(EXPOSE)等信息。我们稍后会专门为大家介绍这些Dockfile中的指令。
接下来我们可以使用docker build
命令来创建镜像,如下所示。
docker build -t="jackfrued/webserver" .
提示:上面的命令最后面的
.
千万不要漏掉了哦,它表示从当前路径下寻找Dockerfile。
通过下面的命令可以查看创建好的镜像。
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
jackfrued/webserver latest 87d6cb096be2 23 minutes ago 222 MB
如果想知道镜像文件是如何创建出来的,可以使用下面的命令。
docker history jackfrued/webserver
IMAGE CREATED CREATED BY SIZE
87d6cb096be2 25 minutes ago /bin/sh -c #(nop) EXPOSE 80/tcp 0 B
53d3bc3a123e 25 minutes ago /bin/sh -c service nginx start 3 B
10646b63275e 25 minutes ago /bin/sh -c echo 'hello, world!' > /usr/sha... 14 B
f3e3bf3e998e 25 minutes ago /bin/sh -c apt-get update && apt-get insta... 34.3 MB
c98e22cf5a64 26 minutes ago /bin/sh -c #(nop) MAINTAINER jackfrued "j... 0 B
2c5e00d77a67 3 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B
<missing> 3 months ago /bin/sh -c mkdir -p /run/systemd && echo '... 7 B
<missing> 3 months ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0 B
<missing> 3 months ago /bin/sh -c set -xe && echo '#!/bin/sh' >... 195 kB
<missing> 3 months ago /bin/sh -c #(nop) ADD file:1e01ab604c0cc30... 188 MB
使用该镜像来创建容器运行Web服务器。
docker run -d -p 80:80 --name mywebserver jackfrued/webserver nginx -g "daemon off;"
如果希望将上面创建的镜像文件放到dockerhub仓库中,可以按照如下所示的步骤进行操作。
通过下面的命令登录到dockerhub。
docker login
输入用户名和口令进行登录。
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: jackfrued
Password:
Login Succeeded
通过下面的命令将镜像推到仓库中。
docker push jackfrued/webserver
想了解Dockerfile的指令可以查看官方提供的参考手册,下面我们为大家介绍一些常用的指令。
-
FROM:设置基础镜像,必须是Dockerfile中的第一条指令。
FROM <镜像名> [AS <别名>]
或
FROM <镜像名>[:<标签>] [AS <别名>]
-
RUN:指定构建镜像时要执行的命令。
RUN <命令> [参数1], [参数2], ...
或
RUN ["可执行文件", "参数1", "参数2", ...]
-
CMD:指定构建镜像后要执行的命令。
CMD <命令> [参数1], [参数2], ...
或
CMD ["可执行文件", "参数1", "参数2", ...]
说明:Docker不同于虚拟机,容器本身就是一个进程,容器中的应用应该位于前台运行。CMD命令相当于就是用来指定容器主进程(创建容器后要在前台执行的程序)的,如果主进程结束了,容器也就停止运行了。所以在容器中启动Nginx不能使用
service nginx start
或是systemctl start nginx
而是要通过CMD ["nginx", "-g", "daemon off;"]
让它在前台运行。 -
ENTRYPOINT:和CMD类似,也可以执行命令,但
docker run
命令行中指定的任何参数都会被当做参数再次传给ENTRYPOINT指令中的命令,这就使得我们可以构建一个镜像,它既可以运行一个默认的命令,也支持通过docker run
命令行为该命令指定可覆盖的参数选项。ENTRYPOINT <命令> [参数1], [参数2], ...
或
ENTRYPOINT ["可执行文件", "参数1", "参数2", ...]
-
WORKDIR:在通过镜像创建新容器时,在容器内部创建一个工作目录,ENTRYPOINT和CMD指定的程序会在这个目录下执行。在使用
docker run
命令时可以通过-w
参数来覆盖由WORKDIR指定的工作目录。例如:WORKDIR /opt/webapp
docker run -w /usr/share/webapp ...
-
ENV:在创建镜像时设置环境变量。在使用
docker run
命令时,可以通过-e
参数来修改环境变量的设置。例如:ENV DEFAULT_PORT=8080
docker run -e "DEFAULT_PORT=8000" ...
-
USER:指定镜像会以什么用户身份去运行。例如:
USER nginx
-
VOLUME:在创建容器时添加一个数据卷的挂载点。通过数据卷操作可以实现容器间数据的共享和重用,对卷所作的修改可以马上生效而不需要重新启动容器,我们之前创建容器时使用
—volume
参数就是为了实现数据卷的映射操作。VOLUME ["/路径1", "/路径2/子路径2.1/", ...]
-
ADD:将构建目录下的文件和文件夹复制到镜像中,如果是压缩文件和归档文件,ADD命令会对这些文件进行解压缩解归档的操作。
ADD [--chown=<用户>:<用户组>] <源文件> <目标文件>
-
COPY:非常类似于ADD,但不会主动对文件进行提取操作。
-
LABEL:为Docker镜像添加一些元数据,在使用
docker inspect
命令时会看到这些元数据。LABEL version="1.0.0" location="Chengdu"
-
ONBUILD:为镜像添加触发器,当一个镜像被用作其他镜像的基础镜像,触发器将会被执行。例如:
ONBUILD ADD . /app/src ONBUILD RUN cd /app/src && make
我们的项目可能会使用了多个容器,容器多了之后管理容器的工作就会变得麻烦。如果要对多个容器进行自动配置使得容器可以相互协作甚至实现复杂的调度,这就需要进行容器编排。Docker原生对容器编排的支持非常弱,但是可以通过社区提供的工具来实现容器编排。
可以通过安装Docker Compose工具来实现基于YAML文件的容器编排,YAML文件会定义一系列的容器以及容器运行时的属性,Docker Compose会根据这些配置来管理容器。
-
安装Docker Compose。
curl -L https://github.com/docker/compose/releases/download/1.25.0-rc2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose
说明:如果没有curl工具,在CentOS下可以先通过包管理工具yum安装curl再执行上面的命令。
当然我们也可以使用Python的包管理工具pip来安装Docker Compose,命令如下所示。
pip3 install -U docker-compose
-
使用Docker Compose。
我们先创建一个名为
composeapp
的文件夹并在该文件夹下创建两个子文件夹product-service
和web-site
,如下所示。mkdir composeapp cd composeapp mkdir product-service mkdir web-site
我们先在
product-service
文件夹下编写提供数据的API接口程序。vim product-service/api.py
我们用Flask来实现一个非常简单的数据接口服务程序。
from pickle import dumps, loads from flask import Flask from flask_restful import Resource, Api from redis import Redis from flask_cors import CORS app = Flask(__name__) CORS(app, resources={r'/api/*': {'origins': '*'}}) api = Api(app) redis = Redis(host='redis-master', port=6379) class Product(Resource): def get(self): data = redis.get('products') if not data: products = ['Ice Cream', 'Chocolate', 'Coca Cola', 'Hamburger'] redis.set('products', dumps(products)) else: products = loads(data) return {'products': products} api.add_resource(Product, '/api/products') if __name__ == '__main__': app.run(host='0.0.0.0', port=8000, debug=True)
由于上面的项目需要依赖
flask
、flask-restful
等三方库,所以我们再添加一个指明依赖库的文件并将其命名为requirements.txt
,其内容如下所示。vim product-service/requirements.txt
flask flask-restful flask-cors redis
稍后我们会将上面的接口服务放在一个容器中运行,为此我们先编写一个Dockerfile文件以便创建对应的镜像,其内容如下所示。
vim product-service/Dockerfile
FROM python:3 ADD . /root/product-service WORKDIR /root/product-service RUN pip install -r requirements.txt CMD ["python", "api.py"]
我们再去到
web-site
目录下创建一个页面,稍后我们会通一个容器来提供Nginx服务并运行该页面,而这个页面会访问我们刚才部署的数据接口服务获取数据并通过Vue.js将数据渲染到页面上。vim web-site/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>首页</title> </head> <body> <div id="app"> <h2>产品列表</h2> <ul> <li v-for="product in products">{{ product }}</li> </ul> </div> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> <script> new Vue({ el: '#app', data: { products: [] }, created() { fetch('http://1.2.3.4:8000/api/products') .then(resp => resp.json()) .then(json => {this.products = json.products}) } }) </script> </body> </html>
接下来,我们要通过一个YAML文件来创建三个容器并指明容器之间的依赖关系。
vim docker-compose.yml
version: '3' services: product-service: build: ./product-service ports: - '8000:8000' links: - redis-master web-site: image: nginx ports: - '80:80' volumes: - ./web-site:/usr/share/nginx/html redis-master: image: redis expose: - '6379'
有了这个YAML文件,我们就可以使用
docker-compose
命令来创建和管理这三个容器,其命令如下所示。docker-compose up
Creating network "composeapp_default" with the default driver Building product-service Step 1/5 : FROM python:3 ---> e497dabd8450 Step 2/5 : ADD . /root/product-service ---> fbe62813d595 Removing intermediate container 6579e845565a Step 3/5 : WORKDIR /root/product-service ---> 3a722675e3b1 Removing intermediate container 57fc490436ce Step 4/5 : RUN pip install -r requirements.txt ---> Running in cadc2d0c1b9b ... ... ---> fc747fc11f4a Removing intermediate container cadc2d0c1b9b Step 5/5 : CMD python api.py ---> Running in ecbbd2a69906 ---> 637e760f2e5b Removing intermediate container ecbbd2a69906 Successfully built 637e760f2e5b WARNING: Image for service product-service was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`. Creating composeapp_redis-master_1 ... done Creating composeapp_web-site_1 ... done Creating composeapp_product-service_1 ... done Attaching to composeapp_redis-master_1, composeapp_web-site_1, composeapp_product-service_1 ... ...