从零构建Alpine中间件镜像并部署至openEuler单Master K8s集群的完整实践引言在云原生时代Kubernetes 已成为容器编排的事实标准。容器镜像作为应用的最终交付载体其安全性与可控性至关重要。越来越多的企业出于安全合规、离线部署以及深度定制的需求要求不使用公共镜像仓库的现成镜像而是从零开始手动构建所有中间件。本实验正是基于这一思想在openEuler 24.03 SP3 系统的单 Master 节点 Kubernetes 集群上以Alpine Linux为基础手工制作Nginx、MariaDB、DNS (BIND)、Redis四种中间件镜像通过 K8s 实现单 Pod 部署并最终验证所有服务的可行性。整个过程采用docker cri-dockerd的容器运行时完全离线可控体现了“自建镜像、完全掌控”的工程实践。一、理论基础与功能说明1.1 为什么选择 Alpine LinuxAlpine Linux 是一个面向安全的轻量级 Linux 发行版其基础镜像大小仅约 3 MB。它使用musl libc和BusyBox专为容器环境优化。相比 Ubuntu、openEuler 等动辄百兆的基础镜像Alpine 能够显著减小最终镜像的体积加快分发和启动速度。其包管理器apk简洁高效拥有丰富的软件包支持非常适合制作精简的中间件镜像。1.2 中间件功能简介Nginx高性能的 HTTP 和反向代理服务器常用于 Web 服务、负载均衡与静态资源托管。MariaDBMySQL 数据库的经典分支完全兼容 MySQL 协议与语法是广泛使用的关系型数据库。DNS (BIND)互联网上应用最广泛的域名解析服务软件支持正向域名到IP和反向IP到域名解析。Redis基于内存的高性能键值存储系统常用于缓存、会话管理、消息队列等场景。1.3 Kubernetes Pod 与单Pod部署Pod 是 Kubernetes 最小的调度单元可以包含一个或多个容器。本实验采用“单 Pod 部署”策略为每个中间件创建一个独立的 Pod非常符合作业的直观要求。相比 Deployment 主要管理多副本和滚动更新直接使用 Pod 对象在实验场景下更简洁明确。1.4 自建镜像 vs 官方镜像拉取官方镜像虽然方便但可能包含不必要的组件、未被审计的漏洞且版本受制于维护者。自建镜像让我们能够掌控每一层内容确保安全与合规深度定制配置文件如 nginx.conf、my.cnf实现完全离线交付无需依赖外部仓库。本次实验完全贯彻这一原则从 Alpine 裸系统开始手动安装软件、写入配置、设置权限最终生成可直接使用的镜像。二、实验环境与架构2.1 集群节点与网络规划实验集群由三台运行 openEuler 24.03 SP3 的虚拟机构成相关环境信息如下表所示。节点角色主机名IP 地址操作系统Master (兼 Node)k8s-master192.168.64.128openEuler 24.03 SP3Worker Node 1k8s-node1192.168.64.129openEuler 24.03 SP3Worker Node 2k8s-node2192.168.64.130openEuler 24.03 SP3核心软件与运行时容器运行时Docker cri-dockerd因 Kubernetes 1.24 版本起移除了内置的dockershim必须额外安装cri-dockerd作为适配器桥接 kubelet 的 CRI 接口与 Docker 的 API。软件版本Kubernetes 1.28 系列Docker 20.10以及最新稳定版的 cri-dockerd。2.2 DNS 解析规划我们规划了hzx.com作为内部域名配置了以下解析记录用于验证 DNS 服务的功能。记录类型域名或 IP指向说明A (正向)ns.hzx.com192.168.64.128名称服务器记录A (正向)www.hzx.com192.168.64.128Web 服务测试记录A (正向)k8s-master.hzx.com192.168.64.128Master 节点记录A (正向)k8s-node1.hzx.com192.168.64.129Worker 1 节点记录A (正向)k8s-node2.hzx.com192.168.64.130Worker 2 节点记录PTR (反向)192.168.64.128k8s-master.hzx.com反向解析示例三、镜像制作详细过程制作目标是四个镜像my-nginx:alpine、my-mariadb:alpine、my-dns:alpine、my-redis:alpine。所有构建在开发机可联网上完成然后导出为 tar 包分发至各 Worker 节点。3.1 Nginx 镜像Nginx 作为本次实验的 Web 服务器需要提供一个默认页面来验证服务是否正常启动。然而Alpine 官方维护的 Nginx 包非常精简存在两个需要解决的问题静态资源目录不匹配Alpine 默认将网页根目录设定在/var/www/localhost/htdocs这与我们日常使用的/var/www/html习惯不同。更重要的是该目录默认为空没有index.html直接访问会触发 404 错误。非 root 用户权限限制Linux 系统严格限制非 root 用户绑定 1024 以下的特权端口而 HTTP 的标准服务端口正是 80。为了构建一个既简单又安全的镜像我们进行了专门的设计统一目录并预置内容在 Dockerfile 中我们主动创建约定俗成的/var/www/html目录并写入一句测试信息h1nginx ok/h1作为默认首页。主进程降权运行我们不设置USER nginx让容器以 root 身份启动这样 Nginx 主进程就能顺利绑定 80 端口。同时在 Nginx 的主配置文件/etc/nginx/nginx.conf中我们设置了user nginx;这一指令它会强制所有处理用户请求的 worker 进程切换到低权限的nginx用户从而兼顾了服务功能与系统安全。Dockerfile 及配置FROM alpine:3.19 RUN apk add --no-cache nginx \ mkdir -p /run/nginx /var/www/html \ echo nginx ok /var/www/html/index.html \ chown -R nginx:nginx /run/nginx /var/www/html COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80 CMD [nginx, -g, daemon off;] nginx user nginx; worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { listen 0.0.0.0:80; # 强制绑定IPv4解决Alpine默认监听IPv6的问题 server_name localhost; root /var/www/html; index index.html index.htm; } }关键点解释必须使用listen 0.0.0.0:80;明确绑定到 IPv4 地址。在某些环境下简单的listen 80;可能默认绑定到::1:80导致使用 IPv4 的wget连接被拒。实际测试中wget http://127.0.0.1能成功而wget http://localhost则失败原因正是localhost解析为了::1。3.2 MariaDB 镜像鉴于 Alpine 官方源不包含真正的 MySQL而提供完全兼容且更轻量的 MariaDB我们选择后者。MariaDB 默认仅允许 root 用户通过 Unix Socket 无密码登录这会导致后续的 TCP 测试mysql -u root失败。构建过程中我们通过先启动一个临时实例来修改 root 的认证方式。Dockerfile 及配置FROM alpine:3.19 RUN apk add --no-cache mariadb mariadb-client COPY my.cnf /etc/mysql/my.cnf RUN mysql_install_db --usermysql --datadir/var/lib/mysql \ chown -R mysql:mysql /var/lib/mysql RUN mkdir -p /run/mysqld chmod 755 /run/mysqld chown mysql:mysql /run/mysqld # 后台启动修改root认证方式允许TCP无密码登录 RUN mysqld --usermysql --datadir/var/lib/mysql --skip-name-resolve \ sleep 3; \ mysql -u root -S /run/mysqld/mysqld.sock -e \ ALTER USER rootlocalhost IDENTIFIED VIA mysql_native_password; FLUSH PRIVILEGES;; \ killall mysqld; sleep 2; \ rm -f /run/mysqld/mysqld.sock VOLUME /var/lib/mysql EXPOSE 3306 USER mysql CMD [mysqld, --usermysql, --skip-name-resolve] ini [mysqld] datadir/var/lib/mysql socket/run/mysqld/mysqld.sock port3306 skip-name-resolve character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci关键点解释提前创建/run/mysqld目录并设定权限防止服务启动时因找不到 socket 目录而崩溃。通过ALTER USER ... IDENTIFIED VIA mysql_native_password将认证方式改为兼容模式使得mysql -u root可以通过 TCP 连接。3.3 DNS (BIND) 镜像BIND 需要提供正向和反向区域文件。其默认以非 root 用户named运行无法直接绑定 53 特权端口。因此需要通过setcap为named二进制程序赋予cap_net_bind_service的能力。Dockerfile 及配置FROM alpine:3.19 RUN apk add --no-cache bind libcap COPY named.conf /etc/bind/named.conf COPY hzx.com.zone /var/bind/hzx.com.zone COPY 64.168.192.zone /var/bind/64.168.192.zone RUN deluser named 2/dev/null || true \ adduser -D -H named \ mkdir -p /var/bind /var/log/named \ chown -R named:named /var/bind /var/log/named /etc/bind \ /var/bind/hzx.com.zone /var/bind/64.168.192.zone \ setcap cap_net_bind_serviceep /usr/sbin/named EXPOSE 53/tcp 53/udp USER named CMD [sh, -c, named-checkconf /etc/bind/named.conf exec named -g -c /etc/bind/named.conf -u named]# named.conf 关键部分 options { directory /var/bind; listen-on { any; }; listen-on-v6 { none; }; # 禁用IPv6监听避免干扰 allow-query { any; }; ... }; zone hzx.com IN { type master; file hzx.com.zone; }; zone 64.168.192.in-addr.arpa IN { # 反向区名必须符合规范 type master; file 64.168.192.zone; };关键点解释反向区域名称必须是64.168.192.in-addr.arpa不能误写为文件名64.168.192.zone。否则 BIND 会将文件名当作区域名导致反向解析失败日志显示zone 64.168.192.zone/IN loaded。禁用 IPv6通过listen-on-v6 { none; };避免 IPv6 地址干扰是实践中一个重要的排错手段。能力设置setcap cap_net_bind_serviceep让非 root 的named用户也能绑定 53 端口。3.4 Redis 镜像Redis 配置相对简单需要确保绑定所有 IP 并为其持久化文件创建/data目录。Dockerfile 及配置FROM alpine:3.19 RUN apk add --no-cache redis COPY redis.conf /etc/redis.conf RUN deluser redis 2/dev/null || true \ adduser -D redis \ mkdir -p /data \ chown redis:redis /etc/redis.conf /data USER redis EXPOSE 6379 CMD [redis-server, /etc/redis.conf]# redis.conf 关键部分 bind 0.0.0.0 port 6379 daemonize no dir /data关键点解释daemonize no配置保证 Redis 在前台运行避免容器启动后立刻退出。dir /data必须指向一个存在且有写入权限的目录否则 Redis 进程会报错崩溃。四、构建与分发在包含所有 Dockerfile 的开发机上执行以下命令进行构建然后导出为一个 tar 包。cd /media/kubernetes1/middleware # 依次构建四个镜像 cd nginx docker build -t my-nginx:alpine . cd ../mariadb docker build -t my-mariadb:alpine . cd ../dns docker build -t my-dns:alpine . cd ../redis docker build -t my-redis:alpine . # 导出所有镜像 docker save -o final-middleware.tar \ my-nginx:alpine \ my-mariadb:alpine \ my-dns:alpine \ my-redis:alpine使用scp和docker load将镜像分发到集群中的每一个 Worker 节点。for ip in 129 130; do scp final-middleware.tar root192.168.64.$ip:/tmp/ ssh root192.168.64.$ip docker load -i /tmp/final-middleware.tar done五、Kubernetes 部署所有 Pod 的 YAML 文件都必须显式设置imagePullPolicy: Never强制 Kubernetes 使用已经分发到节点上的本地镜像。首先创建一个专门的命名空间2516kubectl create namespace 2516将四个 Pod 的定义合并到一个all-pods.yaml文件中方便统一管理。DNS Pod 必须使用hostNetwork: true来共享宿主机网络以便外部客户端能够直接查询其监听的 53 端口。# all-pods.yaml apiVersion: v1 kind: Pod metadata: name: my-nginx namespace: 2516 labels: app: nginx spec: containers: - name: nginx image: my-nginx:alpine imagePullPolicy: Never ports: - containerPort: 80 --- apiVersion: v1 kind: Pod metadata: name: my-mariadb namespace: 2516 labels: app: mariadb spec: containers: - name: mariadb image: my-mariadb:alpine imagePullPolicy: Never ports: - containerPort: 3306 --- apiVersion: v1 kind: Pod metadata: name: my-dns namespace: 2516 labels: app: dns spec: hostNetwork: true containers: - name: bind image: my-dns:alpine imagePullPolicy: Never ports: - containerPort: 53 protocol: UDP - containerPort: 53 protocol: TCP --- apiVersion: v1 kind: Pod metadata: name: my-redis namespace: 2516 labels: app: redis spec: containers: - name: redis image: my-redis:alpine imagePullPolicy: Never ports: - containerPort: 6379执行部署命令并检查 Pod 的运行状态kubectl apply -f /root/k8s-yaml/all-pods.yaml kubectl get pods -n 2516 -o wide输出示例四个 Pod 的 STATUS 均为 Running并分布在 k8s-node1 和 k8s-node2 上。六、测试与验证在所有 Pod 成功运行后我们逐一验证其功能。Nginx 测试kubectl exec -n 2516 my-nginx -- wget -qO- http://127.0.0.1 # 预期输出: nginx ok测试结果表明 Nginx 能够成功提供 Web 服务。使用 IP127.0.0.1而非localhost可有效规避容器内 DNS 解析为 IPv6 而 Nginx 仅监听 IPv4 的连接问题。MariaDB 测试kubectl exec -n 2516 -it my-mariadb -- mysql -u root -e SELECT VERSION(); # 预期输出: 10.11.14-MariaDBDNS 测试 (假设 pod 调度在 k8s-node2 / 192.168.64.130 上)# 正向解析 www.hzx.com nslookup www.hzx.com 192.168.64.130 # 预期输出: Address: 192.168.64.128 # 反向解析 192.168.64.128 nslookup 192.168.64.128 192.168.64.130 # 预期输出: name k8s-master.hzx.com测试结果证明 DNS 服务配置的正向和反向区域都已生效。Redis 测试kubectl exec -n 2516 my-redis -- redis-cli ping # 预期输出: PONG七、排错实录常见问题与解决方案在整个自建镜像和部署过程中我们遇到了许多典型的技术陷阱。下面将这六个最具代表性的问题进行梳理以供参考。问题现象深层原因解决方案容器启动即退出 (Error/CrashLoopBackOff)MariaDB、Redis、Nginx 启动所需的目录如/run/mysqld、/data在镜像中缺失进程报错退出。在 Dockerfile 中使用RUN mkdir -p预先创建所有必要目录并赋予正确的用户和权限。MariaDB 登录被拒默认仅允许 root 通过 Unix Socket 无密码认证TCP 连接被拒绝。在构建时启动临时mysqld实例执行ALTER USER将认证方式改为mysql_native_password并清空密码。Nginx 服务“连接被拒绝”wget http://localhost时localhost被解析为 IPv6 地址::1而 Nginx 的listen 80;在某些环境下可能只绑定了 IPv6 或绑定不明确。在nginx.conf中明确指定listen 0.0.0.0:80;强制监听所有 IPv4 地址测试时也直接使用127.0.0.1。DNS 正向解析 NXDOMAIN正向区域文件如hzx.com.zone中缺少目标域名如www的 A 记录。在正向区域文件中添加缺失的 A 记录如www IN A 192.168.64.128更新序列号并重建镜像。DNS 反向解析失败 (区域名错误)named.conf中反向区的名称误写为文件名64.168.192.zone导致 BIND 不将其视为标准反向区域。将区名修正为符合规范的64.168.192.in-addr.arpa文件名保持不变。构建时报错adduser: user in useapk add bind或apk add redis等命令会自动创建同名用户导致后续adduser失败。在 Dockerfile 中使用deluser 用户名 2/dev/null || true adduser...语句先尝试删除可能存在的用户再创建保证指令幂等性。八、总结与展望本次实验完整演示了如何从零开始在 openEuler 操作系统上基于 Alpine Linux 手工制作 Nginx、MariaDB、DNS、Redis 四个中间件的容器镜像并成功在 Kubernetes 集群中以单 Pod 模式部署和验证其功能。全过程严格遵循“不使用官方仓库现成镜像”的原则从选择最精简的基础系统、手动处理包依赖、到解决每一个目录权限和端口绑定的细节深刻实践了生产环境中自建镜像的安全与可控性理念。通过对这些典型排错案例的解决我们不仅掌握了 Alpine 的apk包管理特点、BIND 的配置细节还深入理解了 Kubernetes Pod 的调度策略、hostNetwork与特权端口的处理方式。这套方法可作为教学实验或中小企业私有化交付的可靠模板为更复杂的云原生实践奠定坚实的基础。