Docker 实战:构建 Nginx 驱动的多游戏 Docker 镜像及 ADD/LABEL 指令详解
在现代应用部署中,Docker 已成为不可或缺的工具。它能够将应用及其依赖打包成轻量、可移植的容器镜像。本文将通过一个实战案例,演示如何利用 Dockerfile 构建一个包含多个 H5 静态网页游戏(黄金矿工、愤怒的小鸟、坦克大战)并通过 Nginx 提供服务的 Docker 镜像。我们将重点探讨 Dockerfile 的编写、ADD 指令的妙用以及如何使用 LABEL 为镜像添加规范的元数据。
学习目标:
- 掌握编写
Dockerfile构建包含 Nginx 和静态资源的应用镜像。 - 深入理解
ADD指令相较于COPY的特性,尤其是在处理压缩包时的自动解压功能。 - 了解如何组织项目文件以配合
Dockerfile构建。 - 学会使用
LABEL指令为 Docker 镜像添加描述性元数据。 - 实践通过简单的 Shell 脚本自动化镜像构建和容器运行流程。
准备工作:
- Docker 环境: 确保你的系统中已安装并运行 Docker。
- 游戏文件: 准备好需要部署的游戏的压缩包文件(例如:
oldboyedu-huangjinkuanggong.tar.gz,oldboyedu-killbird.tar.gz,oldboyedu-tanke.tar.gz)。这些文件应包含游戏的所有静态资源(HTML, CSS, JavaScript, 图片等)。 - 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_name 或 location 块来区分游戏。
# /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 需要根据你的游戏文件结构和访问方式调整 location 和 root/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应用代码)放在后面。 ADDvsCOPY: 理解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 的世界里更进一步!
浙公网安备 33010602011771号