↓ 跳过正文

显示设置

330
折腾 VPS 的常用命令备忘录(持续更新)

折腾 VPS 的常用命令备忘录(持续更新)

·6405 字·13 分钟
目录

近几个月折腾了好几台小鸡,期间又把博客从 WordPress 搬到了 Hugo。命令当时用得挺顺,过段时间再碰到同类问题,还是显得不够从容。

干脆给自己留一份备忘录。先收平时确实用得上的,之后碰到新的场景再往里补。这里主要按我在用的 Debian / Ubuntu 写,Docker 项目用 Compose 管理。

查阅时可以用页面目录,或者直接搜「内存」「端口」「日志」这些关键词。

大部分命令在服务器上执行;博客本地预览和 SSH 登录另有标注。已经是 root 时可以省略 sudo。示例中的 SERVER_IP、SSH_PORT、SERVICE_NAME 等需要换成实际值。

1. 登录之后的几个基本操作
#

SSH 登录与提权
#

在自己电脑的终端连接服务器,-p 指定 SSH 端口:

ssh -p SSH_PORT haru@SERVER_IP

不过现在基本都用 Termius 之类的ssh软件管理了。

whoami        # 当前用户
hostname      # 当前机器
sudo -i       # 进入 root shell
exit          # 退出当前 shell

刚拿到小鸡,先做一点安全加固
#

不展开写成装机教程,给自己记一下顺序:更新系统 → 配普通用户和公钥 → 验证登录与提权 → 收紧 SSH → 检查开放端口。

Debian / Ubuntu 上,如果还没有自己的普通用户,可以先创建,再加入 sudo 组:

sudo adduser haru
sudo usermod -aG sudo haru

把自己电脑上的 SSH 公钥放进这个用户的 ~/.ssh/authorized_keys,私钥留在本地。检查目录和文件属于该用户,权限分别为 700、600。另开一个会话,用 haru 的密钥登录,再确认 sudo -i 能正常提权。

这些验证通过后,再调整 SSH 配置。我们的目标是普通用户密钥登录,不允许直接登录 root,也不开放密码或键盘交互认证。相关设置含义见 OpenSSH 配置手册:

PubkeyAuthentication yes
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no

这不是让自己直接覆盖整个配置文件;还要检查 sshd_config.d 和已有规则,最终用第五节的 sshd -t、sshd -T 核对生效值。

换 SSH 端口时,先选未占用的端口、放行云平台安全组和本机防火墙,再修改监听配置。有的系统还有 ssh.socket,不能只盯着 sshd_config。保留旧会话,新端口、新用户、密钥和 sudo 都测试成功后,再关闭旧入口。 同时确认 Fail2Ban 保护的是新端口。

换端口主要减少扫描噪音,密钥登录才是重点。防火墙只开放自己确实需要的入口,管理面板尽量不要裸露在公网;动手前也先确认服务商控制台或救援入口可用,免得把自己锁在外面。

找到目录、修改配置
#

pwd
ls -lah
cd /etc/nginx/sites-available
nano monoe.moe

pwd 看当前路径,ls -lah 显示隐藏文件、权限和大小。Nano 保存用 Ctrl+O,按回车确认文件名;退出用 Ctrl+X。

改配置之前可以习惯性留一份副本,文件名加时间,方便找回来:

sudo cp -a /etc/nginx/sites-available/monoe.moe \
  "/etc/nginx/sites-available/monoe.moe.bak-$(date +%Y%m%d-%H%M%S)"

Debian / Ubuntu 更新软件时常用:

sudo apt update
sudo apt upgrade

前一条更新软件包索引,后一条安装升级;升级可能重启服务,找个方便检查的时间再做。

2. 看看小鸡现在忙不忙
#

内存、Swap 与负载
#

free -h
swapon --show
uptime

free -h 我们主要看 available:它比单看 free 更适合判断还有多少内存可供程序使用。uptime 里的三个负载值分别对应最近 1、5、15 分钟;负载也包含等待中的任务,不能直接当 CPU 百分比。

Swap 已经用了一些,先看看它有没有持续交换:

vmstat 1 10

第一行通常是开机以来的平均数据,重点看后面的采样。si / so 是换入和换出;持续明显增长,并且伴随卡顿,才值得继续查。

之前比较几个评论系统时,我一度盯着内存百分比看。后来还是得把可用内存、交换活动和服务是否稳定放在一起判断。内存占用上了六成,不等于机器已经撑不住。

哪个进程占用比较多
#

ps -eo pid,user,comm,rss,%mem --sort=-rss | head -15
top

