云服务器部署全流程详解:从SSH连接到Nginx反向代理
说实话我刚接触云服务器的时候第一反应是这东西离我挺远的。后来自己在芯飞云服务器上把一个 Web 项目从零部署到上线踩了一堆坑之后发现整条链路其实就那么几件事连上机器、把代码放上去、让程序跑起来、再把端口对到公网。这篇文章就把这条链路完整写一遍。如果你是第一次买云服务器或者打算把自己写的项目真正部署到线上这篇文章应该能帮你少走不少弯路。这篇文章不止讲“怎么敲命令”我会把每一步背后的原理也讲清楚比如为什么要用普通用户不用 root、为什么明明服务起来了但浏览器就是访问不了、为什么进程跑了一会儿就没了。服务器运维这东西说白了就是“约定 检查”你只要知道每一步在做什么、怎么验证后面基本不会慌。另外说明一下我用的机器是芯飞云的一台 2核4G 的云服务器系统选的 Ubuntu 22.04 LTS。这套流程放在阿里云、腾讯云、AWS 上基本都是通用的差异最多就在控制台的按钮位置和安全组配置上。1. 先搞清楚这台机器能干什么1.1 买服务器不等于能跑程序先理解“远程机器”这件事很多人第一次拿到云服务器打开控制台看到一堆参数就开始发懵其实你不用管那么多。云服务器本质上就是一台放在机房里的电脑你通过 SSH 远程登录进去然后用命令行操作它。跟你在自己电脑上开终端不一样的地方在于它不是你的电脑你碰不到它的实体所有操作都得靠网络传输。这就引出一个核心概念端口。一台服务器上可能跑着好几个程序比如一个 Web 服务占 80 端口一个数据库占 3306 端口一个 SSH 登录占 22 端口。公网用户访问你的服务器时必须通过“IP 端口”才能找到对应的程序。所以后面你部署完项目发现访问不了八成是下面几个原因之一程序没起来、端口没放开、防火墙拦了、进程挂了。还有一个概念叫安全组。这是云厂商在虚拟机外面加的一道门槛就跟小区门卫一样。你服务器里的防火墙是“室内门锁”安全组是“小区大门门禁”两道都得放行外面的人才能进来。很多新手只在服务器里开了防火墙端口忘了去云控制台配安全组结果怎么都连不上这个坑我后面会专门讲。1.2 系统镜像选 Ubuntu 还是 CentOS其实不用纠结我当年第一次买服务器在系统选择上磨了半小时现在想起来有点好笑。熟手一般都有自己的偏好但新手我统一建议选 Ubuntu LTS 版本比如 22.04 或者 24.04。原因是 Ubuntu 的资料最多出了问题搜索解决方案最容易命中而且 apt 包管理器用起来比 yum 直观不少。你说 CentOS 怎么样也不是不行但 CentOS 官方已经停止维护了很多源现在用起来挺折腾。Debian 也挺稳但社区资料比 Ubuntu 少一些。另一个原因是目前 AI 相关的工具链和 Docker 官方文档默认示例基本都基于 Ubuntu你照着文档操作时不会因为发行版差异换来换去。镜像选完之后系统会自动给你生成一个 root 密码或者让你自己设置。root 是 Linux 里的超级管理员账号权限最大操作无限制。但我要提醒你日常操作不要一直用 root后面我会专门讲怎么创建普通用户。这跟 Windows 里没事别老用 Administrator 一样权限越大误操作的成本越高。2. 连上服务器把你的电脑变成远程开发机2.1 SSH 连接从密码登录到密钥登录第一次连服务器最直接的办法就是在本地终端执行ssh root你的服务器IP输入密码就能进去。如果你用的是 Windows直接用 PowerShell 或者 Windows Terminal 都行新版系统自带 OpenSSH 客户端不用额外装软件。Mac 和 Linux 更不用说自带终端就行。输密码登录当然能用但不安全也不方便。每次连接都要输一遍密码不说密码如果太简单还容易被暴力破解。我建议你第一次登进去之后立刻配置 SSH 密钥登录。密钥登录的原理可以这么理解它相当于一把“锁”和一把“钥匙”公钥放在服务器上私钥留在本地。你连接时服务器会验证你手里的私钥能不能对上公钥对上就直接放行不需要密码。配置方法分两步。第一步在本地生成密钥对ssh-keygen -t ed25519 -C 你的备注一路回车就行默认会在~/.ssh/下生成id_ed25519私钥和id_ed25519.pub公钥。第二步把公钥放到服务器上ssh-copy-id root你的服务器IP这个命令会把本地的公钥自动追加到服务器的~/.ssh/authorized_keys文件里。之后你再 SSH 登录就不需要输密码了。如果你用的是 Windows没有ssh-copy-id命令也没关系手动把公钥内容复制到服务器的~/.ssh/authorized_keys文件里就行效果一样。配好之后我建议你顺手做一件事把服务器的 root 密码登录关掉只允许密钥登录。编辑/etc/ssh/sshd_config找到PasswordAuthentication改成no然后执行systemctl restart sshd。这样你的服务器就只能通过密钥登录了安全性直接上一个台阶。但要提醒改之前务必确认密钥登录已经生效别把自己锁在外面了。2.2 用 VS Code Remote-SSH 把服务器变成“本地开发环境”如果你要在服务器上写代码、改代码纯靠命令行编辑器不是不行但效率确实低。这里我要强烈推荐 VS Code 的 Remote-SSH 插件。装上这个插件之后你可以直接在本地 VS Code 窗口里打开服务器上的文件夹写代码、看文件树、用终端体验跟开发本地项目几乎一模一样。你本地的 VS Code 只是一个“客户端界面”真正执行代码的是服务器所有的计算、存储都在服务器上。具体操作很简单在 VS Code 里按CtrlShiftP输入Remote-SSH: Connect to Host填服务器的 IP 或者你在 SSH config 里配好的别名回车就能连上。第一次连接会自动在服务器上安装 VS Code Server 组件等一会儿就好。如果你是前端开发这个组合尤其好用。前端项目在本地开发完要部署到服务器上你完全可以通过 VS Code Remote-SSH 直接编辑服务器上的文件、跑构建命令、看运行日志。整套流程都在这一个窗口里完成省去来回切换工具的成本。2.3 登录后必做的三件基础优化不管服务器买来要跑什么程序我建议你登录之后先做这几件事后面能省掉大量莫名其妙的坑。第一件事更新系统源和软件包apt update apt upgrade -y这一步会把系统自带的软件源缓存刷新一遍并升级所有已安装的软件到最新版本。时间可能比较长但值得等因为它能顺便把一些已知的安全漏洞补上。如果发现 apt 源下载速度很慢可能是源的问题可以换成国内镜像源后面在排查章节我会写。第二件事设置服务器时区。很多人都会忽略这个然后程序里打印日志的时间总是对不上。执行timedatectl set-timezone Asia/Shanghai然后输入date确认时间输出是不是北京时间。时间同步还有个关键点云服务器一般默认装了systemd-timesyncd它会自动跟时间服务器同步不用额外装 NTP 工具。如果你发现服务器时间还是不准可以用timedatectl status查看NTP synchronized是不是yes。第三件事创建一个普通用户并用它来日常操作adduser deploy usermod -aG sudo deploy第一条命令会创建用户deploy并设置密码第二条命令把它加入 sudo 组让它有临时提权的能力。之后日常操作都用这个用户登录需要执行管理员操作时前面加sudo。这样万一你某条命令敲错影响范围最多就在普通用户权限内不会把系统搞坏。3. 把代码跑起来三种最常见的部署方式3.1 方式一后台进程直接跑适合脚本、小服务、临时验证最原始的方式就是让程序直接在服务器上跑。比如你写了一个 Python 脚本app.py按道理执行python3 app.py就能跑但问题是你关掉 SSH 终端进程就跟着没了。程序打印的日志直接输出到终端没法留存。程序崩溃之后没人把它拉起来。所以生产环境不能这么干。你想让一个进程在后台稳定运行至少要用nohup或者systemd。nohup是 “no hang up” 的缩写意思是终端关闭后进程不挂断配合可以放到后台nohup python3 app.py app.log 21 这条命令的意思是后台运行app.py把标准输出1和错误输出2都重定向到app.log文件里。之后你想看日志就tail -f app.log想停进程就先ps aux | grep app.py找到进程号再kill 进程号。但nohup有个问题服务器重启之后这个进程不会自动起来。如果你项目要长期运行我更推荐用systemd来管。它会把你的程序注册成一个系统服务开机自启、崩溃自动重启、日志统一管理全部安排得明明白白。举个例子假如我有一个 Node.js 项目入口文件是server.js我想让它以服务方式跑。在/etc/systemd/system/myapp.service文件里写[Unit] DescriptionMy Node.js App Afternetwork.target [Service] Userdeploy WorkingDirectory/home/deploy/myapp ExecStart/usr/bin/node server.js Restartalways RestartSec3 EnvironmentNODE_ENVproduction [Install] WantedBymulti-user.target然后执行sudo systemctl daemon-reload sudo systemctl enable myapp sudo systemctl start myappenable是设置开机自启start是立即启动。改完代码之后重启服务用sudo systemctl restart myapp看日志用sudo journalctl -u myapp -f。这套组合拳比nohup不知道高到哪里去了我现在的服务基本全是用 systemd 管的。3.2 方式二用 Docker 部署前端、后端统一打包省心这几年 Docker 火不是没道理的它解决了部署环节最大的痛点环境不一致。你在本地跑得好好的代码换到服务器上就各种报错版本不对、依赖缺失、路径不一样原因就是环境不同。Docker 把应用和它的运行环境打成一个镜像镜像在哪儿跑都一样这就彻底解决了环境差异问题。以我最近上线的一个项目为例目录结构是这样的myapp/ ├── backend/ │ ├── Dockerfile │ └── src/ ├── frontend/ │ ├── Dockerfile │ └── src/ └── docker-compose.yml后端 Dockerfile 大概长这样FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install --production COPY . . EXPOSE 3000 CMD [node, server.js]前端如果是 Vue/React 项目部署方式比较特殊。前端构建出来是一堆静态文件不需要 Node.js 运行时用 Nginx 托管就行。Dockerfile 可以写成两段# 第一阶段构建 FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # 第二阶段用 Nginx 跑静态文件 FROM nginx:alpine COPY --frombuilder /app/dist /usr/share/nginx/html EXPOSE 80 CMD [nginx, -g, daemon off;]这种写法叫多阶段构建。第一阶段用来编译生成静态文件第二阶段是干净的 Nginx 镜像只把构建产物复制进去。好处是最终镜像体积小了很多而且不用把 Node.js 和源码都带进生产环境安全性和部署速度都更好。如果你项目同时有前端、后端、数据库那一定得用 Docker Compose。在docker-compose.yml里一次定义所有服务version: 3.8 services: backend: build: ./backend ports: - 3000:3000 restart: always frontend: build: ./frontend ports: - 80:80 depends_on: - backend然后在服务器上执行docker compose up -d所有服务就都跑起来了。-d是后台运行的意思。以后每次更新代码重新 build 一下再up -d就能生效。这套流程对“前端怎么使用 docker 部署项目上线”这个疑问算是给出了一个比较完整的答案先构建静态文件再用 Nginx 托管最后用 Compose 把前后端串起来。3.3 方式三Nginx 反向代理让服务“正式见客”程序跑起来之后直接访问也不安全服务器上有太多服务需要统一入口管理。这时候就要请出 Nginx。它最常用的角色是反向代理用户访问服务器的 80 端口或 443 端口Nginx 再根据请求路径把流量转发给对应的后端程序。举个例子我的后端 API 跑在 3000 端口我不可能让用户直接访问http://IP:3000又难看又暴露细节。我只想让他们访问http://IP/api剩下的交给 Nginx 处理。安装 Nginxsudo apt install nginx -y然后编辑站点配置文件/etc/nginx/sites-available/myappserver { listen 80; server_name yourdomain.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这个配置的意思是所有访问yourdomain.com的请求都会被转发到本机的 3000 端口。关键的三行proxy_set_header是干嘛的它们是让后端程序知道“真正访问的人是谁”。如果没有这几行后端程序会认为所有请求都来自 Nginx 所在的 IP拿到错误的客户端地址有的框架还会因此生成错误的跳转链接。启用配置文件sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginxnginx -t是测试配置语法任何 nginx 配置改动之前都必须跑一次有报错别急着 reload先看清楚错误再改。这套反向代理方案能让你的服务结构变得清晰外面看只有一个 80/443 端口里面可以挂无数个服务通过不同的server_name或者location路径来区分。如果你买好了域名记得去 DNS 服务商那儿把域名解析到服务器 IP 上然后在 Nginx 配置里填上域名。之后用sudo certbot --nginx可以免费申请 SSL 证书让网站支持 HTTPS。这个命令会自动帮你改 Nginx 配置基本上是无脑操作但我建议你在执行之前先备份一下配置文件万一 certbot 改出了玄学问题你还有机会还原。4. 项目上线的完整流程与检查清单4.1 从代码提交到上线一个标准的发布流程前面讲了部署方式现在把整条发布的流程串起来讲讲。一次标准的发布我一般会走这几步第一步本地开发完成代码推到 Git 远程仓库。如果你还没搭 Git 服务器可以用 GitHub 或者 GitLab 的免费仓库。如果你想自己搭一个 Git 服务器也完全可以参考“git 进阶之搭建 git 服务器”的教程在一个 Linux 机器上用git init --bare建一个裸仓库配合 SSH 公钥就能实现私有 Git 服务过程不复杂。第二步登录服务器拉取代码。进入项目目录cd /home/deploy/myapp git pull origin main第三步安装依赖并构建。这一步看项目语言而定。Node.js 项目执行npm install npm run buildPython 项目执行pip install -r requirements.txt。如果你用了 Docker这一步会被镜像构建替代执行docker compose up -d --build就好。第四步重启服务。如果程序直接跑在机器上用 systemd 的就sudo systemctl restart myapp用 Docker 的就docker compose restart。重启完之后一定要看一眼状态systemctl status myapp或者docker compose ps确认服务是active (running)而不是failed。第五步外部验证。在本地浏览器访问你的域名或公网 IP确认页面能正常打开。如果页面打不开先不要慌按照后面排查章节逐个排除。4.2 服务器巡检上线之后怎么确认真的一切正常程序上线后不能撒手不管。服务器是有寿命的磁盘可能会满内存可能会爆进程可能会挂。我给自己定了一个习惯上线后每天至少看一眼这几个指标。用top或者htop看 CPU 和内存占用。正常情况 CPU 占用不会长期 100%内存也不会一直涨到顶。如果你发现某个进程内存占用异常高多半有内存泄漏。用df -h看磁盘空间。日志文件如果长期不清理很容易把磁盘塞满磁盘一满很多程序就写不了数据直接报错。我吃过大亏所以现在会给日志目录单独挂一个大分区并设置logrotate自动切割日志。用uptime看系统负载。这条命令会输出最近 1 分钟、5 分钟、15 分钟的平均负载。负载可以粗略理解成“有多少任务在排队等 CPU”如果负载数值长期大于 CPU 核心数说明机器忙不过来了要考虑升配或者优化代码。4.3 上线后的日志管理不懂这个等于白跑日志是排查问题最重要的线索但日志如果没人管它就会变成一个“磁盘杀手”。我见过一台服务器因为日志文件太大导致磁盘写满数据库直接崩了的案例。所以日志一定要做切割和清理。Linux 自带的logrotate就是干这个的。它按天、按大小自动切割日志删除多余的旧日志。配置一个日志规则很简单在/etc/logrotate.d/myapp写入/home/deploy/myapp/logs/*.log { daily rotate 7 compress missingok notifempty }意思是每天切割一次保留最近 7 份旧的压缩存档。如果你的程序日志特别多可以把rotate 7调大一点或者调小一点按实际需要来。日志这东西平时看着不起眼真出问题的时候它就是唯一的破案线索别等磁盘满了才发现。5. 常见问题与排查技巧实录5.1 SSH 连接不上从头到尾查一遍这是新手遇到最多的第一个问题。买了服务器SSH 就是连不上网络不通、认证失败、超时各种报错。排查思路按这个顺序来第一步检查云控制台的安全组规则。这是我在芯飞云上踩过最典型的坑安全组里没放行 22 端口外面自然连不上。在控制台找到“安全组”确认入方向规则里有TCP:22来源建议限制为你自己的 IP可以用0.0.0.0/0但不太安全。第二步确认服务器里的 sshd 服务在运行sudo systemctl status ssh如果状态不是active (running)用sudo systemctl start ssh启动它再执行sudo systemctl enable ssh设置开机自启。第三步本地用telnet IP 22或者在 SSH 命令后面加-v参数看详细输出。ssh -v rootIP会打印客户端和服务器的握手过程你看它卡在哪一步能定位出大部分问题。比如它显示Connection timed out那就是网络不通八成是安全组或者防火墙的问题显示Permission denied那就是认证失败检查账号密码或者密钥是否配对。第五步检查服务器本地防火墙。Ubuntu 默认不一定开了 ufw但如果你自己启用了记得放行 22 端口sudo ufw allow 22。5.2 端口不通安全组、防火墙、服务状态三者缺一不可像这类报错其实有固定套路ss -tlnp | grep 3000这句命令可以清晰地看到当前机器有哪些端口正在监听。如果端口没有出现在列表里说明程序压根没起来问题出在应用本身。如果端口在监听列表里但你从外部还是访问不了那问题基本出在网络层也就是防火墙或者安全组。Ubuntu 用的是 ufw 来管理防火墙。查看状态用sudo ufw status如果状态是active检查有没有放行对应端口sudo ufw allow 3000。如果状态是inactive说明本机防火墙没拦截那就大概率是安全组的问题要回到云控制台检查安全组规则。很多排查链路里还有个容易忽略的点如果你的服务是绑定在127.0.0.1上的外部用户永远访问不到因为127.0.0.1表示只有本机能访问。绑定的时候要看清楚开发环境的默认监听地址可能是127.0.0.1上线了要改成0.0.0.0表示所有网卡都可以访问。Docker 部署还要注意端口映射ports里3000:3000和127.0.0.1:3000:3000的区别就是前者对外映射后者只对内网映射。如果是这种配置外网访问不到属于正常行为。5.3 服务莫名其妙挂了先看日志再查内存最常见的情况服务跑着跑着没了systemctl status显示inactive (dead)有的还会显示Main process exited加一个退出码。很多新手第一步是赶紧把服务拉起来但我要严肃说一句只拉起来不排查等于埋雷。拉起来之前先看日志sudo journalctl -u myapp -n 100这段命令会显示服务最近 100 行日志。看到报错之后再搜索通常能定位到问题。我遇到过的常见退出原因有这么几种内存不足OOMOut Of Memory。程序占用的内存超过系统可用内存Linux 内核会杀掉最耗内存的进程在日志里能看到oom-killer相关记录。解决方法是升级内存配置或者优化代码里的内存占用比如 Node.js 可以给 V8 设置内存上限NODE_OPTIONS--max-old-space-size512。绑定的端口被其他进程占用了。换一个端口或者先找到占用进程lsof -i:3000跟它协商一下。代码崩溃。这种就要看具体的报错信息了。有时候不是必现的崩溃而是某个请求触发了 bug这时候建议在代码里加一些 try-catch 或者错误上报让问题暴露得早一点。如果你用的 systemd 且设置了Restartalways进程崩了之后 systemd 会自动帮你拉起来这能减少“半夜爬起来重启服务”的情况但日志还是要及时看因为频繁自动重启说明代码本身存在缺陷。5.4 Ubuntu 软件源 apt 更新慢或报错在国内服务器上执行apt update慢得让人怀疑人生这种情况很常见。原因很简单Ubuntu 默认的软件源服务器在境外国内访问延迟很高。解决方案是换成国内镜像源。Ubuntu 的软件源配置文件在/etc/apt/sources.list新版系统还会读取/etc/apt/sources.list.d/目录下的文件。我建议修改之前先备份sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak然后编辑文件把archive.ubuntu.com替换成国内镜像域名再用sudo apt update验证一下。镜像源有好多家可选比如阿里云、华为云、腾讯云的镜像站都有。每家都有配置说明照着复制就行。这里提醒一句改完源之后如果还是报错注意看一下错误里提到的域名或者密钥信息有时候是因为缺少 GPG 公钥需要单独把公钥导入。5.5 服务器时间不对日志时间对不上服务器默认时区可能是 UTC也就是比北京时间慢 8 个小时。这时候本地浏览器看到的报错时间和服务器日志里的时间对不上排查问题非常痛苦。解决方案就是我在文章开头说的那个命令sudo timedatectl set-timezone Asia/Shanghai再顺便确认一下时间同步是否正常timedatectl status重点看两行Time zone是不是Asia/ShanghaiSystem clock synchronized是不是yes。如果NTP synchronized是no说明系统时间没同步成功可以检查一下网络能不能访问到ntp.ubuntu.com或者手动执行sudo systemctl restart systemd-timesyncd重启时间同步服务。时间不同步这事看着小但对依赖时间戳的应用来说问题一出现就非常奇怪比如登录 token 莫名失效、日志顺序错乱。我当年排查过一个诡异问题就是服务器自动同步时间后导致一个状态机应用的时间判断错乱最后把时区和 NTP 同步都配好了才解决。6. 最后再分享两个小技巧6.1 把常用命令写成脚本别每次都手敲部署操作经常是重复的。更新代码、装依赖、重启、看日志每次都要敲好几条命令。我建议你把这些命令写成一个 shell 脚本放到服务器上比如/home/deploy/deploy.sh#!/bin/bash cd /home/deploy/myapp git pull origin main npm install --production npm run build sudo systemctl restart myapp echo 部署完成然后执行chmod x /home/deploy/deploy.sh以后部署只需要跑一次这个脚本就完事。如果你用了 Docker就把里面的命令替换成docker compose up -d --build就行。这种脚本看着简单但能让每次发布的操作都完全一致减少人为遗漏。6.2 上线前做一次压力测试如果你项目要正式对外提供服务我强烈建议在上线前做一次简单的压力测试。装一个abApache Benchsudo apt install apache2-utils -y然后对你的服务跑一下并发请求测试ab -n 1000 -c 100 http://你的域名/-n 1000代表总共发 1000 个请求-c 100代表 100 个并发。跑完之后会展示请求失败率和平均响应时间。如果失败率很高或者响应时间异常那就说明服务器配置或者代码性能有瓶颈趁早优化别等真上线了被用户投诉。我之前有个项目上线前没做压测结果上线后第一个小时就被打爆那叫一个狼狈。从那以后我就学乖了不管大项目小项目上线前至少压测一轮不求性能多好但至少心里有个底。写在最后我是在几次“部门切换环境配置”和“新服务器踩坑”的过程中一点点把这些经验攒起来的。说实话服务器部署这事看起来知识点很多但真正用到的核心概念就那几个SSH 登录、进程管理、端口转发、日志排查。把这几样练熟你就能从容应对绝大多数日常部署需求。林林总总写了这么多其实我最想告诉你的是部署这事儿千万别怕它不像你想象的那么难但也确实不能瞎敲命令。每一次操作之前想清楚“这个命令是干嘛的”“坏了怎么回滚”长期下来就能从“照着教程敲”变成“自己能设计部署方案”的人。希望这篇文章能成为你从买下服务器到成功上线项目之间的一座桥。