
1. 项目概述为什么在 Ubuntu 上用 GitLab CI/CD 做持续部署不是“炫技”而是工程效率的刚性需求GitLab CI/CD、Ubuntu、Docker、SSH——这四个词凑在一起不是技术堆砌而是一套被无数中小团队验证过的、成本可控、链路清晰、运维可追溯的自动化交付闭环。我从2018年开始在客户现场落地这类方案最早是给一家做智能硬件固件更新平台的创业公司搭流水线当时他们还在用“本地打包 → U盘拷贝 → 运维手动上传 → 重启服务”的方式发布新版本一次发布平均耗时47分钟出错率高达31%。现在这套 GitLab CI/CD 流水线跑在一台 4核8G 的 Ubuntu 22.04 虚拟机上从代码 push 到服务热更新完成全程 92 秒失败自动回滚日志可查、步骤可溯、权限可控。它解决的从来不是“能不能跑起来”的问题而是“能不能让三个人同时改代码、五个人同时测功能、两个人同时上线、零人值守运维”这个真实业务场景下的协同熵增问题。你可能正面临这些具体困境开发写完代码不敢合主干怕影响测试环境测试同学每天反复问“最新版部署好了吗”运维凌晨三点被电话叫醒处理一个漏掉的配置文件产品经理想看个新功能预览得等两天排期。这些问题背后本质是“人肉协调”替代了“机器契约”。而 GitLab CI/CD 在 Ubuntu 上的落地恰恰把“谁在什么时候做了什么、依据什么规则、触发了哪些动作、结果是否符合预期”全部固化进.gitlab-ci.yml这个纯文本文件里——它不依赖某个人的记忆不依赖口头约定不依赖临时脚本只依赖 Git 提交历史和 YAML 语法。Ubuntu 作为宿主系统不是随便选的它长期支持周期LTS 版本支持5年、包管理成熟apt、Docker 官方首选支持平台、SSH 服务开箱即用且社区文档密度远超其他发行版。Docker 不是为“容器化”而容器化它是解决“在我机器上能跑到你服务器上就报错”这个经典环境不一致问题的终极隔离层SSH 则是整个流水线与目标服务器建立可信、加密、免密通信的唯一通道——没有它CI 作业连服务器的门都敲不开。所以这不是一个“教你怎么配 YAML”的教程而是一个从真实故障现场反推出来的、带血丝的部署工程实践笔记。2. 整体架构设计与核心组件选型逻辑为什么不用 Jenkins为什么必须是 Ubuntu为什么 Docker 和 SSH 缺一不可2.1 架构全景图四层收敛拒绝过度设计整套持续部署流水线不是单点工具拼接而是分层收敛的有机体。我把它拆成四个明确职责层第一层源码中枢层GitLab所有代码、分支策略、合并请求MR、Issue 跟踪、权限控制全部收口于自建或托管的 GitLab 实例。这里不做任何构建只做“事实记录”——谁提交了什么、何时提交、关联哪个 Issue、是否通过 MR 检查。GitLab Runner 是它的延伸触手但本身不存储状态。第二层执行引擎层Ubuntu GitLab Runner一台干净的 Ubuntu 22.04 LTS 服务器物理机或虚拟机均可仅安装 GitLab Runner 和基础依赖curl、jq、openssh-client。Runner 以shell或dockerexecutor 模式运行绝不安装 Node.js、Python、Java 等语言环境——这些全部交给第三层的 Docker 镜像去承载。这样做的好处是Runner 主机永远“无状态”重装系统只需 10 分钟所有构建环境一致性由镜像保证。第三层环境沙盒层Docker每个构建任务job都运行在一个指定的基础镜像中比如node:18-alpine用于前端构建python:3.11-slim用于后端测试docker:24.0.7-dind用于构建并推送 Docker 镜像。关键点在于所有镜像必须来自可信源Docker Hub 官方或私有 Harbor且禁止使用latest标签。我见过太多团队因为node:latest突然升级导致构建失败最后发现是 Node.js 20 的某个 API 在 18 里根本不存在。我们强制要求镜像标签精确到小版本号例如node:18.18.2-alpine并在.gitlab-ci.yml中显式声明这是稳定性的第一道防火墙。第四层交付终点层目标服务器 SSH应用最终部署的目标机器可能是另一台 Ubuntu 服务器也可能是 Kubernetes 集群节点其唯一接入方式是 SSH。GitLab CI 作业通过ssh命令连接目标机执行部署脚本如deploy.sh、拉取新镜像、重启容器、校验健康端点。这里 SSH 不是“传输工具”而是“执行代理”——它把 CI 环境的指令安全、精准地投递到生产环境且全程可审计SSH 日志记录每条命令。提示为什么坚决不用 JenkinsJenkins 插件生态虽庞大但配置分散全局配置、节点配置、Job 配置、Pipeline 脚本权限模型复杂升级易断裂。GitLab CI 将一切收敛到.gitlab-ci.yml文件中版本控制、Code Review、权限继承天然一体化。一个 MR 合并就等于流水线配置变更生效无需登录后台点点点。2.2 Ubuntu 选型的硬性理由不只是“用的人多”选择 Ubuntu 22.04 LTS而非 Debian、CentOS Stream 或 Arch有三个不可替代的技术动因Docker 官方支持矩阵的黄金标准Docker Engine 的每个稳定版本如 24.0.7的官方安装文档第一条就是 “Ubuntu 22.04 (Jammy Jellyfish)”。这意味着内核模块如 overlay2 存储驱动、cgroup v2 支持、systemd 集成都是经过 Docker 团队逐行验证的。我曾用 CentOS Stream 8 部署 Docker结果发现docker build时随机卡死排查三天才发现是 cgroup v1/v2 混用导致的内核竞态而 Ubuntu 22.04 默认启用 cgroup v2完全规避此坑。SSH 密钥管理的最小心智负担Ubuntu 的openssh-server默认配置开箱即用sshd_config中PubkeyAuthentication yes和PasswordAuthentication no是安全基线。更重要的是ssh-copy-id工具在 Ubuntu 上行为最稳定——它能正确处理~/.ssh/authorized_keys的权限必须 600、目录权限必须 700、SELinux 上下文Ubuntu 无 SELinux 干扰。对比之下某些发行版的ssh-copy-id会错误地将公钥追加到 root 用户的 authorized_keys导致非 root 用户无法免密登录。APT 包管理的确定性与可重现性apt install gitlab-runner命令在 Ubuntu 上安装的是 GitLab 官方仓库提供的二进制包非 snap版本锁定严格无后台自动更新干扰。而某些发行版的包管理器会静默升级 runner导致.gitlab-ci.yml中声明的image: docker:24.0.7-dind与主机上实际运行的 Docker daemon 版本不匹配引发dind容器启动失败。Ubuntu 的 APT 仓库策略确保了“今天装的明天还是这个版本”。2.3 Docker 与 SSH 的耦合设计为什么它们必须一起出现Docker 和 SSH 在此架构中不是并列关系而是主从协作Docker 解决“构建环境一致性”SSH 解决“部署动作原子性”。Docker 的不可替代性假设你有一个 Python Web 应用依赖pandas1.5.3和numpy1.23.5。如果直接在 Ubuntu 主机上pip install不同时间安装可能因 PyPI 镜像缓存、编译器版本差异导致二进制 wheel 不同进而引发线上段错误。而docker build命令基于 Dockerfile每一层缓存哈希值固定只要 Dockerfile 和源码不变产出的镜像 SHA256 值就绝对一致。这是可重现部署的数学基础。SSH 的不可替代性有人问“为什么不用 GitLab 的deployjob 类型或 Kubernetes 的 Helm”答案是简单场景下SSH 是最轻量、最透明、最易调试的交付通道。一个ssh userprod-server cd /app git pull docker-compose up -d命令你可以立刻在终端里看到每一步输出失败时直接登录服务器journalctl -u docker查日志。而 Kubernetes 的kubectl apply抽象层级太高一个ImagePullBackOff错误你需要查 Events、查 Pod Describe、查 Registry 认证调试路径长 5 倍。SSH 让部署过程“看得见、摸得着、改得快”。注意SSH 免密登录不是“为了省事”而是 CI 流水线自动化的前提。GitLab CI 作业运行时没有交互式终端无法输入密码。必须提前在 Runner 主机上生成密钥对并将公钥部署到目标服务器的~/.ssh/authorized_keys中。密钥必须设置强密码短语passphrase并通过 GitLab CI 变量加密存储私钥内容这是安全底线。3. 核心细节解析与实操要点.gitlab-ci.yml的 7 个生死攸关参数3.1image字段别再用latest精确到 patch 版本是职业素养.gitlab-ci.yml中image字段定义每个 job 运行的容器环境。新手常犯的致命错误是写image: node或image: docker。这会导致node标签指向node:20-alpine当前最新但你的package.json依赖node18构建时报SyntaxError: Unexpected token ??空值合并运算符是 Node 19 特性docker标签指向docker:24.0.7但你的 Runner 主机 Docker daemon 是 23.0.6dind容器启动时因 API 版本不兼容直接退出。正确做法是在项目根目录创建Dockerfile.base显式声明基础环境# Dockerfile.base FROM node:18.18.2-alpine RUN npm install -g pnpm8.15.4 WORKDIR /app然后在.gitlab-ci.yml中引用stages: - build - test - deploy build-app: stage: build image: name: registry.gitlab.com/your-group/your-project:base-v1.0.0 entrypoint: [] script: - pnpm install - pnpm build artifacts: paths: - dist/构建base-v1.0.0镜像的 CI job 必须先于其他 job 运行且每次修改Dockerfile.base都要更新 tag。tag 命名规则v主版本.次版本.修订号-环境例如v1.0.0-prod。这样当某天发现v1.0.0-prod镜像有安全漏洞你只需修复Dockerfile.base重新构建v1.0.1-prod然后在应用 job 中修改一行name: ...:v1.0.1-prod全量滚动更新完成。3.2before_script环境初始化的黄金三板斧每个 job 开始前执行的before_script是避免“环境漂移”的关键防线。我强制要求所有 job 的before_script包含以下三行before_script: - apk add --no-cache curl jq bash # Alpine 系统必备工具 - mkdir -p ~/.ssh chmod 700 ~/.ssh - echo $SSH_KNOWN_HOSTS ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts第一行apk addAlpine Linux 的包管理器apk默认不包含curlHTTP 请求、jqJSON 解析、bash脚本兼容。很多教程教人用sh但sh不支持[[ ]]条件判断和数组写复杂逻辑极易翻车。bash是更健壮的选择。第二行mkdir -p ~/.ssh确保 SSH 目录存在且权限正确。chmod 700是硬性要求OpenSSH 会拒绝读取权限过宽的~/.ssh目录报错Bad owner or permissions on /root/.ssh/config。第三行echo $SSH_KNOWN_HOSTS$SSH_KNOWN_HOSTS是 GitLab CI 变量存储目标服务器的 SSH 公钥指纹如github.com ssh-rsa AAAAB3NzaC1yc2E...。不设置此变量ssh命令首次连接时会交互式询问 “Are you sure you want to continue connecting (yes/no)?”导致 CI 卡死。将指纹预置到known_hosts实现全自动信任。实操心得$SSH_KNOWN_HOSTS变量必须在 GitLab 项目 Settings CI/CD Variables 中添加类型选File不是Variable。因为known_hosts文件内容含换行符用普通变量会丢失格式。添加后GitLab 会自动将文件内容注入 job 环境echo $SSH_KNOWN_HOSTS即可原样输出。3.3variables敏感信息的加密保险柜数据库密码、API 密钥、Docker Registry 凭据绝不能明文写在.gitlab-ci.yml中。GitLab 提供两种安全变量机制Project-level Variables项目级变量适用于所有 job如DOCKER_REGISTRY_USER、DOCKER_REGISTRY_PASSWORD。在 Settings CI/CD Variables 设置勾选Protected仅在受保护分支生效和Mask variable日志中显示为***。File-type Variables文件型变量适用于私钥、证书等二进制内容。如SSH_PRIVATE_KEY类型选FileGitLab 会将其内容写入/builds/group/project/SSH_PRIVATE_KEY文件job 中可直接cat $SSH_PRIVATE_KEY读取。关键技巧用ssh-agent管理私钥避免硬编码路径在before_script中启动ssh-agent并添加私钥before_script: - eval $(ssh-agent -s) - echo $SSH_PRIVATE_KEY | tr -d \r | ssh-add - /dev/null - mkdir -p ~/.ssh chmod 700 ~/.ssh - echo $SSH_KNOWN_HOSTS ~/.ssh/known_hoststr -d \r是关键Windows 编辑器保存的私钥文件末尾有\r\nssh-add会将其识别为非法字符报错invalid format。tr命令清除回车符确保私钥纯净。3.4cache与artifacts理解它们的本质区别新手常混淆cache和artifacts导致构建变慢或部署失败。cache是“加速器”作用于 job 内部缓存node_modules/、~/.m2/Maven 仓库等重复下载的依赖。它基于 key如node-modules-${CI_COMMIT_REF_SLUG}匹配跨 job、跨 pipeline 复用。但 cache 不保证存在——GitLab Runner 可能因磁盘满而清理 cache。因此cache中的内容必须是“可重建的”不能是构建产物。artifacts是“交付物”作用于 job 之间dist/目录、编译好的二进制文件、Docker 镜像 tar 包必须通过artifacts传递给下游 job。artifacts会被 GitLab 服务器持久化存储直到 pipeline 过期默认30天。下游 job 通过dependencies显式声明需要哪些上游 job 的 artifacts。典型错误配置# ❌ 错误把构建产物放 cache下游 job 无法获取 build: cache: key: ${CI_COMMIT_REF_SLUG} paths: - dist/ deploy: dependencies: [] # 未声明依赖dist/ 不会自动下载正确配置# ✅ 正确artifacts 传递构建产物 build: artifacts: paths: - dist/ expire_in: 1 week deploy: dependencies: - build # 显式声明依赖 build job 的 artifacts script: - scp -r dist/* userprod:/var/www/html/3.5only/except与rules分支策略的现代写法旧语法only: - main已被rules取代因其支持更复杂的条件表达式。一个健壮的部署策略应满足main分支 push → 自动部署到预发环境stagingmain分支打 tag如v1.2.0→ 自动部署到生产环境production并触发 release 创建其他分支如feature/login→ 仅运行构建和测试不部署stages: - build - test - deploy deploy-staging: stage: deploy image: alpine:3.18 script: - apk add openssh-client - ssh -o StrictHostKeyCheckingno userstaging cd /app git pull origin main docker-compose up -d rules: - if: $CI_COMMIT_BRANCH main when: always deploy-production: stage: deploy image: alpine:3.18 script: - apk add openssh-client curl - ssh -o StrictHostKeyCheckingno userprod cd /app git pull origin $CI_COMMIT_TAG docker-compose up -d - | curl -X POST https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/releases \ -H PRIVATE-TOKEN: ${GITLAB_TOKEN} \ -H Content-Type: application/json \ -d {\name\:\Release $CI_COMMIT_TAG\,\tag_name\:\$CI_COMMIT_TAG\,\description\:\Deployed from pipeline $CI_PIPELINE_ID\} rules: - if: $CI_COMMIT_TAG when: alwaysrules的优势在于可组合- if: $CI_COMMIT_TAG $CI_PIPELINE_SOURCE push表示“仅当是 tag 推送时才触发”排除了通过 UI 手动创建 tag 的情况确保 release 动作与代码变更强绑定。3.6retry与allow_failure为网络抖动留出呼吸空间CI 流水线运行在公网环境网络请求如npm install、docker pull失败是常态。盲目设置retry: 2会导致失败 job 重试 3 次原始 2 次浪费资源。更合理的方式是对外部依赖拉取类 job如install-dependencies设置retry: 2但限定只重试网络错误install-deps: script: npm install retry: max_times: 2 when: - runner_system_failure - stuck_or_timeout_failure - unknown_failure对非关键路径 job如code-quality代码质量扫描设置allow_failure: true即使失败也不阻塞 pipelinecode-quality: script: npx eslint . allow_failure: trueallow_failure: true不代表“可以忽略”而是“失败时继续执行后续 job但 pipeline 状态仍为 failed”。GitLab 会在 UI 中明确标出该 job 是“allowed to fail”便于团队聚焦真正阻断交付的问题。3.7timeout给每个 job 设定生命倒计时默认 job 超时是 1 小时但一个docker build通常 5 分钟内完成npm test通常 2 分钟。设置过长 timeout 会导致问题被掩盖比如ssh连接因防火墙策略变更而卡死job 一直等待直到 1 小时超时你收到告警时已过去 60 分钟。最佳实践按任务类型设定 timeoutbuildjobtimeout: 10 minutestestjobtimeout: 15 minutes集成测试可能较慢deployjobtimeout: 5 minutes部署脚本应极致精简超时说明架构有问题deploy: timeout: 5 minutes script: - ssh userprod cd /app ./deploy.sh如果deploy.sh经常超时说明它承担了不该承担的职责如数据库迁移、大文件同步应将其拆分为独立 job每个 job 职责单一、timeout 明确。4. 实操过程与核心环节实现从零搭建一条可运行的 CI/CD 流水线4.1 环境准备Ubuntu 主机的 5 个必做初始化操作在 Ubuntu 22.04 服务器上执行以下命令这是所有后续操作的基石# 1. 更新系统并安装基础工具 sudo apt update sudo apt upgrade -y sudo apt install -y curl wget gnupg2 software-properties-common apt-transport-https ca-certificates # 2. 配置时区和 locale避免中文乱码和时间错误 sudo timedatectl set-timezone Asia/Shanghai sudo locale-gen en_US.UTF-8 sudo update-locale LANGen_US.UTF-8 # 3. 安装 Docker官方源非 snap curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io # 4. 启动 Docker 并加入当前用户组避免每次 sudo sudo systemctl enable docker sudo systemctl start docker sudo usermod -aG docker $USER # 重要执行此命令后需重新登录或运行 newgrp docker 刷新组权限 # 5. 安装 GitLab Runner官方 deb 包 curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash sudo apt install -y gitlab-runner注意事项第 4 步usermod -aG docker $USER后必须退出当前 SSH 会话并重新登录否则docker命令仍会报Permission denied while trying to connect to the Docker daemon socket。这是 Ubuntu 的标准行为不是 bug。4.2 GitLab Runner 注册Shell Executor 为何是新手首选对于首次搭建我强烈推荐shellexecutor而非docker或kubernetes原因有三零学习成本Runner 直接在 Ubuntu 主机上执行命令无需理解容器网络、卷挂载等概念调试直观所有script命令都在你熟悉的 shell 环境中运行ls -l、cat /tmp/log等命令随手可用资源占用低无需额外启动dind容器节省内存。注册命令在 Ubuntu 主机上执行sudo gitlab-runner register # 依次输入 # Please enter the gitlab-ci coordinator URL: https://gitlab.com/ 或你的自建 GitLab 地址 # Please enter the gitlab-ci token for this runner: xxxxxxxx 在 GitLab 项目 Settings CI/CD Runners 页面获取 # Please enter the gitlab-ci description for this runner: ubuntu-shell-runner # Please enter the gitlab-ci tags for this runner (comma separated): shell,ubuntu # Please enter the executor: shell # Please enter the Docker image (default: ruby:2.7): # 直接回车shell executor 不需要注册成功后检查 Runner 状态sudo gitlab-runner list # 输出应为Listing configured runners ConfigFile/etc/gitlab-runner/config.toml # ubuntu-shell-runner Executorshell Tokenxxxxxxxxxx URLhttps://gitlab.com/ sudo gitlab-runner status # 输出应为gitlab-runner: Service is running!4.3 目标服务器 SSH 免密配置三步建立可信通道部署的目标服务器如prod-server必须预先配置好 SSH 免密访问。这不是 GitLab CI 的配置而是基础设施准备步骤 1在 Runner 主机生成密钥对# 切换到 gitlab-runner 用户重要Runner 以该用户身份运行 job sudo su - gitlab-runner # 生成密钥不设密码短语因为 CI 无法交互输入 ssh-keygen -t ed25519 -C gitlab-cirunner -f ~/.ssh/id_ed25519 -N # -N 表示空密码短语步骤 2将公钥复制到目标服务器# 使用 ssh-copy-idUbuntu 自带 ssh-copy-id -i ~/.ssh/id_ed25519.pub userprod-server # 输入 user 用户密码成功后公钥会追加到 prod-server 的 ~/.ssh/authorized_keys步骤 3获取目标服务器的 SSH 公钥指纹存入 GitLab 变量# 在 Runner 主机执行获取 prod-server 的公钥指纹 ssh-keyscan -H prod-server ~/.ssh/known_hosts # 查看 known_hosts 文件找到 prod-server 对应行 cat ~/.ssh/known_hosts | grep prod-server # 输出类似|1|xxx|xxx ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... # 复制整行内容包括 |1|... 部分将复制的整行内容粘贴到 GitLab 项目 Settings CI/CD Variables 中Key 填SSH_KNOWN_HOSTSType 选FileSave。4.4.gitlab-ci.yml完整示例一个可立即运行的 Web 应用部署流水线以下是一个生产可用的.gitlab-ci.yml部署一个简单的 Nginx 静态网站。它覆盖了构建、测试、部署全流程且每个环节都附带注释说明原理# .gitlab-ci.yml # 定义全局变量所有 job 共享 variables: # Docker Registry 地址用于推送镜像 DOCKER_REGISTRY: registry.gitlab.com # 项目命名空间由 GitLab 自动生成 CI_PROJECT_PATH_SLUG: $CI_PROJECT_PATH_SLUG # 构建镜像的完整名称 IMAGE_TAG: $DOCKER_REGISTRY/$CI_PROJECT_PATH_SLUG:$CI_COMMIT_SHORT_SHA # 定义流水线阶段顺序执行 stages: - build - test - deploy # 构建阶段基于 nginx:alpine 构建静态网站镜像 build-image: stage: build image: docker:24.0.7-dind services: - docker:24.0.7-dind before_script: - apk add --no-cache python3 py3-pip - pip3 install docker-compose # 为后续 compose 部署准备 - docker info script: # 构建镜像使用当前 commit SHA 作为 tag确保唯一性 - docker build -t $IMAGE_TAG . # 登录 GitLab Container Registry需提前在 GitLab 设置 CI 变量 CI_REGISTRY_USER 和 CI_REGISTRY_PASSWORD - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin # 推送镜像 - docker push $IMAGE_TAG # 构建产物是镜像无需 artifacts但需 cache Docker layer 加速 cache: key: ${CI_COMMIT_REF_SLUG} paths: - /var/lib/docker/ # 测试阶段验证镜像能否正常启动并响应 HTTP 请求 test-image: stage: test image: docker:24.0.7-dind services: - docker:24.0.7-dind before_script: - apk add --no-cache curl script: # 启动容器映射端口 8080 - docker run -d --name test-nginx -p 8080:80 $IMAGE_TAG # 等待 3 秒让 Nginx 启动 - sleep 3 # 发送 HTTP 请求检查返回状态码是否为 200 - | if curl -s -o /dev/null -w %{http_code} http://localhost:8080 | grep -q 200; then echo ✅ Nginx container responds with 200 else echo ❌ Nginx container failed to respond exit 1 fi # 清理测试容器 - docker rm -f test-nginx # 测试 job 不产生交付物无需 artifacts # 部署阶段将镜像部署到目标服务器 deploy-production: stage: deploy image: alpine:3.18 before_script: - apk add --no-cache openssh-client curl - mkdir -p ~/.ssh chmod 700 ~/.ssh - echo $SSH_KNOWN_HOSTS ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts script: # 1. SSH 到生产服务器拉取最新镜像 - ssh -o StrictHostKeyCheckingno userprod-server docker pull $IMAGE_TAG # 2. 停止旧容器 - ssh -o StrictHostKeyCheckingno userprod-server docker stop my-web-app || true # 3. 启动新容器映射 80 端口后台运行 - ssh -o StrictHostKeyCheckingno userprod-server docker run -d --name my-web-app -p 80:80 --restartalways $IMAGE_TAG # 4. 验证部署检查容器是否运行且端口监听 - | if ssh -o StrictHostKeyCheckingno userprod-server docker ps --filter namemy-web-app --format {{.Status}} | grep -q Up; then echo ✅ Production deployment successful else echo ❌ Production deployment failed exit 1 fi # 仅在 main 分支或 tag 时执行 rules: - if: $CI_COMMIT_BRANCH main || $CI_COMMIT_TAG when: always配套的Dockerfile放在项目根目录# Dockerfile FROM nginx:alpine:1.24.0 # 复制静态文件到 Nginx 默认目录 COPY ./html/ /usr/share/nginx/html/ # 暴露 80 端口 EXPOSE 80 # 启动 Nginx CMD [nginx, -g, daemon off;]4.5 部署脚本deploy.sh让 SSH 命令更健壮上面的deploy-productionjob 直接在script中写了多行ssh命令适合简单场景。对于复杂应用建议将部署逻辑封装到deploy.sh脚本中由 CI 调用#!/bin/bash # deploy.sh - 放在项目根目录与 .gitlab-ci.yml 同级 set -e # 任何命令失败立即退出 set -u # 引用未定义变量时报错 # 参数检查 if [ $# -ne 2 ]; then echo Usage: $0 IMAGE_TAG ENV echo Example: $0 registry.gitlab.com/group/project:abc123 staging exit 1 fi IMAGE_TAG$1 ENV$2 # 根据环境选择目标服务器和配置 case $ENV in staging) TARGET_SERVERstaging-server CONFIG_FILEdocker-compose.staging.yml ;; production) TARGET_SERVERprod-server CONFIG_FILEdocker-compose.prod.yml ;; *) echo Unknown environment: $ENV exit 1 ;; esac # 执行部署 echo Deploying $IMAGE_TAG to $ENV ($TARGET_SERVER)... scp $CONFIG_FILE $TARGET_SERVER:/tmp/docker-compose.yml ssh $TARGET_SERVER cd /app docker-compose -f /tmp/docker-compose.yml pull docker-compose -f /tmp/docker-compose.yml up -d docker-compose -f /tmp/docker-compose.yml ps echo ✅ Deployment completed.在.gitlab-ci.yml中调用deploy-staging: