ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从SSH连接到项目上线:云服务器部署完整实战指南

从SSH连接到项目上线:云服务器部署完整实战指南 很多人第一次买云服务器死磕了三四个小时终于用 SSH 连上了机器看到 root 开头的命令行提示符心里那叫一个激动——然后呢然后就没有然后了。这是我在带新人时见到的最高频场景。说明能连上和项目真正跑起来之间还隔着环境配置、代码同步、进程托管、域名接入这一整条链路。这篇文章我打算用一台芯飞云服务器当例子把从第一次登录到项目对外可访问的全过程完整捋一遍。无论你是第一次部署个人博客的开发者还是公司里刚接手服务器运维的新人照着这条路走一遍基本就不会再对着黑乎乎的终端窗口发懵了。1. 服务器买回来第一步先把系统和入口收拾利索很多教程上来就让你敲apt install nginx但我建议你先别急着装任何软件。新服务器就像一套刚交付的毛坯房你得先确认水电、锁好门窗再考虑怎么装修。这一步花十几分钟能省掉后面无数的安全问题。1.1 登录云控制台后的初始设置在芯飞云后台购买服务器时最关键的选项是镜像系统。我的建议是优先选 Ubuntu 22.04 LTS 或 Debian 12这两个系统的软件源更新及时社区资料多遇到问题搜一下基本都有答案。CentOS 7 已经停止维护了新项目没必要再碰。购买完成后第一件事是重置 root 密码。云厂商的初始密码通常是一串随机字符你在控制台的实例列表里找到重置密码入口设置一个自己能记住的强密码然后用它完成第一次登录。登录方式有两种控制台自带的网页终端VNC适合应急场景比如本地网络连不上 22 端口时救急用本地 SSH 客户端Mac 直接开终端Windows 用 PowerShell 或下载一个 Termius第一次登录执行ssh root服务器IP输入密码进入系统后先别激动按顺序做三个检查whoami # 确认当前用户是 root uname -a # 查看内核版本 cat /etc/os-release # 确认系统版本这三条命令输出的信息决定了后面所有软件包的安装方式。比如看到是 Ubuntu 22.04你才知道该用apt而不是yum。1.2 系统更新与创建普通用户接下来做系统更新把软件源索引和已安装软件包都刷到最新apt update apt upgrade -y这一步耗时取决于服务器带宽耐心等它跑完。为什么强调先更新因为新装系统的软件源里可能有一些已知漏洞的旧版本软件更新能把这些洞补上。如果你连完服务器直接装应用装的可能就是带漏洞的版本属于给黑客留后门。更新完系统立刻创建一个普通用户别用 root 直接干活adduser deploy usermod -aG sudo deploy后面的部署操作我都建议在这个deploy用户下进行。理由很直接root 权限太大一条失误的命令比如rm -rf写错了路径就能毁掉整个系统。普通用户至少能让你多一道确认的缓冲。日常需要用管理员权限时命令前加sudo就好。1.3 防火墙与 SSH 端口加固创建完用户下一步配置防火墙。Ubuntu 自带的 UFW 足够用按照默认拒绝入站、放行必要端口的原则配置apt install ufw -y ufw default deny incoming ufw default allow outgoing ufw allow 22/tcp ufw allow 80/tcp ufw allow 443/tcp ufw enable这里 22 是 SSH 端口、80 和 443 是 Web 服务端口。先把这三个放行其他端口等真有服务要用了再开。我之前见过有人图省事直接关掉防火墙结果服务器被扫描器扫到端口鼓捣了一整天的木马清理起来比配置防火墙痛苦一百倍。还有一个很多人忽略的点云控制台里的安全组和服务器里的 UFW 防火墙是两套独立的东西。安全组在虚拟机外层生效UFW 在系统内生效相当于小区门卫和你家门锁的关系。两者都得配置。我在帮别人排查连不上服务器的问题时发现十有八九是安全组没放行对应端口SSH 怎么都连不上但网页终端却可以——这就是典型的外层拦截。2. 从密码到密钥把 SSH 连接这件事做到顺手服务器买回来之后你每天都要跟 SSH 打交道。如果每次都手动敲ssh rootIP再输密码效率太低而且密码登录本身有被暴力破解的风险。把连接方式升级到密钥登录顺便配置好别名这一步投入二十分钟之后每天省两分钟。2.1 给 SSH 配置一个顺手的 Host 别名在本地机器上打开 SSH 配置文件# Mac / Linux vim ~/.ssh/config # WindowsPowerShell 下同样路径 notepad $HOME\.ssh\config然后写入这样一个配置块Host xinfei HostName 123.45.67.89 User deploy Port 22 IdentityFile ~/.ssh/id_ed25519保存之后连接命令就从ssh deploy123.45.67.89 -p 22缩短成了ssh xinfei。别小看这个别名当你的服务器越来越多这个文件里能装下几十台机器的配置查找和切换的效率提升是实打实的。2.2 密钥登录的正确姿势与权限细节密钥登录的原理是非对称加密本地保存私钥服务器保存公钥登录时用私钥签名证明我是我。生成密钥用 ed25519 算法它比 RSA 更短更快安全性也足够ssh-keygen -t ed25519 -C deployxinfei一路回车会在~/.ssh/下生成两个文件id_ed25519是私钥id_ed25519.pub是公钥。私钥文件必须保持 600 权限否则 SSH 客户端会拒绝使用chmod 600 ~/.ssh/id_ed25519公钥复制到服务器上用ssh-copy-id最方便ssh-copy-id deploy123.45.67.89它会自动把公钥追加到服务器的~/.ssh/authorized_keys文件里同时处理好权限。如果你是用 Windows PowerShell 没有ssh-copy-id可以手动执行cat ~/.ssh/id_ed25519.pub | ssh deploy123.45.67.89 mkdir -p ~/.ssh chmod 700 ~/.ssh cat ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys密钥配好之后测试ssh xinfei能免密登录再考虑关掉密码登录。编辑服务器的 SSH 配置文件sudo vim /etc/ssh/sshd_config修改这三个参数PasswordAuthentication no PermitRootLogin no Port 22改完重启 SSH 服务sudo systemctl restart sshd这一步非常关键但在做之前一定确认密钥登录没问题否则改完密码登录被禁、密钥又用不了你就把自己锁在门外了。我自己就干过一回当时差点要用控制台重置密码还好本地留了另一台机器的跳板。经验就是每次改 SSH 配置都先开一个新的终端窗口测试登录成功再关闭旧窗口。2.3 用 VS Code Remote 把服务器当本地开发环境用很多新手对命令行有恐惧感可以先用图形界面过度一下。VS Code 装上 Remote - SSH 插件填写ssh xinfei回车后就能像打开本地文件夹一样直接编辑服务器上的代码。这个体验有多好呢改代码、看报错、调试都跟本地开发一样流畅文件实时同步还能直接调出终端执行命令。我自己的使用习惯是开发环境放本地服务器上只放生产代码用 VS Code Remote 连接服务器主要是为了快速排查线上问题。这样生产环境干净可控不像有些人直接把服务器当开发机各种依赖装了一堆出问题都不知道是哪一步影响的。2.4 Windows 下命令不存在的坑顺着这个多说一句Windows 用户在本地经常遇到npm 不是内部或外部命令、git 不是内部或外部命令、pnpm 不是内部或外部命令这类报错。本质上是因为命令对应的可执行文件没有加入环境变量 PATH终端找不到而已。解决方案有两个方向一是下载安装包重装时勾选自动加入 PATH选项二是在系统环境变量里手动把C:\Program Files\nodejs这类目录追加进去。但把这些命令放到服务器上时又是另一套逻辑。服务器上是 Linux不叫环境变量 PATH而是通过apt install安装软件后可执行文件自动放进/usr/bin天然就在 PATH 里。所以很多 Windows 用户卡住的命令识别不了问题在服务器上反而不太会出现除非你用了 nvm 这类版本管理工具需要在~/.bashrc里配置初始化的那一行。3. 装环境前先想清楚裸装运行时还是直接上容器到了这一步服务器已经连上了安全也做了基础加固接下来该装运行环境了。但在动手安装之前我强烈建议你先停下来想一个会影响后面所有操作的问题你打算怎么管理这些软件依赖3.1 两种方案的取舍逻辑目前主流就两条路线对比维度裸装运行时apt / nvm容器化Docker学习成本低软件源直接装中高需要理解镜像、容器等概念隔离性差多个项目共享一套环境好每个项目一套独立环境迁移部署麻烦换机器要重新装方便一条 compose 文件拉起整套资源占用低直接跑在系统上略高多一层容器运行时适合场景单项目、学习阶段、小内存机器多项目、团队协作、复杂依赖如果你只是跑一个个人项目服务器内存 2G 以内我建议裸装。Docker 虽然强但容器本身和镜像会占一部分磁盘和内存对低配机器有压力。如果你是给公司部署多个服务或者准备把项目迁移到不同机器那就直接上 Docker一次配置好后面复制粘贴就能部署。3.2 裸装 Node.js 与 Python 环境选裸装的装 Node.js 建议走 nvm方便切换版本curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc nvm install 20 node -v npm -vPython 方面系统自带的 Python 3 版本可能不够新用 apt 安装基础包即可sudo apt install python3 python3-pip python3-venv -y这里特别提醒一句别用系统的 root 环境直接跑 pip installPython 项目依赖容易相互踩要用虚拟环境。建一个项目目录在里面创建虚拟环境mkdir -p ~/projects/myapp cd ~/projects/myapp python3 -m venv venv source venv/bin/activate看到提示符前面多了(venv)说明虚拟环境生效了接下来装的依赖都会隔离在这个目录里。3.3 用 Docker Compose 拉起一套服务选容器化的先把 Docker 装好curl -fsSL https://get.docker.com | bash -s docker安装完成后写一个docker-compose.yml。比如我要同时跑一个 Node 后端和一个 MySQL 数据库配置长这样version: 3.8 services: app: image: node:20 working_dir: /app volumes: - ./app:/app ports: - 3000:3000 command: sh -c npm install npm run start mysql: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: your_password volumes: - mysql_data:/var/lib/mysql ports: - 3306:3306 volumes: mysql_data:然后用docker compose up -d拉起来。这套方案最香的地方在于换一台新服务器只要把项目目录和docker-compose.yml拷过去执行同一条命令环境就完整恢复了。不用再一个个装依赖、配环境变量。3.4 小内存服务器的 Swap 补充不管选哪条路有一个坑我都要提前预警很多入门款服务器只有 1G 或 2G 内存编译 Node 项目或者安装 Python 依赖的时候内存一爆进程就被系统杀掉了。表现为终端突然报Killed没有任何错误堆栈。这时候我建议给服务器加一块 Swap交换分区用磁盘空间充当内存的应急后备sudo fallocate -l 2G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile为了重启后依然生效还要写进/etc/fstabecho /swapfile none swap sw 0 0 | sudo tee -a /etc/fstab加上 2G Swap 之后2G 内存的服务器实际可用内存达到 4G编译和安装过程的容错率高很多。代价是 Swap 的读写速度比物理内存慢所以它只是应急方案真正的高并发场景还是该升级内存。4. 项目代码怎么上服务器三种正规姿势环境装好了代码还躺在本地仓库里怎么传上去这一步最容易踩坑。很多新手从网上拷了个文件然后发现传不上去或者传上去了跑不起来。我把三种正规姿势都列出来你按场景选。4.1 走 Git最省心也是我最推荐的同步方式如果项目一直在用 Git 管理服务器上直接拉取是最规范的做法git clone gitgithub.com:yourname/yourproject.git这里有个细节让服务器用 SSH 方式连接你的代码托管平台需要在服务器上生成一对新的密钥然后把公钥加到 GitHub / Gitea / 公司的 GitLab 上ssh-keygen -t ed25519 -C server-deploy cat ~/.ssh/id_ed25519.pub这样服务器就成了一个有权限拉取代码的独立开发角色。以后发布新版本流程就变成cd ~/projects/myapp git pull origin main整个流程干净、可追溯每次发布改了什么代码看 git log 就一清二楚。我甚至建议你干脆自建一个 Git 服务器用 Gitea 这类轻量级工具几行命令跑起来团队内部用完全够代码也不用传到外网平台。4.2 rsync适合静态文件和增量同步如果你的项目不含 Git 历史或者只想把本地的构建产物同步到服务器比如前端打包出来的dist目录rsync 是效率最高、最稳的选择rsync -avz --progress ./dist/ deploy123.45.67.89:~/projects/myapp/dist/-a保留文件权限和时间戳-v显示过程-z传输时压缩--progress能看到进度。rsync 的增量特性让它在大目录场景下比全量 scp 快很多第一次全量同步之后每次只传变化的部分。4.3 静态站点构建与目录权限这里重点讲一个前端部署的高频场景本地npm run build生成dist/目录然后传到服务器。传输完成后静态文件要交给 Nginx 服务。这里有个权限问题容易踩Nginx 默认以www-data用户运行如果项目目录是deploy用户所有Nginx 可能没有读取权限页面白屏。解决方式是给 Nginx 开放读取权限sudo chown -R deploy:www-data ~/projects/myapp sudo chmod -R grX ~/projects/myapp这样项目目录属主是你的部署用户属组是www-dataNginx 能读你也能正常写文件。这个组合我在生产环境用了很久没出过权限类的玄学问题。5. 程序不能一关终端就死进程托管方案对比代码上传完毕运行npm start或python app.py程序跑起来了访问 IP 加端口也能看到页面了——但只要你一关终端或者 SSH 连接断开程序瞬间就没了。这是新手遇到最多、也最困惑的现象。原因很简单程序是当前终端的子进程终端关闭会向子进程发送 SIGHUP 信号默认行为是直接终止。要解决这个问题有从简单到正式的三种方案。5.1 nohup 和它的问题最粗暴的解决方案是nohupnohup npm start app.log 21 nohup的作用是忽略 SIGHUP 信号把标准输出和错误都重定向到日志文件末尾的让程序在后台运行。跑完之后程序确实活了但你马上会遇到三个新问题程序死了没人管、开机不会自动拉起、日志文件越来越大没人清理。所以 nohup 只能用来应急演示不适合正式部署。5.2 tmux给程序一个不关门的房间tmux 是终端复用器思路是开一个虚拟会话程序在会话里跑你关闭 SSH 后会话依然存在。用起来非常简单sudo apt install tmux -y tmux new -s myapp # 在里面执行 npm start # Ctrl b 然后按 d 断开会话程序继续跑 tmux attach -t myapp # 重新接回会话tmux 最大的优势是你可以随时回到程序所在的环境看到它实时的输出跟调试本地程序一样。它在维护临时任务、跑一次性脚本时非常好用。但它同样有程序挂着没人管的问题而且如果服务器重启tmux 会话也没了。5.3 systemd正式项目的最佳归宿正式部署我强烈建议用 systemd几乎所有现代 Linux 发行版都内置了它。把你的程序定义成一个服务系统就能帮你管理它的启动、停止、崩溃重启、开机自启。在服务器上新建服务文件sudo vim /etc/systemd/system/myapp.service内容如下[Unit] DescriptionMyApp Service Afternetwork.target [Service] Userdeploy WorkingDirectory/home/deploy/projects/myapp ExecStart/usr/bin/node server.js Restartalways RestartSec5 EnvironmentNODE_ENVproduction [Install] WantedBymulti-user.target然后加载并启动sudo systemctl daemon-reload sudo systemctl enable myapp sudo systemctl start myapp sudo systemctl status myapp这套方案最香的是Restartalways参数程序崩溃后 systemd 会在 5 秒后自动拉起来不用你半夜爬起来手动重启。查看日志也方便sudo journalctl -u myapp -f5.4 Node 项目我为什么偶尔用 pm2给 Node 项目做进程管理还有一个专门的工具叫 pm2。它比 systemd 多提供了一些 Node 生态的特性比如负载均衡模式可以用满多个 CPU 核心再比如方便查看每个进程的 CPU 内存占用sudo npm install -g pm2 pm2 start server.js --name myapp pm2 save pm2 startuppm2 startup会生成一条开机自启的系统命令配合pm2 save服务器重启后 pm2 能自动把所有托管进程拉起来。用它管理 Node 和 Python 脚本都很方便。但如果你是跑 Java 或其他语言还是老老实实写 systemd 文件别额外引入依赖。我个人现在的主力方案是单进程服务用 systemdNode 多实例用 pm2。两个工具都不复杂场景选对就行。6. 让服务被全世界访问域名、反向代理与 HTTPS程序在服务器上稳定跑起来了但目前只能通过http://IP:端口访问。要让它像个正规项目还需要三步绑定域名、让 Nginx 做反向代理、配上 HTTPS 证书。6.1 域名解析的等待与验证在云厂商的域名控制台添加一条 A 记录把域名指向服务器 IP类型A 主机记录www 记录值你的服务器公网 IP如果还想让根域名也生效就再加一条主机记录为的 A 记录。解析生效不是实时的快的几分钟慢的可能要一两个小时。验证方法是ping yourdomain.com或者更精确地查解析记录nslookup yourdomain.com能看到解析出的 IP 和你服务器 IP 一致就可以进入下一步了。6.2 Nginx 静态站点配置安装 Nginxsudo apt install nginx -y然后新建一个站点配置文件sudo vim /etc/nginx/sites-available/myapp静态站点的配置内容server { listen 80; server_name yourdomain.com www.yourdomain.com; root /home/deploy/projects/myapp/dist; index index.html; location / { try_files $uri $uri/ /index.html; } }这里try_files那一段是专门给前端 SPA 项目准备的配合 History 路由模式刷新二级页面时不会 404。创建软链接启用站点sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginxnginx -t是检查配置文件语法输出syntax is ok再 reload这条习惯能帮你避开很多低级问题。6.3 反向代理到后端服务如果后端跑在 3000 端口Nginx 的职责是接收外部 80/443 请求再转发给本地 3000 端口这就是反向代理。配置如下server { listen 80; server_name api.yourdomain.com; location / { proxy_pass http://127.0.0.1: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; } }proxy_set_header这几行务必带上否则后端拿不到真实的客户端 IP也不清楚请求到底是通过 HTTP 还是 HTTPS 进来的一些依赖来源 IP 的功能比如登录日志、限流就会失效。6.4 HTTPS 证书申请与自动续期现在主流浏览器的地址栏对 HTTP 网站都会标记不安全所以 HTTPS 是必须的。用 certbot 申请免费证书非常方便sudo apt install certbot python3-certbot-nginx -y sudo certbot --nginx -d yourdomain.com -d www.yourdomain.comcertbot 会引导你选择是否自动改写 Nginx 配置选 2Redirect就会把 HTTP 请求自动 301 跳转到 HTTPS。证书有效期 90 天需要自动续期。好消息是 certbot 已经内置了定时任务你可以手动验证一下sudo certbot renew --dry-run只要这个命令输出Congratulations说明自动续期链路是通的后面就不用管了。7. 上线后的日常监控、日志与真实踩坑记录项目上线只是开始真正考验人的是接下来的日常维护。我不打算列一套完整的运维体系那对单机部署来说太过了。我只讲四个最实用的检查项再加几个我自己真实踩过的坑这些坑可能比整个教程都值钱。7.1 磁盘、内存与时间的检查每周花两分钟看一眼这几个指标df -h # 磁盘使用率超过 80% 就该清理了 free -h # 内存和 Swap 使用情况 htop # 实时看哪个进程在吃资源另外还有一个很多人忽略的问题服务器时间。云服务器有时候重启之后系统时间会漂移时间不准会导致日志时间线错乱、HTTPS 证书校验失败。检查一下当前时间date不对的话用 NTP 自动校时sudo apt install systemd-timesyncd -y sudo timedatectl set-ntp true sudo timedatectl status看到System clock synchronized: yes就说明时间同步正常。这个坑我在一次排查证书报错时踩过折腾了半天最后发现是服务器时间比真实时间慢了五分钟证书校验被当成尚未生效而拦截特别冤。7.2 看日志的正确方式程序出问题不要慌第一反应应该是看日志而不是盲目重启。不同程序的日志位置不一样服务类型日志查看方式systemd 托管的服务journalctl -u myapp -fpm2 托管的服务pm2 logs myappNginx 访问日志tail -f /var/log/nginx/access.logNginx 错误日志tail -f /var/log/nginx/error.logtail -f是持续跟踪输出适合一边操作一边看实时日志。把日志养成了排查第一习惯之后你会发现很多灵异问题其实都是因为没看日志在那里瞎猜。日志文件还会越来越大尤其是 Nginx 的访问日志一个月下来能到几个 GB。配置一下 logrotate 自动轮转sudo vim /etc/logrotate.d/nginx核心配置就是保留最近 14 天的日志超过就自动压缩并切割/var/log/nginx/*.log { daily missingok rotate 14 compress delaycompress notifempty create 0640 www-data adm sharedscripts postrotate [ -f /var/run/nginx.pid ] kill -USR1 cat /var/run/nginx.pid endscript }7.3 我踩过的几个高频坑第一个坑是端口没放行。我在 Nginx 里配好了 443但浏览器就是打不开排查了两个小时最后发现控制台安全组只放行了 80 和 22443 没开。云厂商安全组的默认规则各不相同你买了新服务器后第一件事就应该确认80、443这些必要端口在安全组里是放行的。第二个坑是环境变量和代码里写死的东西不一致。本地开发时接口地址写http://localhost:3000到了服务器上忘记了前端请求自然全部失败。后来我养成一个习惯所有环境相关的配置全部通过环境变量注入代码里不写死任何地址。服务器的环境变量集中写在/etc/environment或 systemd 文件的Environment字段里一目了然换环境只改一处。第三个坑是构建安装时内存不足被杀。前面提过的 Swap 方案真的救了我好几次。我帮朋友部署一个前端项目npm install阶段连续三次报Killed加上 Swap 之后一次通过。第四个坑是中文乱码。程序日志里中文全是问号或者乱码通常是系统缺中文字体或者 locale 没设好。服务器上一般不需要显示中文界面但程序输出的内容需要正确编码。安装中文字体或者配置时区后问题通常能得到解决比如把时区设为上海sudo timedatectl set-timezone Asia/Shanghai最后说一个安全复查习惯。每次上线完新功能我都会顺手跑一遍sudo apt update sudo apt upgrade -y把系统安全补丁打上。同时定期检查/var/log/auth.log里有没有可疑登录记录。这个习惯花不了几分钟但能让你在被人试图爆破 SSH这件事上始终心里有数。服务器这个东西配置一次只是开始日拱一卒的维护才能让项目真正稳定跑下去。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进