Docker 实战:构建 Nginx 驱动的多游戏 Docker 镜像及 ADD/LABEL 指令详解

在现代应用部署中,Docker 已成为不可或缺的工具。它能够将应用及其依赖打包成轻量、可移植的容器镜像。本文将通过一个实战案例,演示如何利用 Dockerfile 构建一个包含多个 H5 静态网页游戏(黄金矿工、愤怒的小鸟、坦克大战)并通过 Nginx 提供服务的 Docker 镜像。我们将重点探讨 Dockerfile 的编写、ADD 指令的妙用以及如何使用 LABEL 为镜像添加规范的元数据。

学习目标:

  • 掌握编写 Dockerfile 构建包含 Nginx 和静态资源的应用镜像。
  • 深入理解 ADD 指令相较于 COPY 的特性,尤其是在处理压缩包时的自动解压功能。
  • 了解如何组织项目文件以配合 Dockerfile 构建。
  • 学会使用 LABEL 指令为 Docker 镜像添加描述性元数据。
  • 实践通过简单的 Shell 脚本自动化镜像构建和容器运行流程。

准备工作:

  1. Docker 环境: 确保你的系统中已安装并运行 Docker。
  2. 游戏文件: 准备好需要部署的游戏的压缩包文件(例如:oldboyedu-huangjinkuanggong.tar.gz, oldboyedu-killbird.tar.gz, oldboyedu-tanke.tar.gz)。这些文件应包含游戏的所有静态资源(HTML, CSS, JavaScript, 图片等)。
  3. Nginx 配置文件: 准备自定义的 Nginx 配置文件(例如:games.conf),用于配置虚拟主机或路由规则。

第一部分:构建包含“黄金矿工”和“愤怒的小鸟”的多游戏镜像

本部分我们将创建一个 Docker 镜像,该镜像基于 Alpine Linux,安装 Nginx,并将两个游戏(黄金矿工、愤怒的小鸟)的静态文件部署到 Nginx 的 Web 根目录下不同的子目录中。

1. 项目文件结构 (示例)

/oldboyedu/dockerfile/alpine/02-kuanggong-killbird/
├── Dockerfile
├── build.sh
├── games.conf
├── oldboyedu-huangjinkuanggong.tar.gz
└── oldboyedu-killbird.tar.gz

2. 编写 Dockerfile

# Dockerfile for multi-game Nginx server

# 指定轻量级的基础镜像 Alpine Linux
FROM alpine:3.20.2

# 设置维护者信息 (虽然后续会介绍LABEL,此处先按原文保留)
# MAINTAINER Your Name <your.email@example.com> # 建议使用 LABEL

