写在前面
应用部署很麻烦?哪里麻烦了啊!这么多年都是这样搞得,不要睁着眼睛乱说,有的时候自己找找原因,这么多年Linux命令敲得熟不熟,有没有认真工作?(手动狗头)

需求:我最近开发了一个前后端分离的GPU预约系统,这个系统涉及Redis数据库、MySQL数据库、Java环境、Nginx,这么多的东西如果直接部署到服务器上,其复杂度可想而知,何况还要处理各种版本冲突的问题。在这个需求背景下,使用Docker来简化部署就是自然而然的事情了。

下面记录我的Docker学习笔记,主要讲述Docker的基础用法,Docker的功能远不止于此。

Docker 安装和镜像加速

卸载旧版docker:

yum remove docker \
    docker-client \
    docker-client-latest \
    docker-common \
    docker-latest \
    docker-latest-logrotate \
    docker-logrotate \
    docker-engine

配置docker的yum库:

yum install -y yum-utils # 安装yum工具
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo # 配置docker的yum源

安装docker:

yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

启动和校验:

# 启动Docker
systemctl start docker
# 停止Docker
systemctl stop docker
# 重启
systemctl restart docker
# 设置开机自启
systemctl enable docker
# 执行docker ps命令,如果不报错,说明安装启动成功
docker ps

配置镜像加速:阿里云--产品--容器镜像服务ACR,找到镜像工具下的镜像加速器。

# 创建目录
mkdir -p /etc/docker

# 复制内容,注意把其中的镜像加速地址改成你自己的
tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://xxxx.mirror.aliyuncs.com"]
}
EOF

# 重新加载配置
systemctl daemon-reload
# 重启Docker
systemctl restart docker

docker run命令:以部署MySQL数据库为例

当我们利用Docker安装应用时,Docker会自动搜索并下载应用镜像。Docker在运行镜像时候会创建一个隔离环境,我们称之为容器

什么是镜像
镜像包含应用本身,以及应用所需要的环境、配置和系统函数库。

镜像和容器的关系
镜像只需要下载一次,可以启动多个应用,比如上面的命令,改个端口3307后,可以再跑一个MySQL,而且不用再下载镜像。

官方镜像仓库:hub.docker.com 可以类比maven和各种包管理器。

Docker命令的执行过程

  • docker run实际上是客户端,执行这条命令时候docker会向服务端通过RESTAPI发送请求,服务端由 docker daemon守护进程、容器、镜像组成。

下面以运行MySQL容器的命令进行分析:

docker run -d \
--name mysql \
-p 3306:3306 \
-e TZ=Asia/Shanghai \
-e MYSQL_ROOT_PASSWORD=123 \
mysql

解读:

  • docker run : 创建 + 运行一个容器
  • -d后台运行
  • --name指定唯一的容器名
  • -p 端口映射
    • 3306 : 3306宿主机端口:容器内部端口
    • 容器内部的网络结构是对外不可访问的。可以通过 docker inspect 容器名 来查看。
  • -e KEY=VALUE设置环境变量 由镜像决定
    • 可以通过hub.docker.com 查看不同镜像的环境变量
  • mysql : 运行镜像的名称
    • 完整写法: repository:tag,如 mysql:5.7
    • 如果不写,默认是repository:latest

Docker常见命令

镜像相关

  • 下载镜像:docker pull
  • 查看本地镜像:docker images
  • 删除本地镜像:docker rmi
  • 构建镜像,通过Dockerfile:docker build
  • 将镜像保存到本地:docker save
  • 加载镜像到本地:docker load(用的少)
  • 推送镜像到仓库:docker push

容器相关

  • 创建并运行容器:docker run,会创建容器
  • 停止容器:docker stop停止容器内部的进程,容器还在。
  • 启动容器:docker start启动容器内部的进程,不会创建容器,如果乱用docker run,会重复创建容器
  • 查看容器运行状态:docker ps
  • 删除容器:docker rm
  • 查看日志:docker logs
  • 执行命令进入到容器内部:docker exec