前一条按 RSS 从高到低列出进程,RSS 的单位是 KiB;top 用来实时观察,按 q 退出。共享内存可能出现在多个进程的统计里,别把所有 RSS 直接相加当成整机占用。

查内核有没有因内存不足杀进程:

sudo journalctl -k --since "24 hours ago" --no-pager \
  | grep -Ei 'out of memory|oom-kill|killed process'

没有匹配内容,表示当前能读取的这段日志中没找到这些记录。

磁盘满了,先找谁占地方
#

df -h
df -i
sudo du -xhd1 /var /srv /tmp 2>/dev/null

df -h 看文件系统容量;df -i 看 inode。明明还有空间却无法创建文件时,也要看看 inode 是否用完。du 列出目录占用,找到大目录后再往下查。

博客搭建时还踩过一个坑:预览程序已经停了,但临时文件仍留在 /tmp。那台机器的 /tmp 恰好是内存盘,堆积的文件照样占内存或 Swap。

findmnt -T /tmp
sudo du -sh /tmp

先确认挂载类型和具体文件,再决定清理哪些。现在博客构建放在 /srv/hugo 下的磁盘临时目录里,发布完成后由脚本清理。

3. 服务状态和日志
#

服务有没有启动
#

把 SERVICE_NAME 换成 nginx、warp-svc 或要检查的实际服务名:

systemctl status SERVICE_NAME --no-pager -l
systemctl is-enabled SERVICE_NAME
systemctl --failed --no-pager

status 看当前运行状态,is-enabled 看启动配置;--failed 找失败的单元。不同服务可能采用定时器或 socket 激活,不能只看是否 enabled。

需要手动控制服务时:

sudo systemctl start SERVICE_NAME
sudo systemctl stop SERVICE_NAME
sudo systemctl restart SERVICE_NAME

这三条会改变服务状态。重启网络、SSH 或隧道相关服务之前,先想一下自己当前连接是不是靠它维持。

修改了 systemd 的 .service 或 .timer 文件,再让 systemd 重新读取定义:

sudo systemctl daemon-reload

它不会自动重启服务;服务自己的配置文件也未必归它管理。

查看最近日志、实时追踪
#

sudo journalctl -u SERVICE_NAME -n 100 --no-pager
sudo journalctl -u SERVICE_NAME --since "30 minutes ago" --no-pager
sudo journalctl -fu SERVICE_NAME

最后一条实时跟踪,按 Ctrl+C 退出。普通文本日志可以这样看:

sudo tail -n 50 /var/log/nginx/error.log
sudo tail -f /var/log/nginx/error.log

按时间定位故障
#

timedatectl
sudo journalctl -u SERVICE_NAME \
  --since "2026-10-08 04:45:00" \
  --until "2026-10-08 05:30:00" --no-pager

日期换成故障发生的那天。没有注明时区时,上面的时间按服务器本地时区解释。

NAT-SB 换 IP 的排查里就遇到过:每天更换IP的时间是北京时间凌晨四点,机器开机时的时区好像在英国,后来设成东京时区,对应的查询时间应该是五点。所以先看 timedatectl 能省不少绕路。

4. Docker Compose 日常维护
#

我的项目习惯放在 ~/data/docker_data/。这里的 ~ 是当前用户的家目录;从 root 换成 haru 后,路径就不一样了。博客机的 Twikoo 实际目录是 /root/data/docker_data/twikoo。

以下例子先在 root shell 进入项目目录,Compose 操作都在这个目录里执行:

sudo -i
cd /root/data/docker_data/twikoo

看状态、日志和资源占用
#

docker compose ps -a #查看当前 Compose 项目里的所有容器,包括运行中、已停止和退出的容器
docker compose logs --tail=100 twikoo #显示 Twikoo 最近 100 行日志,显示完就结束
docker compose logs -f --tail=50 twikoo #先显示最近 50 行,然后持续显示新日志。-f 就是持续跟踪的意思
docker stats --no-stream #查看当前运行中容器的资源占用,显示一次后退出,不持续刷新。

Compose 命令里的 twikoo 是服务名,其他项目换成自己的服务名。docker stats --no-stream 列出运行容器的单次资源采样。

容器反复退出时,可以继续看:

docker inspect twikoo \
  --format '状态={{.State.Status}} OOM={{.State.OOMKilled}} 重启次数={{.RestartCount}}'

这里的 twikoo 是容器名称。OOM=true 是值得关注的线索,还要结合日志和整机内存判断。

