资讯动态

单台云服务器搭建轻量级CI/CD全链路实战

发布时间:2026/10/16 5:27:43 来源:尧图企业网站定制
1. 为什么要在单台云服务器上折腾CI/CD很多团队一提到CI/CD脑子里第一反应就是Jenkins集群、GitLab Runner池、Harbor私有仓库、Kubernetes命名空间隔离这一整套组合拳。这套东西确实成熟但它的隐性成本极高至少三台机器起步内存没有16G根本跑不顺光是维护这些中间件本身就要吃掉一个运维的大半精力。对于个人开发者、三五人的小团队、或者刚起步的内部项目来说这种投入产出比是严重失衡的。我这次要聊的方案核心思路就一句话把代码托管、持续集成、持续交付、镜像仓库、反向代理全部塞进一台阿里云ECS里用最少的组件跑通从提交代码到服务上线的完整链路。整套方案跑起来之后日常资源占用控制在2G内存以内一台2核4G的入门级ECS就能稳定承载月成本压到几十块钱。它解决的不是大规模高并发的问题而是小团队想拥有正规军一样的自动化流程但预算和人力都极其有限这个非常具体的痛点。这套方案适合谁如果你是一个人维护两三个小项目每次改完代码还要手动SSH上去拉代码、重启服务那这套东西能直接把你从重复劳动里解放出来。如果你是几个人的小团队没有专职运维但又不想每次发版都靠谁有空谁上的原始方式那它也够用。它不适合日构建上千次、需要多环境隔离、有严格审计要求的大型团队那种场景该上专业方案就上专业方案不要硬凑。在展开之前先把这套方案的整体骨架说清楚后面所有细节都围绕它展开。整台ECS上跑这么几个东西一个代码托管服务负责接收推送一个CI引擎负责监听变更并执行构建一个容器运行时负责把构建产物打包成镜像并跑起来一个反向代理负责把外部流量按域名分发到不同服务。这四个角色各司其职全部以容器方式运行互不干扰升级和迁移都很方便。下面我会逐个拆解每个环节的选型理由、配置细节和踩坑经验。2. 整体架构设计与组件选型逻辑2.1 单机方案的边界在哪里先把丑话说在前面单机方案有它天然的天花板认清边界比盲目乐观重要得多。这台ECS同时承担了构建和运行两个角色意味着构建过程会直接消耗生产服务的CPU和内存。如果构建任务特别重比如要编译大型前端项目、跑完整的测试套件那构建期间服务响应变慢是必然的。我的应对策略是把构建任务的资源限制做死同时尽量把构建安排在业务低峰期或者干脆接受发版时短暂抖动这个事实。另一个边界是磁盘。镜像、构建缓存、日志这三样东西会持续吃磁盘如果不做清理一台40G系统盘的ECS大概两三个月就会被撑满。所以整套方案里必须包含定期清理机制这个后面会专门讲。还有一点是单点故障这台机器挂了所有服务都挂所以定期把关键数据备份到对象存储是必须的不能偷懒。提示单机方案的核心哲学是用可接受的可用性损失换取极低的维护成本和资金成本。如果你的业务对可用性要求是99.9%以上请直接跳过这套方案。2.2 组件选型为什么是这几个代码托管这块我选的是轻量级的自托管Git服务。选它的理由很直接资源占用小一个容器几百兆内存就能跑Web界面够用支持Webhook触发这就够了。没必要上功能大而全的重型方案那些东西对单机环境来说是负担。CI引擎我选的是基于配置文件的轻量级方案它的特点是配置即代码一个YAML文件描述整个流水线跟代码一起版本管理。相比图形化配置的重型CI工具它更透明、更容易迁移出问题的时候直接看配置文件就知道发生了什么。而且它对资源的要求很低一个runner容器就能干活。容器运行时用最主流的那个就行生态最全遇到问题最容易搜到答案。反向代理用Nginx这个没什么好纠结的稳定、配置直观、性能足够。镜像仓库我一开始想用重量级的私有仓库方案后来发现对于单机场景直接用容器运行时自带的本地镜像管理就够了构建完的镜像本来就在本机不需要额外的仓库服务来中转。这个决定省掉了一整个组件的维护成本。组件角色选型方向资源占用选它的核心理由代码托管轻量自托管Git约300MB够用、省资源、支持WebhookCI引擎配置文件驱动型约200MB配置即代码、易迁移、低占用容器运行时主流容器引擎约150MB生态成熟、排错资料多反向代理Nginx约50MB稳定、配置直观镜像管理本地镜像存储0额外占用省掉独立仓库组件2.3 网络与域名规划单机上跑多个服务端口冲突是第一个要解决的问题。我的做法是所有业务容器都不直接暴露端口到宿主机而是加入同一个容器网络通过容器名互相访问。只有反向代理这一个容器对外暴露80和443端口所有外部流量都从它进来再由它按域名转发到对应的后端容器。这样一来新增一个服务只需要在反向代理配置里加一段不用去动防火墙规则。域名解析这块把所有子域名都解析到这台ECS的公网IP。比如主站用根域名API用api子域名管理后台用admin子域名各自指向不同的后端容器。证书用自动化的方式申请和续期这个后面实操部分会讲具体怎么做。这套规划的好处是扩展性很好以后加服务就是加一段Nginx配置加一个容器心智负担极低。3. 环境准备与基础组件部署实操3.1 ECS规格选择与系统初始化规格上我的建议是2核4G起步系统盘至少40G如果预算允许直接上ESSD云盘IO性能对构建速度影响很大。操作系统选主流的Linux发行版即可我用的是较新的稳定版本。买好机器之后第一件事不是装软件而是做基础安全加固和系统调优这一步很多人会跳过后面出问题再补就麻烦了。系统初始化我一般做这么几件事更新系统包到最新、设置时区为本地时区、配置SSH只允许密钥登录并禁用密码登录、配置防火墙只放行必要端口、开启系统的自动安全更新。防火墙这块要特别注意只放行SSH端口、80端口、443端口其他一律关闭。容器之间通信用的是内部网络不需要对外开端口。# 更新系统并设置时区 sudo apt update sudo apt upgrade -y sudo timedatectl set-timezone Asia/Shanghai # 配置防火墙只放行必要端口 sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow 22/tcp sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable系统调优方面主要是调整文件描述符限制和内核网络参数。容器跑多了之后文件描述符很容易不够用提前把限制调高能避免很多莫名其妙的报错。网络参数主要是开启IP转发和调整连接跟踪表大小这对容器网络和反向代理的稳定性有帮助。3.2 容器运行时的安装与配置容器运行时的安装我推荐用官方脚本省去手动配置软件源的麻烦。装完之后要做几件配置设置开机自启、配置日志轮转防止日志把磁盘撑爆、配置镜像加速如果拉取镜像慢的话。日志轮转这个特别重要默认配置下容器日志会无限增长一个跑得久的容器日志能吃掉几十G这是新手最容易踩的坑。# 安装容器运行时 curl -fsSL https://get.docker.com | sh # 配置日志轮转限制单个日志文件大小和保留数量 sudo tee /etc/docker/daemon.json -EOF { log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 }, default-address-pools: [ { base: 172.30.0.0/16, size: 24 } ] } EOF sudo systemctl restart docker sudo systemctl enable docker上面配置里的地址池设置是为了避免容器网络和ECS内网网段冲突。有些云厂商的内网网段会跟容器默认网段重叠导致容器访问不了内网服务提前改掉能省很多排查时间。日志配置里单个文件最大10M保留3个也就是每个容器最多占30M日志空间这个量对排查问题足够了。3.3 代码托管服务的部署代码托管服务我用容器方式部署数据目录挂载到宿主机上这样升级容器不会丢数据。部署的时候要注意几个参数SSH端口要映射出来方便推送代码Web端口映射到宿主机的一个内部端口不直接对外由反向代理转发数据目录要指定到有足够空间的路径。docker run -d \ --name git-service \ --restart always \ -p 2222:22 \ -p 3001:3000 \ -v /data/git/data:/data \ -v /etc/timezone:/etc/timezone:ro \ -v /etc/localtime:/etc/localtime:ro \ git-service-image:latest部署完之后第一件事是创建管理员账号然后关闭公开注册。这个服务默认可能允许任何人注册如果不关掉公网上很快就会有陌生人注册进来。关闭注册在管理后台的设置里找到注册相关选项关掉即可。接着创建项目仓库配置好SSH密钥测试一下能不能正常推送代码。注意代码托管服务的SSH端口我映射成了2222因为宿主机的22端口已经被系统SSH占用了。推送代码的时候要指定端口或者在本地的SSH配置里给这个主机单独配置端口不然会连到系统SSH上去。4. CI流水线的搭建与核心配置4.1 CI引擎的部署与Runner注册CI引擎分两部分服务端和Runner。服务端负责接收Webhook、调度任务、展示结果Runner负责实际执行构建。在单机场景下这两个可以跑在同一台机器上服务端用容器跑Runner也用一个容器跑Runner通过挂载宿主机的容器运行时socket来执行构建任务。# 部署CI服务端 docker run -d \ --name ci-server \ --restart always \ -p 3002:3000 \ -v /data/ci/server:/data \ -v /etc/timezone:/etc/timezone:ro \ ci-server-image:latest # 注册Runner docker run -d \ --name ci-runner \ --restart always \ -v /data/ci/runner:/data \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /data/builds:/builds \ ci-runner-image:latestRunner挂载容器运行时socket这个操作要谨慎理解它意味着Runner容器内的进程可以直接操作宿主机的容器。这是必要的因为构建任务需要构建镜像、启动临时容器来跑测试。但这也意味着Runner的权限很高所以千万不要把不可信的代码交给它构建。对于自己的项目这没问题如果要做开源项目的CI得用更隔离的方案。4.2 流水线配置文件怎么写流水线的核心是一个YAML配置文件放在项目根目录。这个文件描述了什么时候触发、在什么环境里跑、跑哪些步骤。我以一个典型的Web应用为例流水线分三个阶段构建、测试、部署。stages: - build - test - deploy variables: IMAGE_TAG: $CI_COMMIT_SHORT_SHA build-job: stage: build script: - docker build -t myapp:$IMAGE_TAG . only: - main test-job: stage: test script: - docker run --rm myapp:$IMAGE_TAG npm run test only: - main deploy-job: stage: deploy script: - docker stop myapp || true - docker rm myapp || true - docker run -d --name myapp --network app-net myapp:$IMAGE_TAG only: - main这个配置里有几个关键点值得展开说。第一镜像标签用commit的短哈希这样每个版本都有唯一标识回滚的时候直接指定旧标签就行不用去猜哪个镜像是哪个版本。第二部署步骤里先stop再rm加了|| true是为了第一次部署时容器不存在也不报错。第三所有任务都限定只在main分支触发避免开发分支的提交把生产环境搞乱。4.3 Webhook打通代码提交与自动构建流水线配置好了还得让代码提交能自动触发它。这靠的是代码托管服务的Webhook功能当有代码推送到仓库时它向CI服务端发一个HTTP请求CI服务端收到后启动对应的流水线。配置Webhook在代码托管仓库的设置里填上CI服务端的触发地址和密钥。这里有个常见的坑CI服务端跑在容器里代码托管服务也在容器里Webhook的地址如果填公网域名请求会绕一圈从公网回来慢而且可能因为防火墙配置不通。正确的做法是让两个容器在同一个容器网络里Webhook地址直接填CI服务端的容器名加端口走内部网络又快又稳。# 创建共享网络把所有相关容器都接进来 docker network create app-net # 把已有容器接入网络 docker network connect app-net git-service docker network connect app-net ci-server docker network connect app-net ci-runner接入同一个网络之后容器之间可以直接用容器名互相访问。比如CI服务端的地址就是http://ci-server:3000代码托管服务发Webhook的时候填这个地址就行。这个细节看起来小但能避免很多Webhook发了但收不到的排查时间。5. 反向代理与自动化部署的落地5.1 Nginx反向代理配置与证书自动化反向代理是整套方案对外的唯一入口所有域名都指向它它再按域名把请求转发给对应的后端容器。Nginx我也用容器跑配置文件挂载到宿主机改配置不用进容器。server { listen 80; server_name app.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name app.example.com; ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem; location / { proxy_pass http://myapp:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }证书这块我用自动化工具申请和续期它会在证书快过期时自动续续完自动重载Nginx。配置好之后基本不用管省心。要注意的是证书申请需要验证域名所有权用HTTP验证方式的话Nginx得先能响应80端口的请求所以证书申请要在Nginx跑起来之后做。提示proxy_set_header这几行一定要加尤其是X-Forwarded-Proto。后端应用如果靠这个头判断请求是HTTP还是HTTPS不加的话应用会以为自己在HTTP环境里生成的链接全是http的导致混合内容问题。5.2 部署脚本的编写与回滚机制自动部署的核心是一个部署脚本CI流水线在构建测试通过后调用它。这个脚本要做的事拉取或构建新镜像、停掉旧容器、启动新容器、检查新容器是否健康、如果健康就保留不健康就回滚到上一个版本。#!/bin/bash set -e APP_NAMEmyapp NEW_TAG$1 NETWORKapp-net # 记录当前运行的镜像标签用于回滚 CURRENT_TAG$(docker inspect --format{{.Config.Image}} $APP_NAME 2/dev/null | cut -d: -f2 || echo none) echo 当前版本: $CURRENT_TAG, 准备部署: $NEW_TAG # 启动新容器用不同名字先不删旧的 docker run -d --name ${APP_NAME}_new --network $NETWORK ${APP_NAME}:$NEW_TAG # 等待并检查健康状态 sleep 10 if docker ps | grep ${APP_NAME}_new | grep -q Up; then echo 新版本启动成功切换流量 docker stop $APP_NAME 2/dev/null || true docker rm $APP_NAME 2/dev/null || true docker rename ${APP_NAME}_new $APP_NAME else echo 新版本启动失败保留旧版本 docker rm -f ${APP_NAME}_new exit 1 fi这个脚本的精髓在于先起新的确认健康再停旧的。很多粗糙的部署脚本是先停旧的再起新的中间有一段服务不可用的窗口而且新版本起不来就直接服务中断了。用我这个顺序最坏情况是新版本起不来旧版本还在跑服务不受影响。健康检查这里我用的是简单的容器是否在运行更严谨的做法是加一个HTTP健康检查接口脚本去请求这个接口返回200才算健康。5.3 磁盘清理与资源监控单机方案跑久了磁盘是第一个报警的。镜像、构建缓存、旧日志这三样是磁盘杀手。我配置了一个定时任务每周清理一次删除悬空镜像、清理超过一定天数的构建缓存、清理旧日志。#!/bin/bash # 清理悬空镜像 docker image prune -f # 清理构建缓存保留最近7天的 docker builder prune -f --filter until168h # 清理停止超过7天的容器 docker container prune -f --filter until168h # 清理系统日志 journalctl --vacuum-time7d资源监控我用一个轻量级的监控容器它能采集CPU、内存、磁盘、网络数据并展示成图表。配置好之后能直观看到每个容器的资源占用哪个服务吃内存一目了然。我还会配一个磁盘使用率的告警超过80%就发通知这样能在磁盘满之前及时处理。清理对象清理命令建议频率注意事项悬空镜像image prune -f每周不影响正在使用的镜像构建缓存builder prune每周保留近期缓存加速构建停止的容器container prune每周确认没有需要保留的容器系统日志journalctl vacuum每周保留7天足够排查问题6. 常见问题排查与实战避坑经验6.1 构建失败的高频原因与排查路径构建失败是日常遇到最多的问题我把常见原因归成几类。第一类是依赖拉取失败通常是网络问题或者依赖源地址变了排查方法是进到构建容器里手动执行拉取命令看报错。第二类是磁盘空间不足构建过程中写临时文件把盘写满了排查方法是构建前先看磁盘剩余空间。第三类是内存不足被系统杀掉构建大项目时特别容易发生排查方法是看系统日志里有没有OOM记录。# 查看系统OOM记录 dmesg | grep -i out of memory # 查看容器退出原因 docker inspect --format{{.State.ExitCode}} 容器名 # 查看构建过程中的实时资源占用 docker stats --no-stream内存不足这个问题我踩过好几次。有一次构建一个前端项目Node进程直接吃了3G内存把整台机器拖垮了。解决办法是给构建任务加内存限制同时在构建命令里设置Node的内存上限让它不要无限制地吃内存。这个参数在构建脚本里通过环境变量传进去就行。6.2 部署后服务异常的定位思路部署完了服务起不来或者起来了但访问报错这个排查要按层次来。先看容器在不在跑docker ps看状态。在跑的话看日志docker logs看有没有报错。日志没问题的话从反向代理容器里测试能不能访问到后端容器docker exec进Nginx容器用curl请求后端容器名加端口。这一步能区分是网络问题还是应用问题。如果反向代理容器里能访问到后端但外部访问不了那问题在Nginx配置或者防火墙。检查Nginx配置有没有语法错误检查防火墙有没有放行端口。如果反向代理容器里也访问不到后端那问题在容器网络检查两个容器是不是在同一个网络里容器名对不对。注意排查网络问题时永远从最内层往外层查。先确认应用本身正常再确认容器间网络通再确认反向代理转发正常最后确认外部访问正常。反过来查会浪费很多时间。6.3 几个让我印象深刻的坑第一个坑是时区问题。容器默认用UTC时间导致日志时间跟本地时间差8小时排查问题时对不上时间线。解决办法是把宿主机的时区文件挂载进容器或者在容器里设置TZ环境变量。这个坑不大但很烦建议一开始就配好。第二个坑是容器重启策略。我一开始没配重启策略结果服务器重启之后所有容器都没起来服务全挂。后来给所有容器都加了--restart always服务器重启后容器自动恢复。这个参数一定要加血的教训。第三个坑是镜像标签管理。早期我用latest标签每次构建都覆盖latest结果想回滚的时候发现旧镜像已经被覆盖了根本回不去。后来改成用commit哈希做标签每个版本都留着回滚就是指定旧标签重新部署几秒钟的事。这个改动让部署的安全感提升了一个档次。第四个坑是Webhook的密钥验证。一开始没配密钥结果有人扫到了Webhook地址随便发请求就能触发构建虽然没造成损失但很危险。后来配上了密钥验证CI服务端会校验请求里的密钥不对就拒绝。这个安全措施必须加尤其是服务暴露在公网上的时候。6.4 常见问题速查表现象可能原因排查命令解决方向构建卡住不动依赖拉取慢或卡死进构建容器手动执行换依赖源或加超时构建被中断内存不足被OOMdmesg查OOM记录限制构建内存部署后502后端容器没起来docker ps看状态看容器日志排错外部访问不了防火墙或Nginx配置从内层往外层测检查端口和配置磁盘满镜像日志堆积df -h看使用率执行清理脚本证书过期自动续期没生效看续期工具日志手动续期并修配置7. 方案扩展与个人实践体会7.1 从单机到多机的平滑演进路径这套单机方案不是终点业务长大了自然要扩展。好消息是它的演进路径很平滑因为所有组件都是容器化的迁移的时候把容器搬到新机器上就行。第一步扩展通常是把构建和运行分开构建放到一台专门的机器上运行留在原机器这样构建不再影响生产服务。第二步是把数据库独立出去用云厂商的托管数据库省掉自己维护数据库的麻烦。第三步才是上容器编排平台把多个服务分散到多台机器上。每一步扩展都不需要推翻重来只是把某个组件从这台机器上挪走。这就是容器化带来的好处组件之间的耦合很松迁移成本低。我建议不要一上来就搞复杂架构先用单机方案把流程跑通等真正遇到瓶颈了再针对性扩展这样每一步的投入都是有的放矢的。7.2 我在这套方案上的一些真实体会跑了这套方案大半年最大的感受是自动化带来的心理负担减轻远超预期。以前每次发版都要紧张兮兮地手动操作生怕哪一步敲错命令。现在提交代码之后该干嘛干嘛几分钟后收到构建成功的通知服务已经更新好了。这种确定感是小团队特别需要的它让发版从一件需要专门安排时间做的事变成了顺手就完成的事。另一个体会是简单方案的可维护性被严重低估。我见过太多团队为了追求架构先进性搭了一套复杂系统结果没人完全搞得懂出问题只能重启大法。这套单机方案每个组件我都清楚它在干什么出问题能快速定位升级维护心里有底。对于小团队来说一个自己能完全掌控的简单方案比一个半懂不懂的复杂方案价值高得多。最后分享一个小技巧把这套方案的所有配置——包括容器启动命令、Nginx配置、流水线配置、部署脚本——全部用版本管理起来放在一个专门的仓库里。这样换机器的时候把仓库拉下来按文档跑一遍脚本半小时就能重建整套环境。我实际迁移过一次从旧机器到新机器全程不到一小时服务中断时间只有几分钟。这种基础设施即代码的做法是这套方案能长期稳定运行的底气所在。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