{% image https://obj.cagurzhan.cn/blog/post/docker/1.png, width=400px %}

案例:拉取Nginx镜像,创建并运行Nginx容器

# 需求1:拉取Nginx
docker pull nginx
docker images # 查看所有镜像
docker save -o nginx.tar nginx:latest # 打包
docker rmi nginx # 删除镜像
docker load -i nginx.tar # 重新加载镜像
docker run -d --name nginx -p 80:80 nginx # 运行
docker stop nginx # 暂停容器
docker ps # 查看运行中容器
docker ps -a # 查看所有容器
docker start nginx # 开启容器
docker ps
docker logs nginx # 查看日志
docker logs -f nginx # 以follow模式,一直查看日志
# 进入容器内部 -it 可交互终端 
docker exec -it nginx bash 
# 退出
exit 
# *进入容器内部MySQL
docker exec -it mysql mysql -uroot -p
# 运行中容器不能删
docker stop mysql2
docker rm mysql2
# 也可以强制删除
docker rm mysql2 -f

进入容器内部,模拟了一台Linux文件系统,我们可以使用 docker exec -it 容器名 bash进入。

Linux小技巧:.bashrc

vi ~/.bashrc

# 添加别名
alias dps='docker ps --format "table {{.ID}}\t{{.Images}}\t{{.Ports}}\t{{.Status}}\t{{.Names}}"'

# 退出vi后,让配置文件生效
source ~/.bashrc

数据卷

需求引入:Nginx部署静态资源

我们要通过Nginx部署静态资源,都需要将HTML文件放到了 /usr/share/nginx/html目录下,但是难道我们每次都要通过exec命令进入容器?这显然是不现实的。

什么是数据卷(volume)

数据卷是一个虚拟目录,是容器内目录和宿主机目录之间的映射桥梁
一旦我们实现数据卷的挂载,就会进行目录的双向绑定

数据卷命令

  • 创建数据卷:docker volume create
  • 查看所有数据卷:docker volume ls
  • 删除指定数据卷:docker volume rm
  • 查看某个数据卷的详情:docker volume inspect
  • 清楚数据卷:docker volume prune

实际上,这些命令很少使用。我们一般在执行docker run命令的时候一并时间挂载。值得注意的是,数据卷的挂载是在容器创建的时候才能进行,如果容器已经创建了,是无法进行挂载的。

在docker run命令中,我们使用 -v 数据卷:容器内目录来实现挂载。

检验:使用 docker volume inspect 数据卷 可以查看详细信息。

# 删除原来容器
docker rm nginx -f
# 创建容器并挂载数据卷
docker run -d --name nginx -p 80:80 -v html:/usr/share/nginx/html  nginx

案例2:MySQL容器的数据挂载

查看是否有数据卷挂载:

docker inspect mysql

发现有MySQL默认有数据卷挂载,但是数据卷的Name是一大串看不懂的字母。实际上,这种由容器自己创建的卷,叫做匿名卷。那么我们如何将数据卷挂载到自己指定的目录下?

其实还是 docker run -v,只是语法不同而已。之前我们使用 docker run -v html:xxx表示数据卷名字为html,这个时候会创建一个叫做html的数据卷。但如果我们加上 /,即 docker run -v /html:xxx,就会在html目录下挂载数据卷。一个符号的差别,效果是完全不同的!

回到MySQL,通过查看官方文档,下面我们分别在本地创建data init conf分别表示MySQL的数据、初始化脚本以及MySQL的配置文件。

docker run -d \
--name mysql \
-p 3306:3306 \
-e TZ=Asia/Shanghai \
-e MYSQL_ROOT_PASSWORD=123 \
-v /root/mysql/data:/var/lib/mysql \
-v /root/mysql/init:/docker-entrypoint-initdb.d \
-v /root/mysql/conf:/etc/mysql/conf.d \
mysql:5.7

Dockerfile

Dockerfile用于构建自己的镜像。构建镜像过程,就是将应用程序、系统函数库、运行配置等文件进行打包的过程。下面以构建一个自己的Java应用镜像为例。

镜像镜像结构

  • 基础镜像BaseImage:应用依赖的系统函数库、环境和配置等。如Ubuntu镜像。
  • 层Layer:可以理解为添加安装包、依赖和配置的一系列步骤。如安装JRE、配置JRE环境变量、拷贝Jar包、设置启动脚本。
  • 入口EntryPoint:镜像运行入口。一般是启动脚本。如java -jar xxx

什么是Dockerfile
Dockerfile就是一个文本文件,这个文本文件包含一个个的指令,用指令来说明我们要执行什么操作来构建镜像,这就相当于一个菜谱!!

常见命令

  • FROM:指定基础镜像。FROM centos:6
  • ENV:设置环境变量。ENV key value
  • COPY:拷贝本地文件到容器目录。COPY ./gpu.jar app.jar
  • RUN:执行SHELL命令。
  • EXPOSE:指定容器的监听端口。EXPOSE 8080
  • ENTRYPOINT:指定容器入口。ENTRYPOINT java -jar xxx.jar

案例

FROM ubuntu:16.04
# 环境变量
ENV JAVA_DIR=/usr/local
# 拷贝jdk和jar包
COPY ./jdk8.tar $JAVA_DIR
COPY ./gpu.jar /tmp/app.jar
# 安装JDK
RUN cd $JAVA_DIR \ && tar -xf ./jdk8.tar \ && mv ./jdk1.8.0_144 ./java8
# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin
# 入口
ENTRYPOINT ["java", "-jar", "/app.jar"]

当然我们并不需要每次都从最基础的镜像开始搭建起来,前面已经说了,这只是一些步骤的集合,那么这些繁琐的搭建环境的步骤已经有人总结为镜像了,以上Dockerfile可以简化成:

FROM openjdk:11-0-jre-buster
# 配置时区为东八区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
COPY gpu.jar /app.jar
ENTRYPOINT ["java", "-jar","/app.jar"]

编写好Dockerfile后,利用下面命令构建自己的镜像:

docker build -t myImage:1.0 .

容器网络互连

我们刚才构建了nginx容器和MySQL容器,现在使用 docker inspect nginx docker inspect mysql查看容器内部,发现容器都有相同的内网IP字段。172.17.0.x

默认情况下,所有容器都是以bridge方式连接到Docker的一个虚拟网桥上,默认有一个docker0的虚拟网卡,172.17.0.1/16,16表示IP地址前两位是不能动的。所有容器都会以桥接的形式和docker0进行连接。

我们可以尝试进入一个容器,会发现他可以ping通另外一个容器,所以,如果我要进行容器之间的通信,我们可以直接对另外一个容器进行访问?

实际上这是不可以,假设服务器重新启动,这些内网IP可能会变化!解决此问题就需要用到自定义网络了。

常见网络命令

docker network ls # 查看网络
docker network create gpu # 创建网络
ip addr # 查看网卡
docker network connect gpu mysql # 将容器接入网络
docker run -d --name dd -p 8080:8080 --network gpu docker-demo # 创建容器就接入网络
docker inspect dd # 查看容器网络
docker exec -it dd bash 
ping mysql # 用容器名访问

Docker Compose

Docker Compose 通过一个单独的docker-compose.yml文件来丁艺彝族相关联的容器应用,帮助我们实现多个相互关联docker容器的快速部署。

这里通过我的前后端分离GPU预约项目的docker-compose.yml文件进行学习:

version: "3.8" # docker compose版本,不修改

services: # 服务,每个服务是一个容器
  mysql: # MySQL服务
    image: mysql:5.7 # 镜像名
    container_name: gpu-mysql # 容器名
    ports: # 端口映射,可以多个
      - "7779:3306"
    environment: # 环境变量
      TZ: Asia/Shanghai
      MYSQL_ROOT_PASSWORD: gpu_monitor_7779
    volumes: # 数据卷
      - "./mysql/conf:/etc/mysql/conf.d"
      - "./mysql/data:/var/lib/mysql"
      - "./mysql/init:/docker-entrypoint-initdb.d"
    networks: # 网络
      - gpu-monitor-net
  redis:
    image: redis
    container_name: gpu-redis
    ports:
      - "7776:6379"
    networks:
      - gpu-monitor-net
  gpu-java:
    build:  # 构建
      context: . # 当前目录下
      dockerfile: Dockerfile
    container_name: gpu-java
    ports:
      - "7777:7777"
    networks:
      - gpu-monitor-net
    depends_on:
      - mysql
  nginx:
    image: nginx
    container_name: nginx
    ports:
      - "80:80"
    volumes:
      - "./nginx/nginx.conf:/etc/nginx/nginx.conf"
      - "./nginx/html:/usr/share/nginx/html"
    depends_on:
      - gpu-java
    networks:
      - gpu-monitor-net
networks: # 创建网络
  gpu-monitor-net:  # 我理解为网络别名
    name: gpu-java # 网络名

一键部署:

docker compose up  -d

停止:

docker compose down # 会删除所有容器

命令:

  • up 创建并启动所有服务容器
  • down 停止并删除所有容器
  • ps 列出所有启动
  • logs 日志
  • stop 暂停
  • start 启动
  • restart 重启
  • top 查看运行进程
  • exec 在指定容器中执行命令
  • -f 指定compose文件路径
  • -p 指定项目名

后记

本篇主要介绍docker的基础用法,后面如果实际需求中需要用到docker更高级的用法,或者有更优的Dockerfile文件模板和docker-compose实现方案,我都会发布新的博客进行更新。