🐕 serv00 免费主机操作手册

从开局到实战:环境搭建、PM2 进程守护、站外看门狗——一台欧洲免费小主机的完整玩法记录(已脱敏,可公开)

serv00 是波兰的免费主机商,给的不是普通网页空间,而是一个带 SSH、能跑自己程序的 FreeBSD 小环境——本质是台阉割版 VPS。 本手册记录一次完整的实操:开局配置 → PM2 环境 → 踩坑 → 实战项目(站外看门狗)。

一、免费档规格

项目 规格
存储 3 GB SSD
月流量 不限
内存 512 MB
系统进程 15 个
TCP/UDP 端口 3 个(需预留)
网站数 100 个
数据库 MySQL 10 个、PostgreSQL 3 个、MongoDB 3 个
系统 FreeBSD 14.3(注意:不是 Linux)
备份 每日备份,保留 7 天
有效期 10 年

最值钱的三样:SSH 登录、cron 定时任务、允许跑自己的程序。绝大多数免费主机只给网页上传,这三样是它能当小服务器用的根本。

二、账号信息解读

SSH 登录(login 换成自己的用户名,sX 是你所在的服务器编号):

ssh login@sX.serv00.com

查账号全貌:

devil info account

一份真实输出(已脱敏)逐项解读:

Login: yourlogin ← 你的登录名,SSH 和面板都用它
Plan: FREE ← 免费计划
Expiration date: 2036-11-02 ← 账号到期日(10 年期)

Mail servers ← 邮件服务器都是 mailX.serv00.com
Database servers ← mysqlX / pgsqlX / mongoX.serv00.com,远程连库用
DNS servers ← dns1/dns2.serv00.com,绑自己域名时把 NS 指过来

DevilWEB (panel): https://panelX.serv00.com/ ← 控制面板地址
Roundcube (webmail) / PhpMyAdmin / PhpPgAdmin ← 网页版管理工具

Binexec: Enabled ← 跑自有程序权限,已开(见第三章)
2FA: Disabled ← 两步验证没开,建议去面板打开

注意:df -h ~ 看到的是整台宿主机的磁盘池(好几 TB),不是你的配额。你的真实上限就是规格表里的 3 GB,别被 df 骗了。

三、开局三步

1. 开"跑自有程序"权限(Binexec)

默认关闭。不开这个,你只能放网页文件,任何守护进程都跑不起来。

面板路径:Additional services → Run your own applications → Enable。 验证:devil info account 里 Binexec: Enabled。

2. 预留端口(免费档只有 3 个)

自己跑的程序只能监听预留过的端口。面板 Additional services → Port reservation,或命令行:

devil port add tcp 0 # 端口填 0 = 系统随机分配一个空闲端口
devil port list # 查看已预留的端口

建议:先占 1~2 个,想清楚再用最后一个名额。TCP 给 Web 类服务,UDP 给 QUIC 类协议。

3. 确认运行环境

uname -a # 确认 FreeBSD 版本
node -v; python3 --version; git --version

serv00 预装 Node.js、Python、Ruby、Java、Go 等一堆语言,一般不用自己装运行时。

四、PM2 运行环境

Node 程序的守护进程管家用 PM2:进程挂了自动拉起,配合 cron 还能做到宿主机重启后自恢复。

1. npm 全局目录挪回家目录(必做)

直接 npm install -g 会因权限失败,先把全局前缀指回家目录:

mkdir -p ~/.npm-global
npm config set prefix '~/.npm-global'
echo 'export PATH=$HOME/.npm-global/bin:$PATH' >> ~/.bashrc
export PATH=$HOME/.npm-global/bin:$PATH

2. 安装

npm install -g pm2
pm2 -v

3. 点火测试:整条链验一遍

用一个预留的 TCP 端口(示例 30001)跑最小 HTTP 服务,验证"端口 + 自有程序权限 + PM2"都通:

mkdir -p ~/hello && cd ~/hello
cat > app.js <<'EOF'
require('http').createServer((req, res) => {
res.end('hello from serv00\n');
}).listen(30001, '0.0.0.0');
EOF
pm2 start app.js --name hello
sleep 2
curl http://localhost:30001
curl http://sX.serv00.com:30001

两条 curl 都返回 hello from serv00,链就通了:本机回环、对外访问各验一次。

五、踩坑实录

坑 1:pm2 start 后立刻 curl 连不上——启动竞速

现象:pm2 start 刚打印 Done,马上 curl 报 Could not connect,但进程明明 online、没崩。

真相:PM2 报 Done 只代表进程已拉起,程序 bind 端口还要一点时间,共享主机负载高时这个间隙能超过 1 秒。粘贴整段命令时 curl 几乎同时执行,就撞上了。

结论:start/restart 后等 1~2 秒再测(sleep 2 写进习惯)。判断监听状态看 sockstat -l | grep 端口。

坑 2:sockstat 里的 tcp46 是正常的

FreeBSD 的 sockstat 会显示 tcp46,表示这个监听是 IPv4/IPv6 双栈,不是出错。不指定地址时 Node 默认绑双栈;显式写 '0.0.0.0' 后变纯 tcp4。两种都能用,写死 0.0.0.0 最省心。

坑 3:cron 追加命令会叠出重复行

用 (crontab -l; echo '新行') | crontab - 追加前,先确认旧行不在里面,否则同一条 @reboot 会攒好几份,触发时可能把服务拉起多份。去重一条命令:

crontab -l | sort -u | crontab -

坑 4:内存按"一个 Node 空壳约 100 MB"估

实测:什么都不干的 Node hello 服务占约 99 MB,同机的 Python 小程序只占约 25 MB。总共只有 512 MB,选型时优先 Python/Go 这类轻的,Node 服务别超过 3 个。

坑 5:第三方程序先问一句"有 FreeBSD 版吗"

系统是 FreeBSD 不是 Linux,Linux 二进制直接跑不了。装任何现成程序前先确认有 FreeBSD 构建,没有就找源码编译,或者换实现语言自己写小脚本——很多场景一个 Python 标准库脚本就够了。

六、重启自恢复

serv00 偶尔维护重启宿主机。没有自恢复的话,你的服务重启后就躺平了。两步配齐:

pm2 save
(crontab -l 2>/dev/null; echo '@reboot export PATH=$HOME/.npm-global/bin:$PATH && pm2 resurrect') | crontab -
crontab -l # 确认只有一条 @reboot(重复了用第五章的去重命令)

原理:pm2 save 把当前进程清单存档,@reboot 任务在开机时用 pm2 resurrect 把清单里的进程全部复活。以后新增/删除服务,记得再跑一次 pm2 save 更新存档。

七、保活问题(实测结论)

网上流传"3 个月不登录就回收账号"的说法,但在官方文档里找不到出处。

实测:一个账号放了半年没管,再登录时一切完好——端口预留、Binexec 权限都在,有效期仍显示 10 年期。结论:不需要专门保活,不用挂自动 SSH 登录脚本。偶尔想起来登一次看一眼即可;今天的任何一次 SSH 登录本身也会刷新活跃记录。

八、实战项目:站外看门狗

定位

serv00 最合适的活:常开、免费、坏了不心疼的站外探针。从欧洲方向定时探测你散在各处的 VPS 还活着没,异常了用 Telegram 报警,你还能在 Telegram 里手动发命令随时查状态。

边界说清:探的是"机器和网络活着"(ping + 一个代表性 TCP 端口)。像 Hysteria2 这类 UDP 协议,外人没凭据没法真握手验证——但机器被停、宕机、断网占了故障的绝大多数,这个粒度够用。对被探的机器零侵入:不装任何东西、不改任何配置。

架构