Waline、Artalk、Twikoo 我都试过,最后留下 Twikoo,主要是评论区内嵌管理面板用着顺手。几次采样的内存数字只是当时的结果,不能当成以后每个版本都会保持的固定值。

改完配置怎样生效
#

docker compose config -q
docker compose up -d

前一条校验 Compose 配置,后一条按配置创建或更新容器。改了端口、环境变量、挂载等设置,要用 up -d,单纯 restart 不会应用这些变化。

只是临时停一下或恢复:

docker compose stop
docker compose start

只想重启当前容器:

docker compose restart twikoo

更新镜像
#

先确认项目数据已有可恢复的备份,再执行:

docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=50

更新是否跨版本、是否要迁移数据,还是得看项目说明。固定版本标签的项目,先修改标签才会升级到另一个版本。Twikoo 更新还要核对博客端的前端版本是否匹配。

需要拆掉这个 Compose 项目的容器和默认网络时才用:

docker compose down

它会中断服务,但不会自动删除绑定挂载目录里的文件。不要随手加 -v,那会删除相关的命名卷和匿名卷。 博客源码存在 GitHub,Twikoo 评论数据和图床图片则要另外保存。

5. 端口、网络与 SSH 排查
#

端口到底有没有监听
#

sudo ss -lntp
sudo ss -lnup

分别看 TCP 和 UDP 的监听及进程。127.0.0.1 只对本机开放;0.0.0.0 表示所有 IPv4 地址。真正能不能从外部访问,还取决于防火墙、上游网络和端口映射。

Twikoo 映射到 127.0.0.1:18080,公网通过 Nginx 访问,这是现在有意设置的结构。

从本机到域名,一层层检查
#

curl --max-time 10 http://127.0.0.1:18080/
curl --max-time 10 https://c.monoe.moe/
curl -I --max-time 10 https://monoe.moe/

前两条看响应正文,最后一条用 HEAD 请求看响应头。如果服务不支持 HEAD,就改用普通 GET 测试。

本机接口也失败,先看容器和端口;本机正常、域名异常,再查 Nginx、DNS 和 HTTPS。HTTP 200 只能说明该请求成功响应,评论提交和审核还要实际验证。

getent ahosts c.monoe.moe
ip -br addr
ip route
ping -c 4 1.1.1.1

这几条分别检查系统解析结果、网卡地址、默认路由和基本连通性。Ping 不通也可能是 ICMP 被限制,继续结合 HTTPS 请求判断。

SSH 配置检查与 Fail2Ban
#

sudo /usr/sbin/sshd -t
sudo /usr/sbin/sshd -T \
  | grep -E '^(port|permitrootlogin|passwordauthentication|pubkeyauthentication|kbdinteractiveauthentication) '
sudo fail2ban-client status sshd

sshd -t 成功时通常没有输出;-T 显示解析后的配置,遇到 Match 条件时还需要带具体连接条件核对。实际监听仍用 ss 确认。

JTTI 的 Ubuntu 使用 ssh.socket 接管监听,之前换端口时还专门检查了:

systemctl status ssh.socket --no-pager -l
systemctl cat ssh.socket

这类机器不能只改配置后重启 ssh.service 就认定完成。换端口还要同步防火墙和 Fail2Ban,并保持旧会话,另开一个新会话验证登录。这里只留下检查命令,需要再改时按机器状态处理。

6. WireGuard、WARP 与博客维护(只给自己看)
#

WireGuard 握手与 watchdog
#

sudo wg show
systemctl status wg-quick@wg1 --no-pager -l
sudo journalctl -u wg-quick@wg1 -n 50 --no-pager

接口名按机器实际情况换成 wg0、wg1 或 wg2。wg show 主要看最近握手、endpoint 和流量;握手时间也要结合有没有通信需求判断,不能看到几分钟没握手就直接认定断线。

NAT-SB 上已有 watchdog,想看看它最近有没有出手:

systemctl status wg-watchdog.timer --no-pager -l
sudo journalctl -t wg-watchdog --since "7 days ago" --no-pager

我们的脚本健康时不写事件日志,所以没有记录可能只是没触发异常处理。还要结合 timer 和隧道状态看。它的 oneshot service 执行成功后显示 inactive (dead) 也正常,不等于 timer 停了。

落地出口怎么又变回中转机了
#

之前 IIJ / NAT-SB 接到美国9929时踩过这个坑:本来应该从落地机出去,查到的出口却变成了中转美国9929,太吓人这可关系到我好多账号的风控啊。后来用 JTTI,NTT 时,这件事也成了优先处理的加固项。

