1. 引言

在现代软件开发中,经常会遇到这样的场景:一个正在运行的 Web 应用用到了 Vue、Java 8、Java 15、Tomcat、Nginx、PHP、MySQL 和 Redis。如果要将这个应用迁移到一台新的服务器上运行,就需要在新机器上重新安装所有软件并配置环境变量,这是非常痛苦的过程。

Docker 的出现完美解决了这个问题。通过容器化技术,我们可以将应用及其运行环境打包成镜像,实现”一次构建,到处运行”的目标。而 docker-compose 则进一步简化了多容器应用的部署和管理。

2. 环境安装

2.1 安装 Docker

在 Linux 系统上,可以使用官方提供的安装脚本快速安装 Docker:

# 下载并执行 Docker 安装脚本
curl -fsSL https://get.docker.com | sh

# 启动 Docker 服务
sudo systemctl start docker

# 设置 Docker 开机自启
sudo systemctl enable docker

# 将当前用户添加到 docker 组(避免每次都需要 sudo)
sudo usermod -aG docker $USER

# 验证安装
docker --version

2.2 安装 docker-compose

docker-compose 是 Docker 官方的编排工具,用于定义和运行多容器应用:

# 下载 docker-compose
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

# 赋予执行权限
chmod +x /usr/local/bin/docker-compose

# 验证安装
docker-compose --version

对于 Windows 和 macOS 用户,推荐安装 Docker Desktop,它已经包含了 Docker 和 docker-compose。

3. 核心概念

在深入使用 Docker 之前,需要理解几个核心概念。

3.1 镜像(Image)

Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的配置参数(如环境变量、用户等)。

镜像具有以下特点:

  • 镜像是只读的,构建后内容不会改变
  • 采用分层存储架构,便于复用和定制
  • 可以理解为应用的”备份”或”快照”

Docker 利用 Union FS 技术,将镜像设计为分层存储架构。每一层构建完就不会再发生改变,后一层的改变只发生在自己这一层。

3.2 容器(Container)

容器是镜像的运行实例。镜像是静态的定义,容器是镜像运行时的实体。

容器的特点包括:

  • 容器的实质是进程,但运行在独立的命名空间中
  • 拥有自己的 root 文件系统、网络配置、进程空间等
  • 容器存储层的生命周期与容器一样,容器销毁时数据也会丢失

镜像与容器的关系类似于面向对象编程中的类与实例,镜像是类,容器是实例。

3.3 仓库(Repository)

仓库是存储和分发镜像的服务。一个 Docker Registry 可以包含多个仓库,每个仓库可以包含多个标签(Tag)。

命名规范:

  • 完整格式:<仓库名>:<标签>
  • 示例:ubuntu:20.04nginx:latest
  • 省略标签时默认使用 latest

4. 创建镜像

Docker 镜像的构建是通过读取 Dockerfile 文件来完成的,它本质是一个文本文件,其内包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

4.1 编写配置文件

以开源项目的后端服务为例,在项目根目录创建 Dockerfile

# 指定基础镜像
FROM tomcat:9.0.41-jdk8-openjdk

# 复制 WAR 包到 Tomcat 的 webapps 目录
COPY ./chat-system-server.war /usr/local/tomcat/webapps/

# 复制配置文件
COPY ./tomcat/conf/server.xml /usr/local/tomcat/conf/server.xml

# 声明运行时端口
EXPOSE 8080

指令说明:

  • FROM 指定基础镜像
  • COPY 复制文件到镜像内
  • EXPOSE 声明服务运行时的端口号

4.2 常用指令

指令 用途 示例
ADD 从 URL 获取文件并放到目标路径 ADD app.tar.gz /app/
RUN 执行命令行命令 RUN apt-get update
CMD 容器启动时运行的程序 CMD ["java", "-jar", "app.jar"]
ENV 设置环境变量 ENV PATH=/usr/bin:$PATH
WORKDIR 指定工作目录 WORKDIR /app

注意: 如果需要执行多个类似于 RUN 的指令时,请用 && 来拼接,避免创建多层镜像:

RUN apt-get update && \
    apt-get install -y gcc && \
    apt-get clean

4.3 构建镜像

打开终端,进入 Dockerfile 文件所在目录,执行构建命令:

docker build -t chat-system-server:1.0.0 -f Dockerfile .

参数说明:

  • -t 指定容器名
  • -f 指定配置文件(可选,默认使用 Dockerfile)
  • . 表示当前目录,指定构建镜像的上下文路径

5. 启动容器

启动容器有两种方式:一种是基于镜像新建一个容器并启动,另一种是启动一个处于终止状态的容器。

5.1 新建并启动

使用 docker run 镜像名 即可创建一个容器并启动它:

# 基本启动
docker run chat-system-server:1.0.0

容器启动后,你会发现通过镜像中声明的 8080 端口访问不了。这是因为容器启动后没有做端口映射,需要在启动命令中添加 -p 参数:

# 端口映射
docker run -p 127.0.0.1:8080:8080 chat-system-server:1.0.0

5.2 常用参数

后台运行:

docker run -d -p 127.0.0.1:8080:8080 chat-system-server:1.0.0

为容器命名:

docker run --name local_chat_system_server -d -p 127.0.0.1:8080:8080 chat-system-server:1.0.0

启动已终止容器:

docker container start 容器名