~/watchdog/
├── config       # bot token + chat id(权限 600,只存这一份)
├── targets.txt  # 目标清单:名字|IP|端口,每行一个
├── check.sh     # cron 每 5 分钟跑:探测 + 状态机 + 报警
└── tg_bot.py    # PM2 常驻:长轮询收命令,回 /status 查询

设计要点:

准备:Telegram bot

  1. 找 @BotFather 发 /newbot,拿到 token
  2. 给 bot 随便发一句话,再浏览器打开 https://api.telegram.org/bot<token>/getUpdates(<token> 连尖括号换成你的 token),在返回 JSON 里找 "chat":{"id":数字},那就是你的 chat id
  3. token 等于 bot 的钥匙:只填进服务器上的 config 文件,别发到任何聊天里

配置文件

cat > ~/watchdog/config <<'EOF'
BOT_TOKEN=填bot token
CHAT_ID=填你的chat id
EOF
chmod 600 ~/watchdog/config

cat > ~/watchdog/targets.txt <<'EOF'
我的VPS|1.2.3.4|22
EOF

探针脚本 check.sh

#!/bin/sh
DIR="$HOME/watchdog"
. "$DIR/config"

send() {
  code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
    -d "chat_id=${CHAT_ID}" -d "text=$1" 2>/dev/null)
  echo "$(date '+%Y-%m-%d %H:%M:%S') telegram -> HTTP ${code:-000} : $1" >> "$DIR/watchdog.log"
}

while IFS='|' read -r name ip port; do
  case "$name" in ''|'#'*) continue ;; esac
  STATE="$DIR/state.$name"
  FAILS="$DIR/fails.$name"

  ok=1
  ping -c 2 -W 2000 -t 5 "$ip" >/dev/null 2>&1 || ok=0
  if [ "$ok" = 1 ] && [ "$port" != "0" ]; then
    nc -z -w 5 "$ip" "$port" >/dev/null 2>&1 || ok=0
  fi

  prev=$(cat "$STATE" 2>/dev/null || echo up)
  fails=$(cat "$FAILS" 2>/dev/null || echo 0)

  if [ "$ok" = 0 ]; then
    fails=$((fails+1)); echo "$fails" > "$FAILS"
    if [ "$fails" -ge 2 ] && [ "$prev" = up ]; then
      send "🔴 [$name] 疑似下线:探测连续失败(serv00 站外探针)"
      echo down > "$STATE"
    fi
  else
    echo 0 > "$FAILS"
    if [ "$prev" = down ]; then
      send "🟢 [$name] 已恢复:探测重新通过"
    fi
    echo up > "$STATE"
  fi
done < "$DIR/targets.txt"

挂 cron,每 5 分钟一轮:

chmod +x ~/watchdog/check.sh
(crontab -l 2>/dev/null; echo '*/5 * * * * $HOME/watchdog/check.sh') | crontab -

查询 bot tg_bot.py

Python 标准库实现,不装任何第三方包,常驻内存约 25 MB。长轮询 Telegram 收消息,只响应 config 里那个 chat id 的命令;/status 会现场把全部目标探一遍再回复。

#!/usr/bin/env python3
import json, os, subprocess, time
import urllib.parse, urllib.request

DIR = os.path.expanduser("~/watchdog")
cfg = {}
for line in open(os.path.join(DIR, "config")):
    line = line.strip()
    if "=" in line and not line.startswith("#"):
        k, v = line.split("=", 1)
        cfg[k.strip()] = v.strip()
TOKEN, MY_CHAT = cfg["BOT_TOKEN"], cfg["CHAT_ID"]
API = "https://api.telegram.org/bot" + TOKEN

def api(method, **params):
    data = urllib.parse.urlencode(params).encode()
    req = urllib.request.Request(API + "/" + method, data=data)
    with urllib.request.urlopen(req, timeout=40) as r:
        return json.load(r)