问题不能只靠 wg show 判断。我们的路径是 Xray 给出站连接打 mark,再由 Linux 策略路由选对应隧道;ip rule 丢失,或者对应表没有可用路由且没有阻断兜底时,就可能继续查主路由表,从美国9929的公网口出去。握手正常,也不代表这条选择路径正常。

目前我的主力JTTI 的对应关系是:

落地Xray mark(十进制 / 十六进制)路由表隧道
IIJ100 / 0x64xray_wgwg0
NAT-SB101 / 0x65xray_natwg1
NTT102 / 0x66xray_nttwg2
GLBB103 / 0x67xray_glbbwg3

以 NTT 为例,先在 JTTI 中转机上检查这几层:

sudo wg show wg2
ip -4 rule show
ip -4 route show table xray_ntt
ip -4 route get 1.1.1.1 mark 102
sudo nft list table inet xray_killswitch

正常时,带 mark 102 的路由查询应指向 wg2 和 xray_ntt,而不是公网网卡。路由表里还留着两条默认路由:

default dev wg2 scope link metric 10
blackhole default metric 32767

这次加固按下面几个层次做,目标是:落地可以断,但绝不能悄悄换成中转机出口。

  1. 路由丢失时先阻断。 每张专用路由表常驻一条低优先级的 blackhole default。隧道路由消失后,命中黑洞,而不是继续回落到主路由表。
  2. 策略规则丢失时再拦一层。 光有黑洞不够,ip rule 整条没了,流量根本不会进入那张表。另建 inet xray_killswitch 表,拦住带 0x64、0x65、0x66、0x67 的包从 JTTI 公网口 enp1s0 直接发出。这里只针对专用 mark,不是一刀切禁止公网通信;匹配方式可查 nftables 的 mark 与出口接口说明。
  3. 把修复和启动顺序接好。 fix-xray-routing.sh 补齐规则、黑洞路由,以及接口存在时的隧道路由;开机、systemd-networkd 重启后执行,另由 timer 约每 60 秒检查一次。kill-switch 单独持久化,并接到 x-ui.service 的启动依赖和启动前检查,避免 Xray 先跑、保护后到。定时修复负责恢复,不能代替故障当下的拦截。
  4. 配置从源头保存,再验证故障。 Xray 的出站 mark 和分流规则从 3x-ui 面板保存,不只手改会被重新生成的运行 JSON。测试覆盖删除策略规则、删除隧道路由和停掉 wg2:故障期间应阻断,恢复后再检查实际出口。带 SO_MARK 的测试连接能验证底层保护,最终还要从对应客户端节点发请求,核对落地 IP。

查看保护与自愈有没有在工作:

systemctl status xray-killswitch.service --no-pager -l
systemctl status fix-xray-routing.timer --no-pager -l
systemctl list-timers --all --no-pager | grep fix-xray
sudo journalctl -u fix-xray-routing.service -n 30 --no-pager

还有几个边界要记住:这些名字是我这台机器的配置,不是装了 WireGuard 就自带的;不要为了加载规则直接 flush ruleset,把 Fail2Ban 的表一起清掉。当前路由记录以 IPv4 为主,以后启用 IPv6 或换公网网卡,也要补齐对应保护并重新测试。

普通 curl 测到 JTTI 的 IP,并不能单独证明泄漏,它可能本来就没经过 Xray 的 mark / 分流。DNS 目前仍统一走 IIJ 上的解析服务,WARP 和有意保留的直连出口则另算,不要求所有请求都显示同一个落地 IP。

WARP 运行与出口检查
#

systemctl status warp-svc --no-pager -l
sudo journalctl -u warp-svc -n 50 --no-pager
curl -4 --max-time 10 https://www.cloudflare.com/cdn-cgi/trace

Trace 里的 ip、loc、colo、warp 用于观察这次请求的出口。如果 WARP 是代理模式,上面普通 curl 未必经过它,需要显式指定实际代理地址:

curl --proxy socks5h://127.0.0.1:PROXY_PORT \
  --max-time 10 https://www.cloudflare.com/cdn-cgi/trace

PROXY_PORT 换成自己配置的 SOCKS5 端口。它能验证指定代理路径,但不能证明 Google 或其他域名也命中了同一条分流规则。

WARP 没断,Google 却突然“送中”了
#

2026 年 10 月 9 日又遇到一个新情况:Google 突然出现“送中”的地区异常,但 JTTI 上的 WARP 仍显示已连接,代理请求也能正常出去(心累)。