# 在容器内执行命令
RUN \
    # 1. 更换 Alpine 的软件包源为国内镜像,加速下载
    sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
    # 2. 更新软件包索引
    apk update && \
    # 3. 安装 Nginx 服务
    apk add --no-cache nginx && \
    # 4. 清理不必要的缓存和 Nginx 默认示例文件,减小镜像体积
    rm -rf /var/cache/apk/* && \
    rm -f /usr/share/nginx/html/* && \
    # 5. 创建存放两个游戏的特定目录
    mkdir -p /usr/share/nginx/html/huangjinkuanggong && \
    mkdir -p /usr/share/nginx/html/killbird

# --- ADD 指令详解 ---
# ADD 指令不仅可以复制文件/目录,还具备两个特殊功能:
# 1. 如果源是 URL,它会尝试下载文件。
# 2. 如果源是一个本地的、可识别的压缩包(如 tar, gzip, bzip2, xz),
#    并且目标路径是容器内的一个目录,ADD 会自动将压缩包解压到目标目录。
# 这相当于先 COPY 压缩包到容器,然后执行 tar xf 解压,最后删除压缩包。

# 使用 ADD 将本地的黄金矿工游戏压缩包解压到指定的游戏目录
ADD oldboyedu-huangjinkuanggong.tar.gz /usr/share/nginx/html/huangjinkuanggong/

# 使用 ADD 将本地的愤怒的小鸟游戏压缩包解压到指定的游戏目录
ADD oldboyedu-killbird.tar.gz /usr/share/nginx/html/killbird/

# --- 对比 COPY 指令 ---
# 如果使用 COPY 指令,你需要手动解压:
# COPY oldboyedu-huangjinkuanggong.tar.gz /tmp/
# COPY oldboyedu-killbird.tar.gz /tmp/
# RUN tar xf /tmp/oldboyedu-huangjinkuanggong.tar.gz -C /usr/share/nginx/html/huangjinkuanggong && \
#     tar xf /tmp/oldboyedu-killbird.tar.gz -C /usr/share/nginx/html/killbird && \
#     rm -f /tmp/oldboyedu-huangjinkuanggong.tar.gz /tmp/oldboyedu-killbird.tar.gz
# 可见 ADD 在此场景下简化了 Dockerfile。
# 最佳实践:除非确实需要 ADD 的自动解压或 URL 下载功能,否则优先使用 COPY,因为它行为更明确、可预测。

# 将本地准备好的 Nginx 配置文件复制到容器内 Nginx 的配置目录下,覆盖默认配置
COPY games.conf /etc/nginx/http.d/default.conf

# 暴露端口 (可选,因为下面用了 --network host,但写上更规范)
# EXPOSE 80

# 定义容器启动时执行的命令
# 使用前台模式运行 Nginx,这是 Docker 容器中运行服务的标准方式
CMD ["nginx", "-g", "daemon off;"]

3. 编写 Nginx 配置文件 (games.conf)

此配置文件需要根据你的访问需求设定,例如使用不同的 server_namelocation 块来区分游戏。

# /oldboyedu/dockerfile/alpine/02-kuanggong-killbird/games.conf

server {
    listen 80 default_server; # 监听 80 端口
    server_name _; # 接受任何主机名的请求

    # 黄金矿工 访问路径: http://<host_ip>/huangjinkuanggong/
    location /huangjinkuanggong/ {
        alias /usr/share/nginx/html/huangjinkuanggong/;
        index index.html index.htm;
        try_files $uri $uri/ /huangjinkuanggong/index.html; # 适应单页应用或 H5 路由
    }

    # 愤怒的小鸟 访问路径: http://<host_ip>/killbird/
    location /killbird/ {
        alias /usr/share/nginx/html/killbird/;
        index index.html index.htm;
        try_files $uri $uri/ /killbird/index.html; # 适应单页应用或 H5 路由
    }

    # 可以添加根路径或其他配置
    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
        # 可以放一个简单的欢迎页或导航页在 /usr/share/nginx/html/index.html
    }

    # 可选:日志配置
    access_log /var/log/nginx/host.access.log main;
    error_log /var/log/nginx/error.log warn;
}

注意:实际的 games.conf 需要根据你的游戏文件结构和访问方式调整 locationroot/alias 配置。

4. 编写自动化编译脚本 (build.sh)

这个脚本简化了构建、清理旧容器和运行新容器的过程。

#!/bin/bash

# /oldboyedu/dockerfile/alpine/02-kuanggong-killbird/build.sh

# 设置镜像标签,如果执行脚本时没有提供参数,默认为 4
TAG=${1:-4}
IMAGE_NAME="linux92-games"
IMAGE_TAG="v2.${TAG}"

echo "Building image ${IMAGE_NAME}:${IMAGE_TAG}..."
# 使用当前目录作为构建上下文构建镜像
docker image build -t ${IMAGE_NAME}:${IMAGE_TAG} .

# 检查构建是否成功
if [ $? -ne 0 ]; then
  echo "Error: Docker image build failed."
  exit 1
fi

echo "Removing existing containers (if any)..."
# 尝试停止并删除名为 "games" 的旧容器 (如果存在)
docker container stop games > /dev/null 2>&1
docker container rm games > /dev/null 2>&1
# 清理所有已停止的容器 (更通用的清理方式)
# docker container prune -f

echo "Running new container 'games'..."
# 以后台模式运行新容器,命名为 "games"
# 使用 --network host 使容器共享宿主机的网络栈,可以直接通过宿主机 IP 访问容器的 80 端口
# 注意:--network host 模式方便调试,但在生产环境中可能有安全风险和端口冲突问题,建议优先使用端口映射 (-p 80:80)
docker container run -d --name games --network host ${IMAGE_NAME}:${IMAGE_TAG}

echo "Verifying container status..."
# 显示最近启动的容器信息
docker container ps -l

echo "Build and run complete. Access games at http://<your_host_ip>/huangjinkuanggong/ and http://<your_host_ip>/killbird/"

5. 执行编译和运行

02-kuanggong-killbird 目录下执行:

chmod +x build.sh
./build.sh 6 # 使用标签 v2.6

你会看到 Docker 的构建过程,每个 Dockerfile 指令都会生成一个中间层(可以通过 docker image history <image_name> 查看)。脚本最后会启动名为 games 的容器。

6. 观察镜像层与测试访问

  • 镜像层: 如你原文所示,使用 docker image ls -a | grep <layer_id> 可以看到构建过程中产生的中间层镜像。这展示了 Docker 的分层构建机制,有助于缓存和效率。最终的 linux92-games:v2.6 是所有层的叠加。
  • 测试: 打开浏览器,访问 http://<你的Docker主机IP>/huangjinkuanggong/http://<你的Docker主机IP>/killbird/,应该能看到对应的游戏界面。如果使用了主机名(如 game01.oldboyedu.com),确保在访问机器的 hosts 文件中添加了相应的 IP 映射,或者配置了 DNS。

第二部分:使用 LABEL 添加镜像元数据并优化项目结构

在实际项目中,良好的组织结构和清晰的镜像信息至关重要。这部分我们将构建一个只包含“坦克大战”游戏的镜像,并演示更规范的项目布局以及如何使用 LABEL 指令替代已废弃的 MAINTAINER

1. 优化后的项目文件结构

将代码、配置、脚本分门别类存放,使项目更清晰。

/oldboyedu/dockerfile/alpine/03-tankedazhan-MAINTAINER-LABELS/
├── code/
│   └── oldboyedu-tanke.tar.gz
├── config/
│   └── games.conf
├── scripts/
│   └── build.sh
└── Dockerfile

2. 编写 Dockerfile (使用 LABEL)

# Dockerfile for Tank Game with Metadata

# 指定基础镜像
FROM alpine:3.20.2

# 使用 LABEL 添加元数据 (推荐方式)
LABEL maintainer="JasonYin <y1053419035@qq.com>" \
      org.opencontainers.image.authors="JasonYin <y1053419035@qq.com>" \
      org.opencontainers.image.vendor="OldboyEdu" \
      org.opencontainers.image.title="Tank Game Server" \
      org.opencontainers.image.description="Docker image running Nginx to serve the Tank web game." \
      school="oldboyedu" \
      class="linux92" \
      project="Tank Game Deployment" \
      version="2.0"

# 弃用的 MAINTAINER 指令 (仅作演示对比,新项目不应使用)
# MAINTAINER JasonYin y1053419035@qq.com www.oldboyedu.com xixi haha

# 安装 Nginx 并创建游戏目录 (与之前类似)
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
    apk update && \
    apk add --no-cache nginx && \
    rm -rf /var/cache/apk/* && \
    rm -f /usr/share/nginx/html/* && \
    mkdir -p /usr/share/nginx/html/tanke

# 使用 ADD 从 code/ 子目录添加并解压游戏文件
# 注意:源路径是相对于构建上下文 (context) 的路径
ADD code/oldboyedu-tanke.tar.gz /usr/share/nginx/html/tanke/

# 使用 COPY 从 config/ 子目录复制 Nginx 配置文件
COPY config/games.conf /etc/nginx/http.d/default.conf

# 暴露端口 (可选)
# EXPOSE 80

# 容器启动命令
CMD ["nginx", "-g", "daemon off;"]

3. Nginx 配置文件 (config/games.conf)

这次配置为坦克大战游戏服务。

# /oldboyedu/dockerfile/alpine/03-tankedazhan-MAINTAINER-LABELS/config/games.conf
server {
    listen        0.0.0.0:80;
    server_name   game01.oldboyedu.com; # 或者使用 _ 作为默认服务器
    root          /usr/share/nginx/html/tanke; # Web 根目录指向坦克游戏
    index         index.html index.htm;

    location / {
        try_files $uri $uri/ /index.html; # 标准 H5 应用路由处理
    }

    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log warn;
}

4. 更新编译脚本 (scripts/build.sh)

注意 docker image build 命令中的构建上下文路径变为 ..,因为脚本在 scripts 目录下,而 Dockerfile 和其他资源在上一级目录。

#!/bin/bash

# /oldboyedu/dockerfile/alpine/03-tankedazhan-MAINTAINER-LABELS/scripts/build.sh

# 设置镜像标签,如果执行脚本时没有提供参数,默认为 7
TAG=${1:-7}
IMAGE_NAME="linux92-games" # 可以改成更具体的名字,如 linux92-tank-game
IMAGE_TAG="v2.${TAG}"

echo "Building image ${IMAGE_NAME}:${IMAGE_TAG} from context '../'..."
# 使用父目录作为构建上下文 ('..')
docker image build -t ${IMAGE_NAME}:${IMAGE_TAG} ..

if [ $? -ne 0 ]; then
  echo "Error: Docker image build failed."
  exit 1
fi

echo "Removing existing containers (if any)..."
docker container stop games > /dev/null 2>&1
docker container rm games > /dev/null 2>&1

echo "Running new container 'games'..."
# 以后台模式运行,命名为 "games",使用 host 网络
docker container run -d --name games --network host ${IMAGE_NAME}:${IMAGE_TAG}

echo "Verifying container status..."
docker container ps -l

echo "Build and run complete. Access game at http://game01.oldboyedu.com (ensure hosts mapping) or http://<your_host_ip>/"

5. 执行编译和运行

进入 scripts 目录执行:

cd /oldboyedu/dockerfile/alpine/03-tankedazhan-MAINTAINER-LABELS/scripts
chmod +x build.sh
./build.sh 8 # 使用标签 v2.8

6. 查看镜像元数据与测试

  • 查看 MAINTAINER (Author): (虽然不推荐,但为了完整性)

    docker image inspect -f "{{.Author}}" linux92-games:v2.8
    # 输出: JasonYin y1053419035@qq.com www.oldboyedu.com xixi haha
    # 注意:inspect .Author 会显示 MAINTAINER 指令的完整内容
    
  • 查看 LABEL:

    # 查看所有 Labels
    docker image inspect -f "{{json .Config.Labels}}" linux92-games:v2.8 | jq . # 使用 jq 美化输出
    
    # 查看特定 Label (例如作者)
    docker image inspect -f "{{.Config.Labels.auther}}" linux92-games:v2.8
    # 输出: JasonYin (这里原文Dockerfile的label key是auther,应为author或使用标准如org.opencontainers.image.authors)
    
    docker image inspect -f '{{index .Config.Labels "org.opencontainers.image.authors"}}' linux92-games:v2.8
    # 输出: JasonYin <y1053419035@qq.com>
    

    LABEL 提供了更结构化、更丰富的元数据存储方式,易于查询和自动化处理。推荐使用 OCI (Open Container Initiative) 标准标签(如 org.opencontainers.image.*)以增强互操作性。

  • 测试: 访问 http://game01.oldboyedu.com (需配置 hosts 文件) 或 http://<你的Docker主机IP>,应该能玩坦克大战游戏了。


总结与最佳实践

  • Dockerfile 结构: 保持清晰,合理组织指令顺序以利用 Docker 的构建缓存。将不变的指令(如 FROM, RUN apt-get install)放在前面,经常变动的文件复制(如 COPY/ADD 应用代码)放在后面。
  • ADD vs COPY: 理解 ADD 的自动解压和 URL 下载特性。优先使用 COPY,因为它行为更单一、明确。仅在确实需要 ADD 的特殊功能时才使用它。
  • 减小镜像体积: 使用 .dockerignore 文件排除不必要的文件和目录进入构建上下文;在 RUN 指令中合并多个命令,并在同一层清理缓存(如 apt-get clean, rm -rf /var/cache/apk/*);选择 Alpine 等轻量级基础镜像;考虑使用多阶段构建(Multi-stage builds)来分离构建环境和运行时环境。
  • 元数据: 使用 LABEL 指令为镜像添加详细的元数据,遵循 OCI 等标准标签规范,弃用 MAINTAINER
  • 项目组织:Dockerfile 与源代码、配置文件、脚本等分开存放,使项目结构清晰,方便管理和构建。
  • 网络模式: 理解 --network host 的便利性和潜在风险,生产环境通常推荐使用端口映射 (-p host_port:container_port) 以提供更好的隔离性。
  • 自动化: 利用 Shell 脚本或其他自动化工具简化构建、测试和部署流程。

通过本文的实践,你不仅学会了如何构建一个服务多个静态资源的应用镜像,还深入了解了 ADD 指令的特性和 LABEL 的重要性。希望这些知识能帮助你在 Docker 的世界里更进一步!

posted on 2025-04-10 08:50  Leo-Yide  阅读(196)  评论(0)    收藏  举报