从零开始 Spring Cloud 8:Docker
图源:laiketui.com
Docker 可以帮助我们更方便地部署 Spring Cloud 应用。
环境准备
准备 Docker 环境可以参考 这篇文章。
操作镜像
docker 的镜像相关操作主要涉及以下命令:
docker pull
,从 DockerHub 拉取镜像到本地。docker images
,查看本地镜像列表。docker save
,导出镜像到 tar 文件。docker load
,从 tar 文件加载镜像。docker rmi
,删除镜像。
示例
下面演示如何使用上面这些命令。
首先从 DockerHub 下载 Redis 镜像到本地。
DockerHub 在国内无法正常访问,需要科学上网。
在 DockerHub 上搜索 Redis:
第一个镜像的 DOCKER OFFICIAL IMAGE 标识表示这是一个官方镜像。
点开这个镜像后,右侧有一个镜像下载命令 docker pull redis。可以直接在 Linux 的控制台通过这个命令拉取镜像到本地:
[icexmoon@192 ~]$ docker pull redis
Using default tag: latest
latest: Pulling from library/redis
a2abf6c4d29d: Pull complete
c7a4e4382001: Pull complete
4044b9ba67c9: Pull complete
c8388a79482f: Pull complete
413c8bb60be2: Pull complete
1abfd3011519: Pull complete
Digest: sha256:db485f2e245b5b3329fdc7eff4eb00f913e09d8feb9ca720788059fdc2ed8339
Status: Downloaded newer image for redis:latest
docker.io/library/redis:latest
可以通过docker images
命令查看本地的镜像:
[icexmoon@192 ~]$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 7614ae9453d1 19 months ago 113MB
这里的TAG
指的是镜像的版本,一个镜像是由 repository 和 tag 唯一确定的,如果没有指定 tag,将会使用 latest 作为 tag。
我们可以在 DockerHub 上查看 repository 的所有 tag:
可以指定 tag 以下载指定版本的镜像,比如:
[icexmoon@192 ~]$ docker pull redis:6.0.20
通过 Docker 下载的镜像是由 Docker 管理的,我们可以将其导出为 tar 文件:
[icexmoon@192 ~]$ docker save -o ./redis.tar redis
需要注意的是,与docker pull
命令不同的是,上边的命令会将所有的redis
镜像打包:
[icexmoon@192 docker_images]$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
redis 6.0.20 787454454b81 2 weeks ago 126MB
redis latest 7614ae9453d1 19 months ago 113MB
[icexmoon@192 docker_images]$ docker save -o ./redis.6.0.20.tar 787454454b81
[icexmoon@192 docker_images]$ docker save -o ./redis.latest.tar redis:latest
[icexmoon@192 docker_images]$ ls -alh
总用量 472M
drwxr-xr-x. 2 icexmoon icexmoon 71 7月 26 19:36 .
drwx------. 15 icexmoon icexmoon 4.0K 7月 26 19:34 ..
-rw-------. 1 icexmoon icexmoon 125M 7月 26 19:35 redis.6.0.20.tar
-rw-------. 1 icexmoon icexmoon 111M 7月 26 19:36 redis.latest.tar
-rw-------. 1 icexmoon icexmoon 236M 7月 26 19:32 redis.tar
所以导出镜像时最好指定 tag 或使用 image ID。
可以用docker rmi
命令删除镜像:
[icexmoon@192 docker_images]$ docker rmi redis
[icexmoon@192 docker_images]$ docker rmi redis:6.0.20
[icexmoon@192 docker_images]$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
可以用docker load
命令从 tar 文件加载镜像:
[icexmoon@192 docker_images]$ docker load -i ./redis.latest.tar
[icexmoon@192 docker_images]$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 7614ae9453d1 19 months ago 113MB
操作容器
镜像是程序代码外加所需的运行环境(函数库等),是静态资源。容器(Container)可以看做是“运行的镜像”或“镜像的运行实例”,是运行的进程。
容器拥有三种状态:
- 运行:进程正常运行
- 暂停:进程暂停,CPU不再运行,并不释放内存
- 停止:进程终止,回收进程占用的内存、CPU等资源
可以用以下命令改变容器状态:
- docker run:创建并运行一个容器,处于运行状态
- docker pause:让一个运行的容器暂停
- docker unpause:让一个容器从暂停状态恢复运行
- docker stop:停止一个运行的容器
- docker start:让一个停止的容器再次运行
- docker rm:删除一个容器
可以用下图表示命令和状态的关系:
示例:修改容器状态
下面用一个示例进行说明具体命令的使用。
先下载一个 Nginx 镜像:
[icexmoon@192 ~]$ docker pull nginx
利用这个镜像启动一个容器:
[icexmoon@192 ~]$ docker run --name my-nginx -d -p 80:80 nginx
7108f6283ddd9d0a5725959bd223c3f89dfcbd679575f894e97eacc0f8c54bb3
- 启动命令下输出的一行字符串是容器 ID。
- 可以在 DockerHub 上查看镜像对应的容器启动命令。
这条命令包含以下参数:
--name
,指定在 Docker 中唯一的容器名称。-d
,在后台运行容器。-p
,容器的端口映射,80:80
意味着将宿主机的80
端口映射到my-nginx
容器的80
端口。这样才能让外部应用通过网络访问容器。前一个端口指的是宿主机的端口,后一个端口指的是容器的端口。nginx
,启动容器用的镜像。可以是镜像名称也可以使镜像 ID。
查看容器运行状态:
[icexmoon@192 ~]$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7108f6283ddd nginx "/docker-entrypoint.…" 3 seconds ago Up 2 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp my-nginx
打印出的信息中容器 ID 不是完整的,只显示前边的部分。
命令打印的信息包含:
CONTAINER ID
,容器 ID。IMAGE
,容器使用的镜像名称。COMMAND
,容器的内部启动命令。CREATED
,容器的创建时间。STATUS
,运行状态。Up 2 seconds
表示正在运行,已经运行了2s。PORTS
,容器的端口映射。NAMES
,容器名称。
可以通过同一个局域网的设备访问虚拟机的 IP(比如 http://192.168.0.88/),就可以看到 Nginx 的欢迎页面。
如果无法访问,可能是 Linux 防火墙阻止了访问。可以关闭防火墙:
sudo systemctl stop firewalld sudo systemctl disable firewalld
关闭防火墙后需要重启 Docker 服务:
sudo systemctl restart docker
暂停容器:
[icexmoon@192 ~]$ docker pause my-nginx
my-nginx
[icexmoon@192 ~]$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7108f6283ddd nginx "/docker-entrypoint.…" 33 minutes ago Up 33 minutes (Paused) 0.0.0.0:80->80/tcp, :::80->80/tcp my-nginx
解除暂停状态:
[icexmoon@192 ~]$ docker unpause my-nginx
my-nginx
[icexmoon@192 ~]$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7108f6283ddd nginx "/docker-entrypoint.…" 34 minutes ago Up 34 minutes 0.0.0.0:80->80/tcp, :::80->80/tcp my-nginx
停止容器:
[icexmoon@192 ~]$ docker stop my-nginx
my-nginx
[icexmoon@192 ~]$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0ee45eecfc6d nginx "/docker-entrypoint.…" 16 minutes ago Exited (0) 3 seconds ago my-nginx
默认情况下
docker ps
只显示运行和暂停的容器,不显示被停止的容器,要显示全部容器,需要使用docker ps -a
。
重新启动容器:
[icexmoon@192 ~]$ docker start my-nginx
my-nginx
[icexmoon@192 ~]$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7108f6283ddd nginx "/docker-entrypoint.…" 36 minutes ago Up 2 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp my-nginx
删除容器:
[icexmoon@192 ~]$ docker stop my-nginx
my-nginx
[icexmoon@192 ~]$ docker rm my-nginx
my-nginx
[icexmoon@192 ~]$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
删除容器前必须先停止容器。
示例:查看日志
查看容器的日志:
[icexmoon@192 ~]$ docker logs my-nginx
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
...
如果容器已经删除,需要重新启动一个容器。
可以添加一个参数-f
持续输出日志:
[icexmoon@192 ~]$ docker logs my-nginx -f
此时只要有新的请求访问 Nginx,日志就会立即打印。
示例:进入容器
有时候我们需要进入容器以修改容器中的文件,可以:
[icexmoon@192 ~]$ docker exec -it my-nginx bash
root@0ee45eecfc6d:/#
docker exec
命令可以让我们在容器中执行命令,这里的-it
参数意思是为容器创建标准输入和输出,能够让我们与容器交互。my-nginx
是要执行命令的容器。bash
是我们要在容器中执行的命令。
bash
是一个 Linux 中流行的字符中终端,具体介绍可以阅读Linux 之旅 8:初识 BASH。
可以在容器的 Bash 环境执行一些基本命令:
root@0ee45eecfc6d:/# ls
bin dev docker-entrypoint.sh home lib64 mnt proc run srv tmp var
boot docker-entrypoint.d etc lib media opt root sbin sys usr
root@0ee45eecfc6d:/# pwd
/
定位 Nginx 的默认 html 文件:
root@0ee45eecfc6d:/# cd /usr/share/nginx/html
root@0ee45eecfc6d:/usr/share/nginx/html# ls -al
total 8
drwxr-xr-x. 2 root root 40 Dec 29 2021 .
drwxr-xr-x. 3 root root 18 Dec 29 2021 ..
-rw-r--r--. 1 root root 497 Dec 28 2021 50x.html
-rw-r--r--. 1 root root 615 Dec 28 2021 index.html
root@0ee45eecfc6d:/usr/share/nginx/html# cat index.html
<!DOCTYPE html>
<html>
<head>
...
Linux 下常用的文本编辑器有 vim 和 vi,但遗憾的是我们都不能使用:
root@0ee45eecfc6d:/usr/share/nginx/html# vim index.html
bash: vim: command not found
root@0ee45eecfc6d:/usr/share/nginx/html# vi index.html
bash: vi: command not found
因为容器只包含必要的工具,并没有安装这两个命令。
不过我们可以用 sed 命令进行简单替换:
sed -i -e 's#Welcome to nginx#传智教育欢迎您#g' -e 's#<head>#<head><meta charset="utf-8">#g' index.html
命令 sed 的详细说明可以阅读这篇文章。
现在再通过浏览器访问容器中的 Nginx,就可以看到欢迎页面已经改变。
和使用 Bash 一样,可以用exit
命令退出容器中的 Bash 环境:
root@0ee45eecfc6d:/usr/share/nginx/html# exit
exit
[icexmoon@192 ~]$
数据卷
虽然我们可以像前面演示的那样进入容器,并修改容器中文件的内容。但一般并不推荐这样做。因为存在以下缺陷:
- 缺少工具不容易修改内容。
- 数据与容器耦合,很难在多个容器之间共享数据。
- 如果容器需要升级,无法保留已经修改的部分数据。
所以我们需要使用数据卷(Volumn)来解决这些问题。
数据卷可以看做是 Docker 管理的一个虚拟的目录,它映射到本地的真实目录,并且可以挂载到容器中:
查看数据卷的相关命令:
[icexmoon@192 ~]$ docker volume --help
Usage: docker volume COMMAND
Manage volumes
Commands:
create Create a volume
inspect Display detailed information on one or more volumes
ls List volumes
prune Remove all unused local volumes
rm Remove one or more volumes
数据卷相关命令有:
- create,创建数据卷。
- inspect,显示数据卷的详细信息。
- ls,列出数据卷。
- prune,删除没有使用的数据卷。
- rm,移除数据卷。
示例:挂载数据卷
创建数据卷:
[icexmoon@192 ~]$ docker volume create html
html
查看数据卷:
[icexmoon@192 ~]$ docker volume ls
DRIVER VOLUME NAME
...
local html
除了刚创建的数据卷,还包括一些 Docker 自己创建的数据卷。
查看数据卷详细信息:
[icexmoon@192 ~]$ docker volume inspect html
[
{
"CreatedAt": "2023-07-27T16:26:54+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/html/_data",
"Name": "html",
"Options": null,
"Scope": "local"
}
]
这里的Mountpoint
是数据卷对应的宿主机真实目录,即“挂载点”。
要使用数据卷,需要将数据卷映射到容器目录。
先删除之前启动的容器:
[icexmoon@192 ~]$ docker ps
...
[icexmoon@192 ~]$ docker stop my-nginx
my-nginx
[icexmoon@192 ~]$ docker rm my-nginx
my-nginx
重新启动新的容器:
[icexmoon@192 ~]$ docker run --name my-nginx -p 80:80 -v html:/usr/share/nginx/html -d nginx
95b882d80f1a1b6382e65041180637c6bb9d49cc3b2f6a5d48d213a028896d68
这里使用-v
参数完成数据卷到容器的映射,此时启动后的容器的/usr/share/nginx/html
目录内容将使用数据卷 html 中的内容。
换言之,现在我们再修改容器/usr/share/nginx/html
中的文件,只需要直接修改宿主机上的真实目录中的内容即可:
[icexmoon@192 ~]$ docker volume inspect html
[
{
"CreatedAt": "2023-07-27T16:26:54+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/html/_data",
"Name": "html",
"Options": null,
"Scope": "local"
}
]
[icexmoon@192 ~]$ su
密码:
[root@192 icexmoon]# cd /var/lib/docker/volumes/html/_data
[root@192 _data]# vim index.html
[root@192 _data]# exit
exit
[icexmoon@192 ~]$
- 访问数据卷的真实目录需要 root 权限,因此需要先用
su
命令提权。- 除了用
vim
工具修改文件,还可以用 Linux 主机上的图形化文本编辑器或者 IDE 远程访问的方式进行修改。
再次访问容器中的 Nginx 欢迎页面,就可以看到内容已经改变。
通过这种方式,我们可以让多个容器映射同一个数据卷:
[icexmoon@192 ~]$ docker run --name my-nginx2 -p 8080:80 -v html:/usr/share/nginx/html -d nginx
访问这个 Nginx 的欢迎页面(比如:http://192.168.0.88:8080/),同样可以看到修改后的欢迎页。
使用中的数据卷是不能被删除的:
[icexmoon@192 ~]$ docker volume rm html
Error response from daemon: remove html: volume is in use - [95b882d80f1a1b6382e65041180637c6bb9d49cc3b2f6a5d48d213a028896d68, ae74de357e3f4a251011ba342c367a927eb6f5a581af87416e3bd9a35ff56e4d]
所以需要先删除相关的容器再删除数据卷:
[icexmoon@192 ~]$ docker rm -f 95b882d80f1a1b6382e65041180637c6bb9d49cc3b2f6a5d48d213a028896d68 ae74de357e3f4a251011ba342c367a927eb6f5a581af87416e3bd9a35ff56e4d
...
[icexmoon@192 ~]$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[icexmoon@192 ~]$ docker volume rm html
html
[icexmoon@192 ~]$ docker volume ls
DRIVER VOLUME NAME
...
docker rm -f
可以强制删除运行中的容器。
特别的是,如果启动容器时,通过-v
参数指定一个不存在的数据卷进行映射,Docker 将自动创建相应的数据卷:
[icexmoon@192 ~]$ docker run --name my-nginx -p 8080:80 -v html:/usr/share/nginx/html -d nginx
8111a98d1a17080f85ad9d99605fc60488df0fd13f4974ef49c4d9c6dfadefae
[icexmoon@192 ~]$ docker volume ls
DRIVER VOLUME NAME
...
local html
示例:直接挂载目录
除了上面那样将数据卷挂载到容器,还可以直接挂载宿主机的目录和文件到容器。
创建本地目录:
[icexmoon@192 ~]$ mkdir /tmp/mysql/data -p
[icexmoon@192 ~]$ mkdir /tmp/mysql/conf -p
[icexmoon@192 ~]$ cd /tmp/mysql/conf
[icexmoon@192 conf]$ ls
hmy.cnf
[icexmoon@192 conf]$ cat hmy.cnf
[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
这里的
hmy.cnf
文件是通过远程工具上传的一个文件,包含一些自定义 MySQL 配置,内容比较简单,也可以自行创建。
我们的目标是将/tmp/mysql/data
目录挂载到 MySQL 容器的持久数据保存目录,将/tmp/mysql/conf/hmy.cnf
文件挂载到 MySQL 容器的自定义配置文件。
确保已经存在 MySQL 镜像:
server-id=1000[icexmoon@192 conf]$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 605c77e624dd 19 months ago 141MB
redis latest 7614ae9453d1 19 months ago 113MB
mysql latest 3218b38490ce 19 months ago 516MB
如果没有就拉取。
编写启动命令:
docker run \
--name mysql \
-e MYSQL_ROOT_PASSWORD=mysql \
-d \
-p 3306:3306 \
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \
-v /tmp/mysql/data:/var/lib/mysql \
mysql
用户自定义配置文件存放目录以及持久数据的存放目录都可以在 DockerHub 的镜像说明页面中找到。
执行:
[icexmoon@192 conf]$ docker run \
--name mysql \
-e MYSQL_ROOT_PASSWORD=mysql \
-d \
-p 3306:3306 \
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \
-v /tmp/mysql/data:/var/lib/mysql \
mysql
507f9f9efbd4d3710b3480af643c59e9e52a9b7142428cc5c52c1d3f4d46861e
总结
使用数据卷进行挂载的优点是由 Docker 管理数据卷对应的真实目录,不需要我们进行管理。缺点是真实目录由 Docker 创建,难以定位和理解其用途。
自行创建目录并挂载到容器的优点是,我们可以创建有意义的层级目录,可以明确其挂载的意图。缺点是目录需要由我们自己创建和管理。
自定义镜像
镜像结构
简单的说,镜像中就是包含了除 Linux 内核之外的全部应用运行所需的环境,包括 Linux 发行版的相关基本应用、所需的函数库、需要的运行环境等。
大致可以用下图表示:
Dockerfile
要从零开始构建镜像,我们需要定义一个 Dockerfile 文件,Docker 会根据这个文件一步步构建镜像。
Dockerfile 包含一系列指令:
更新详细语法说明,请参考官网文档: https://docs.docker.com/engine/reference/builder
示例:构建 Java 应用镜像
创建构建镜像的工作目录:
[icexmoon@192 ~]$ mkdir -p ./docker/build
[icexmoon@192 ~]$ cd ./docker/build/
[icexmoon@192 build]$ mkdir docker-demo
[icexmoon@192 build]$ cd docker-demo/
上传用于示例的 jar 包和 JDK 到该目录:
[icexmoon@192 docker-demo]$ ls
docker-demo.jar jdk8.tar.gz
上面的文件可以从这里获取。
创建 Dockerfile 文件:
[icexmoon@192 docker-demo]$ vim Dockerfile
其内容如下:
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local
# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar
# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8
# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
构建镜像:
[icexmoon@192 docker-demo]$ docker build -t javaweb:1.0 .
结尾的
.
表示对当前目录进行构建。
这里的-t
参数含义是tag
,即创建一个指定tag
的镜像。
查看创建好的镜像:
[icexmoon@192 docker-demo]$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
javaweb 1.0 9539979d8f15 31 seconds ago 722MB
...
启动容器:
[icexmoon@192 system]$ docker run --name jw -p 8090:8090 -d javaweb:1.0
一切 OK 的话就可以访问容器中的 java 应用了,比如我这里访问的是 http://192.168.0.88:8090/hello/count。
如果容器启动失败,自动退出。可以参考这篇文章进行排查解决。
示例:基于 Java8 构建镜像
虽然上边的构建是可行的,但实际上如果我们的 Java 应用都是基于某个版本的 JDK 进行构建(这里是 Java8),每次构建都要从底层的 Linux 发行版和 Java 环境开始构建太过麻烦,这些构建实际上都有相同的底层镜像结构,即 Linux 发行版 + JDK。所以完全可以将这些共同的底层结构创建为一个镜像,再加上我们自己的 jar 包以及启动命令即可。这样可以简化 Dockerfile 文件。
实际上类似的镜像已经存在,直接使用就可以了:
[icexmoon@192 docker-demo]$ vim Dockerfile
[icexmoon@192 docker-demo]$ cat Dockerfile
FROM java:8-alpine
COPY ./docker-demo.jar /tmp/app.jar
EXPOSE 8090
ENTRYPOINT java -jar /tmp/app.jar
[icexmoon@192 docker-demo]$ docker build -t javaweb:2.0 .
这个版本的 Dockerfile 精简了很多,底层直接从镜像 java:8-alpine
开始构建,这个镜像提供了基本的 JDK 8 环境支持。
Docker-Compose
如果我们开发的是微服务,有很多微服务需要创建成镜像并进行部署,就会很麻烦。Docker Compose 就是一个可以帮助我们一次性创建和部署多个镜像(容器)的工具。
安装
Docker Compose 的安装可以参考这篇文章。
Compose 文件
需要定义一个 Compose 文件来说明 Docker Compose 如何部署多个容器:
version: "3.8"
services:
mysql:
image: mysql:5.7.25
environment:
MYSQL_ROOT_PASSWORD: 123
volumes:
- "/tmp/mysql/data:/var/lib/mysql"
- "/tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf"
web:
build: .
ports:
- "8090:8090"
Compose 文件的格式类似于 yaml 文件。services 下的节点是多个服务(Service),这里的 web 和 mysql 都是服务名称。
Docker Compose 中的服务可以理解为容器。
Compose 服务有两种方式创建,一种是从现有镜像创建,比如image: mysql:5.7.25
,另一种是用docker build
工具从本地创建镜像并利用创建好的镜像创建容器,比如build: .
。
其他诸如可以用environment
定义环境变量,用volumes
定义数据卷挂载等,用ports
定义端口映射等。
DockerCompose的详细语法可以参考官网。
示例:部署微服务
示例中使用的 Spring Cloud 示例项目可以从这里获取。
先创建 Docker Compose 的工程目录,结构如下:
├─gateway
├─mysql
│ ├─conf
│ └─data
├─order-service
└─user-service
项目根目录下添加一个docker-compose.yml
文件:
version: "3.0"
services:
nacos:
image: nacos/nacos-server
environment:
MODE: standalone
ports:
- "8848:8848"
mysql:
image: mysql:5.7.25
environment:
MYSQL_ROOT_PASSWORD: mysql
volumes:
- "$PWD/mysql/data:/var/lib/mysql"
- "$PWD/mysql/conf:/etc/mysql/conf.d/"
user-service:
build: ./user-service
order-service:
build: ./order-service
gateway:
build: ./gateway
ports:
- "10010:10010"
这里只对外暴露了 Nacos 和 Gateway 的端口,因为微服务之间的调用是不需要对外暴露的,只通过网关对外提供服务。暴露 Nacos 端口是为了能访问 Nacos 的管理页面。
因为这里的3个微服务都是从头开始创建镜像,而不是使用现有镜像,所以需要添加Dockerfile
文件:
FROM java:8-alpine
COPY ./app.jar /tmp/app.jar
ENTRYPOINT java -jar /tmp/app.jar
每个自定义微服务下都要放一个
Dockerfile
文件,比如/user-service/Dockerfile
等。
简单起见,3个微服务的镜像构建文件内容是相同的,所以我们要让其打包成命名相同的app.jar
包。
给自定义微服务的 POM 文件添加:
<project ...>
<!-- ... -->
<build>
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
这里的 finalName 就是 Maven 打包后的 jar 包名称。
之前在项目的配置文件中,对外部服务的引用是通过指定 IP 实现的,比如:
spring:
# ...
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
现在通过 DockerCompose 部署,需要将其修改为通过服务名称访问:
spring:
# ...
datasource:
url: jdbc:mysql://mysql:3306/cloud_order?useSSL=false
Docker Compose 部署的服务之间会通过网络(Network)共享信息,所以这里利用服务名称即可访问目标服务。
使用 Maven 工具打包。
将微服务打包好后拷贝到 Docker Compose 工程目录下的对应目录。这可以编写一个 shell 脚本或 bat 批处理脚本来完成:
icexmoon@Awalon:/mnt/d/temp/shopping$ cat jar_copy.sh
#!/bin/bash
jar_home=/mnt/d/workspace/learn-spring-cloud/ch8/shopping
cp -f "${jar_home}/shopping-user/target/app.jar" ./user-service
cp -f "${jar_home}/shopping-order/target/app.jar" ./order-service
cp -f "${jar_home}/gateway/target/app.jar" ./gateway
exit 0
这里我利用 WSL 执行 shell 脚本。
现在可以将 Docker Compose 工程目录整个拷贝到 Docker 宿主机上进行容器部署:
[icexmoon@192 ~]$ cd 下载
[icexmoon@192 下载]$ ls -al
drwxr-xr-x. 6 icexmoon icexmoon 120 7月 29 11:08 shopping
[icexmoon@192 下载]$ cd shopping/
[icexmoon@192 shopping]$ ls -al
总用量 8
drwxr-xr-x. 6 icexmoon icexmoon 120 7月 29 11:08 .
drwxr-xr-x. 3 icexmoon icexmoon 57 7月 29 11:08 ..
-rw-r--r--. 1 icexmoon icexmoon 491 7月 29 11:08 docker-compose.yml
drwxr-xr-x. 2 icexmoon icexmoon 39 7月 29 11:08 gateway
-rw-r--r--. 1 icexmoon icexmoon 260 7月 29 11:08 jar_copy.sh
drwxr-xr-x. 4 icexmoon icexmoon 30 7月 29 11:08 mysql
drwxr-xr-x. 2 icexmoon icexmoon 39 7月 29 11:08 order-service
drwxr-xr-x. 2 icexmoon icexmoon 39 7月 29 11:08 user-service
[icexmoon@192 shopping]$ docker-compose up -d
启动后可以看到有多个容器已经在运行:
[icexmoon@192 shopping]$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS
NAMES
48f620bfcaeb shopping-order-service "/bin/sh -c 'java -j…" 51 minutes ago Up 2 minutes
shopping-order-service-1
97a3ba43af01 shopping-gateway "/bin/sh -c 'java -j…" 51 minutes ago Up 2 minutes 0.0.0.0:10010->10010/tcp, :::10010->10010/tcp shopping-gateway-1
f3da817f4cdd nacos/nacos-server "bin/docker-startup.…" 51 minutes ago Up 51 minutes 0.0.0.0:8848->8848/tcp, :::8848->8848/tcp shopping-nacos-1
5e843185a487 shopping-user-service "/bin/sh -c 'java -j…" 51 minutes ago Up 2 minutes
shopping-user-service-1
253c6900ca26 mysql:5.7.25
对应的,也创建了相应的镜像:
[icexmoon@192 shopping]$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
shopping-order-service latest 80c5b3ca7588 50 minutes ago 188MB
shopping-user-service latest f3f8e67152cd 50 minutes ago 188MB
shopping-gateway latest 5080456f4438 50 minutes ago 185MB
...
现在可以访问 Nacos 的管理页面,比如我这里是 http://192.168.0.88:8848/nacos/。
如果发现服务没有成功注册,可以通过以下命令查看所有服务的日志:
[icexmoon@192 shopping]$ docker-compose logs | more
不出意外的话你会看到类似连接 Nacos 失败之类的错误信息。这是因为依赖于 Nacos 的微服务先于 Nacos 服务启动,所以连接失败,且连接失败后不会再次尝试重连。所以这种情况下需要重启依赖于 Nacos 的微服务:
[icexmoon@192 shopping]$ docker-compose restart gateway user-service order-service
输入命令时可以利用自动补全。
现在应该可以在 Nacos 中看到注册的微服务了。
但此时访问接口,比如 http://192.168.0.88:10010/user/1?auth=admin 依然会出现错误,如果查看相应的日志就能发现是 MySQL 连接报错,显然数据库容器中缺少相应的表数据。所以这里还需要导入表数据。
因为 mysql 容器挂载了宿主机目录,所以我们可以将 sql 文件拷贝到这个目录下。
[icexmoon@192 shopping]$ pwd
/home/icexmoon/下载/shopping
[icexmoon@192 shopping]$ sudo cp ./db/ ./mysql/data/ -rf
[icexmoon@192 shopping]$ ls ./mysql/data/db/
cloud_order.sql cloud_user.sql
sql 文件可以从这里获取。
现在就可以进入容器并导入 sql 了:
[icexmoon@192 shopping]$ docker exec -it shopping-mysql-1 bash
root@7b7351cff3b1:/# ls -al /var/lib/mysql/db/
total 16
drwxr-xr-x. 3 mysql mysql 84 Jul 29 06:17 .
drwxr-xr-x. 8 mysql mysql 4096 Jul 29 06:18 ..
-rw-r--r--. 1 mysql mysql 1991 Jul 29 06:17 cloud_order.sql
-rw-r--r--. 1 root root 1709 Jul 29 06:17 cloud_user.sql
root@7b7351cff3b1:/# mysql -u root -p
mysql> source /var/lib/mysql/db/cloud_order.sql
mysql> source /var/lib/mysql/db/cloud_user.sql
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| cloud_order |
| cloud_user |
| db |
| mysql |
| performance_schema |
| sys |
+--------------------+
mysql> exit
现在再访问接口就能正确返回数据了:http://192.168.0.88:10010/user/1?auth=admin
{
"data": {
"id": 1,
"userName": "柳岩",
"address": "湖南省衡阳市"
},
"errorMsg": "",
"errorCode": "",
"success": true
}
可以用以下命令关闭并删除 Docker Compose 创建的容器:
[icexmoon@192 shopping]$ docker-compose down
私有镜像仓库
公司内部基于 Docker 开发的镜像一般不会上传到 DockerHub 这种公开的镜像仓库(Docker Registry),需要我们将其上传到私有镜像仓库。
官方提供的私有仓库搭建镜像是没有 UI 界面的,这里我们使用第三方开发的带 UI 界面的私有镜像仓库。
先准备一个单独的目录用于保存 Compose 文件:
[icexmoon@192 ~]$ mkdir ./docker-compose/registry-ui -p
[icexmoon@192 ~]$ cd ./docker-compose/registry-ui/
创建docker-compose.yml
文件:
[icexmoon@192 registry-ui]$ vim docker-compose.yml
内容如下:
version: '3.8'
services:
registry-ui:
image: joxit/docker-registry-ui:main
restart: always
ports:
- 8080:80
environment:
- SINGLE_REGISTRY=true
- REGISTRY_TITLE=Docker Registry UI
- DELETE_IMAGES=true
- SHOW_CONTENT_DIGEST=true
- NGINX_PROXY_PASS_URL=http://registry-server:5000
- SHOW_CATALOG_NB_TAGS=true
- CATALOG_MIN_BRANCHES=1
- CATALOG_MAX_BRANCHES=1
- TAGLIST_PAGE_SIZE=100
- REGISTRY_SECURED=false
- CATALOG_ELEMENTS_LIMIT=1000
container_name: registry-ui
registry-server:
image: registry
restart: always
environment:
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: '[http://192.168.0.88:8080]'
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods: '[HEAD,GET,OPTIONS,DELETE]'
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Credentials: '[true]'
REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers: '[Authorization,Accept,Cache-Control]'
REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers: '[Docker-Content-Digest]'
REGISTRY_STORAGE_DELETE_ENABLED: 'true'
volumes:
- ./registry-data:/var/lib/registry
container_name: registry-server
包含两个服务,registry 服务是官方的命令行镜像仓库,ui 是第三方的外挂 Web UI 服务。前者不需要对外暴露接口,后者可以通过服务名访问前者。只需要暴露后者的 80 端口给用户访问管理页面即可。
详细的配置说明可以查看 docker-registry-ui 的 Github 项目页面。
创建用于挂载镜像仓库的持久化数据的目录:
[icexmoon@192 registry-ui]$ mkdir registry-data
在用 Docker Compose 启动应用前,还需要将私有镜像仓库的服务地址加入 Docker 的信任仓库列表:
[icexmoon@192 registry-ui]$ sudo vim /etc/docker/daemon.json
[icexmoon@192 registry-ui]$ cat /etc/docker/daemon.json
{
"registry-mirrors": ["https://eqqnz5bo.mirror.aliyuncs.com"],
"insecure-registries": ["http://192.168.0.88:8080"]
}
因为私有仓库没有配置 HTTPS,所以加入 insecure-registries 列表。
需要重启 Docker 服务让配置生效:
[icexmoon@192 registry-ui]$ sudo systemctl daemon-reload
[icexmoon@192 registry-ui]$ sudo systemctl restart docker
启动应用:
[icexmoon@192 registry-ui]$ docker-compose up -d
现在可以打开私有仓库的管理页面,比如 http://192.168.0.88:8080/。不过目前是空的,不包含任何镜像。
在上传镜像到私有仓库前,需要给镜像打上私有仓库的 tag:
[icexmoon@192 registry-ui]$ docker tag nginx:latest 192.168.0.88:8080/nginx:1.0
[icexmoon@192 registry-ui]$ docker images | grep nginx
192.168.0.88:8080/nginx 1.0 605c77e624dd 19 months ago 141MB
nginx latest 605c77e624dd 19 months ago 141MB
可以看到,现在存在两个只有名称不一样,其它都一样的镜像。
上传(推送)镜像到私有镜像仓库:
[icexmoon@192 registry-ui]$ docker push 192.168.0.88:8080/nginx:1.0
推送成功后就可以在镜像仓库的管理页面看到:
点击 tag 右侧的图标还可以拷贝拉取镜像的命令到粘贴板,可以直接用该命令拉取镜像。
测试拉取镜像前,先删除本地镜像:
[icexmoon@192 registry-ui]$ docker rmi 192.168.0.88:8080/nginx:1.0 nginx:latest
[icexmoon@192 registry-ui]$ docker images | grep nginx
拉取镜像:
[icexmoon@192 registry-ui]$ docker pull 192.168.0.88:8080/nginx:1.0
[icexmoon@192 registry-ui]$ docker images | grep nginx
192.168.0.88:8080/nginx 1.0 605c77e624dd 19 months ago 141MB
The End,谢谢阅读。