这次和前面「落地出口变回中转机」不是一回事。不是看到地区不对,就一定是 WireGuard 断了或者流量漏回 JTTI。

先在 运行 WARP 的 JTTI 上查看状态:

warp-cli status

当时显示:

Status update: Connected
Network: healthy

再明确通过 WARP 的 SOCKS5 代理检查出口:

curl --socks5-hostname 127.0.0.1:40000 \
  --max-time 15 -sS \
  https://www.cloudflare.com/cdn-cgi/trace \
  | grep -E '^(ip|loc|colo|warp)='

40000 是我这台机器配置的代理端口,其他机器按实际值替换。代理模式只接管明确使用这个代理的请求,普通 curl 不一定经过 WARP。见 Cloudflare 代理模式说明。

当时的出口记录是:

ip=2a09:bac5:4302:dc::16:185
colo=NRT
loc=JP
warp=on

请求经过了 WARP,Cloudflare Trace 也显示 JP / NRT,但 Google 的实际地区表现仍然不对。这几个字段只能作为这次请求的检查结果,不能当成 Google 一定会判日本的保证。

当时有意识到或许就是WARP的IP自身的问题,就没继续修改其他地方的设置,而是尝试重新建立 WARP会话:

warp-cli disconnect
sleep 5
warp-cli connect

重连完成后,再运行前面的状态和出口检查命令。

这次重新分配到了另一个 IPv6 出口:

ip=2a09:bac1:3b40:10::16:1e4
colo=NRT
loc=JP
warp=on

随后实际打开 Google,地区表现恢复正常。

所以这次的排障结果更支持:异常与当时分配到的 WARP 出口有关,而不是 WARP 服务本身断线。 至于是不是 Google 的 IP 地区记录有误,仅凭这次测试还不能完全确定;现在Google 的位置判断有毒分分钟教你做人,好好的一个IP下一秒可能就被“送中”。

以后再遇到类似情况,先记住这个顺序:

  1. 看 WARP 是否连接,以及代理请求能否正常完成。
  2. 记录当前出口 IP,确认目标流量确实命中了 WARP 分流。
  3. 尝试重连,比较出口是否变化,再实际打开 Google 验证。
  4. 仍然异常,再检查客户端分流、Xray 规则顺序和其他位置判断因素。

重连不保证每次都换 IP,换了 IP 也不保证目标平台一定恢复。这次有效,值得留作一个优先尝试的修复办法。

Nginx 与证书
#

sudo nginx -t && sudo systemctl reload nginx
sudo certbot certificates
systemctl list-timers --all --no-pager | grep -i certbot

Nginx 配置检查通过后再重载。Certbot 前一条查看证书信息,后一条查常见的续期 timer;不同安装方式也可能通过 cron 调度,没有 timer 时继续核对安装方式。

博客换背景时还遇到过一次 403:发布同步把临时目录的权限带到了正式目录,Nginx 无法读取。现在发布脚本已经统一处理公开目录和文件权限。

碰到类似问题,先看看:

ls -ld /var/www/monoe.moe/public
ls -l /var/www/monoe.moe/public/index.html

不要为了省事递归改成 777。先结合 Nginx 错误日志,找到具体哪个路径读不了。

博客本地预览与正式发布
#

在 Mac 本地的项目目录运行:

bash scripts/preview.sh

浏览器打开 http://localhost:1313/,检查完按 Ctrl+C 停止。预览会显示草稿,正式发布前记得把文章头里的 draft 改为 false。

我现在的流程是:Typora 写文章 → 本地预览 → GitHub Desktop 提交并 Push → 登录博客服务器发布。

在 博客服务器执行:

cd /srv/hugo/monoe.moe
bash scripts/publish.sh

Push 只上传源码,不会自动让网站更新。发布脚本负责拉取、构建、同步网页和清理临时目录;如果服务器源码有未提交改动,会停下来。

以后文章和样式都在本地修改,尽量保持这条流程。生成后的 public 目录下次会被覆盖,不在那里直接改正文。

后续慢慢补
#

这份笔记先记到这里。以后碰到新问题,把真正用过、确认有用的命令加到对应位置,再更新文章时间。

更新记录
#

  • 2026-10-07:整理首版,加入 VPS 状态检查、Docker Compose、SSH、WireGuard、WARP 和博客维护命令。
  • 2026-10-09:补充 WARP 连通但 Google 地区异常的案例,以及检查出口、重连后验证的方法。

文档参考
#

参数忘了时,优先回官方说明核对:


← 千禧梦核

评论

随便聊聊吧。评论审核通过后才会公开显示。