def send(chat_id, text):
    try:
        api("sendMessage", chat_id=chat_id, text=text)
    except Exception:
        pass

def probe(ip, port):
    if subprocess.run(["ping", "-c", "2", "-W", "2000", "-t", "5", ip],
                      stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0:
        return False
    if port != "0":
        return subprocess.run(["nc", "-z", "-w", "5", ip, port],
                              stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
    return True

def status_text():
    lines = []
    for line in open(os.path.join(DIR, "targets.txt")):
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        name, ip, port = line.split("|")
        ok = probe(ip, port)
        sf = os.path.join(DIR, "state." + name)
        saved = open(sf).read().strip() if os.path.exists(sf) else "up"
        lines.append("%s %s (%s) 实时:%s / 记录:%s" %
                     ("🟢" if ok else "🔴", name, ip, "通" if ok else "不通", saved))
    return "\n".join(lines) if lines else "targets.txt 是空的"

def main():
    offset = 0
    try:
        r = api("getUpdates", timeout=0)
        if r.get("result"):
            offset = r["result"][-1]["update_id"] + 1
    except Exception:
        pass
    while True:
        try:
            r = api("getUpdates", offset=offset, timeout=30)
            for u in r.get("result", []):
                offset = u["update_id"] + 1
                msg = u.get("message") or {}
                chat_id = str((msg.get("chat") or {}).get("id", ""))
                text = (msg.get("text") or "").strip()
                if chat_id != MY_CHAT:
                    continue
                if text.startswith("/status") or text.startswith("/check"):
                    send(chat_id, status_text())
                elif text.startswith("/start") or text.startswith("/help"):
                    send(chat_id, "发 /status 立即探测全部目标")
        except Exception:
            time.sleep(5)

main()

托管并验收:

pm2 start ~/watchdog/tg_bot.py --name tg-bot --interpreter python3
pm2 save

然后在 Telegram 给 bot 发 /status,收到带 🟢/🔴 的清单就是全线通了。"实时"是现场探测结果,"记录"是探针脚本的状态机视角——刚出故障还没满 2 次失败时,两者会短暂不一致,正常。

小现象:bot 跑起来后,浏览器再打开 getUpdates 会返回空——消息被 bot 实时收走了,chat id 已经存好,不用再查。

先验报警通道(死 IP 测试)

往 targets.txt 加一行必死的目标,手动跑两次脚本(中间隔一分钟),收到 🔴 后删掉这行和它的状态文件:

测试|192.0.2.1|0
~/watchdog/check.sh; sleep 65; ~/watchdog/check.sh
rm -f ~/watchdog/state.测试 ~/watchdog/fails.测试

九、常用命令速查

devil(serv00 专用管理命令)

命令 作用
devil info account 账号全貌:计划、到期、服务器一览、Binexec 状态
devil port list 已预留端口
devil port add tcp 0 预留一个随机 TCP 端口(udp 同理)
devil port del tcp 端口 释放端口
devil www list 已建网站列表
devil mysql list / devil pgsql list / devil mongo list 数据库列表

PM2

命令 作用
pm2 start 程序 --name 名字 托管一个程序
pm2 list 进程清单(状态、内存、重启数)
pm2 logs 名字 --lines 20 看最近日志(排障第一步)
pm2 restart 名字 重启(重启后等 1~2 秒再测端口)
pm2 delete 名字 摘掉一个进程
pm2 save 把当前清单存档(配合 @reboot resurrect 自恢复)

系统

命令 作用
sockstat -l \| grep 端口 看某端口到底在监听什么地址/协议
crontab -l 当前定时任务
crontab -l \| sort -u \| crontab - cron 去重
quota / 面板 看自己的 3 GB 配额用量(别看 df)

实操环境:serv00 免费档(FreeBSD 14.3),2026 年 11 月。文中账号、端口、IP 均为示例,部署时换成你自己的。