5.3 容器管理

# 终止容器
docker container stop 容器名

# 删除容器
docker container rm 容器名

# 进入容器
docker exec -it 容器名 bash

5.4 数据挂载

容器内存储的数据会随着容器的终止而丢失,需要挂载数据卷来实现数据的持久化存储。通常有两种做法:

数据卷:

# 创建数据卷
docker volume create chat-system-data

# 启动容器并挂载数据卷
docker run -d --name local_chat_system_server \
    --mount source=chat-system-data,target=/usr/local/data \
    chat-system-server:1.0.0

目录映射(推荐):

docker run -d --name local_chat_system_server \
    -v /host/path:/container/path \
    chat-system-server:1.0.0

目录映射的形式会把指定的主机路径与容器内的目标路径做关联,本地主机做的操作会响应到容器内,反之亦然。

6. 编排容器

现在回到文章开头所说的那个场景。如果全部打包到一个镜像里,后期维护与扩展将成为恶梦。一般这种场景我们都会使用 Docker Compose 来实现。

简而言之,Docker Compose 的作用就是将多个独立的容器组合起来,让容器之间可以轻易地互相访问,最终实现我们的需求。

6.1 编写配置文件

容器的编排是通过编写 docker-compose.yml 配置文件来实现的,一般我们会将这个文件创建在项目的根目录。配置文件包含以下主要部分:

  • version - 指定 Docker Compose 文件的格式版本
  • networks - 用于自定义网络
  • services - 定义各种服务(MySQL、Redis、Nginx 等)

6.2 定义网络

在物理机上部署服务时,多个服务之间相互访问需要处于同一个网关下。在 docker-compose 中流程也是一样的,因此我们需要先定义一个网络:

networks:
  app-network:
    external: true
    name: app-network
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.30.0/24
          gateway: 192.168.30.1

网络配置说明:

  • 192.168.30.0/24 表示从 192.168.30.1 到 192.168.30.254 的 IP 地址范围
  • gateway 指定网关地址为 192.168.30.1
  • driver: bridge 指定网络连接模式为桥接

6.3 定义服务

我们可以在 services 指令下定义需要的服务,为它们连接网络、挂载数据卷、设置时区、定义访问端口等。以 MySQL 为例:

services:
  mysql:
    image: mysql:5.7.42
    container_name: local_mysql
    volumes:
      - /host/mysql_data:/var/lib/mysql
      - /host/mysql_conf/my.cnf:/etc/my.cnf
    ports:
      - 3306:3306
    networks:
      app-network:
        ipv4_address: 192.168.30.11
    environment:
      - MYSQL_ROOT_PASSWORD=xxxx
      - TZ=Asia/Shanghai

配置说明:

  • mysql - 服务名称
  • image - 镜像名称
  • container_name - 容器名称
  • volumes - 挂载的数据卷
  • ports - 端口映射
  • networks - 服务接入的网络和分配的 IP 地址
  • environment - 环境变量设置

通过这几行配置,我们就拥有了一个 MySQL 服务,其他服务可以通过 192.168.30.11:3306 访问到这个服务。

6.4 完整示例

以下是一个包含多个服务的完整配置示例:

version: '3.8'

networks:
  app-network:
    driver: bridge
    ipam:
      config:
        - subnet: 192.168.30.0/24
          gateway: 192.168.30.1

services:
  mysql:
    image: mysql:5.7.42
    container_name: local_mysql
    volumes:
      - ./mysql_data:/var/lib/mysql
    ports:
      - 3306:3306
    networks:
      app-network:
        ipv4_address: 192.168.30.11
    environment:
      - MYSQL_ROOT_PASSWORD=rootpassword
      - TZ=Asia/Shanghai

  redis:
    image: redis:6-alpine
    container_name: local_redis
    ports:
      - 6379:6379
    networks:
      app-network:
        ipv4_address: 192.168.30.12
    environment:
      - TZ=Asia/Shanghai

  app-backend:
    image: tomcat:9.0.41-jdk8-openjdk
    container_name: chat_system_server
    ports:
      - 8080:8080
    volumes:
      - ./webapps:/usr/local/tomcat/webapps
      - ./data:/usr/local/data
    environment:
      - TZ=Asia/Shanghai
    networks:
      app-network:
        ipv4_address: 192.168.30.13
    depends_on:
      - mysql
      - redis

  nginx:
    image: nginx:1.18.0
    container_name: local_nginx
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./nginx_config:/etc/nginx
    environment:
      - TZ=Asia/Shanghai
    networks:
      - app-network
    depends_on:
      - app-backend

注意: 上面配置中使用了 depends_on 指令定义服务的启动顺序,确保数据库先启动。

6.5 启动服务

在终端通过以下命令启动所有定义的服务:

# 启动所有服务
docker-compose up

# 后台启动
docker-compose up -d

# 停止服务
docker-compose down

6.6 环境变量管理

实际使用时,本地路径一般会通过变量的形式注入:

# 在 .env 文件中定义
MY_VOLUME_PATH=/path/to/your/volume

# 在 docker-compose.yml 中使用
volumes:
  - ${MY_VOLUME_PATH:-/default/path}/webapps:/usr/local/tomcat/webapps

启动时传入变量:

MY_VOLUME_PATH=/path/to/your/volume docker-compose up

7. 参考资源