一台 1核1G 的海外 VPS,从 SSH 都连不上,到最后拥有自己的网站、博客、监控和评论系统。这篇文章记录完整过程,以及所有踩过的坑。

一、缘起:SSH 连不上

第一次登录服务器时,为了安全禁用了 root 登录(PermitRootLogin no),结果把自己也锁在了外面:

ssh: connect to host ... port 2222: Connection timed out

排查方法:用 ssh -vvv 看完整日志——TCP 能连上、密钥协商正常、主机密钥校验通过,但认证阶段"密钥认了又被拒",再配合密码也被拒(Permission denied, please try again),基本可以锁定是 PermitRootLogin no

解决:通过 VPS 服务商控制台(VNC)进去,改回 PermitRootLogin yes 并重启 sshd:

sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
systemctl restart sshd

教训:安全加固前先确保留好退路(比如先配好密钥再禁密码)。

二、基础:安装 Docker

apt update
apt install -y docker.io docker-compose-v2
systemctl enable --now docker

两个核心概念贯穿始终:

  • 镜像(Image):打包好的软件模板(只读)
  • 容器(Container):镜像运行起来的实例,删了重建很方便

三、第一个网站:nginx 容器

docker run -d --name myweb -p 80:80 -v /opt/myweb:/usr/share/nginx/html nginx

两个最重要的参数:

  • -p 80:80端口映射,把容器内 80 端口暴露到服务器 80 端口
  • -v 宿主机目录:容器目录卷挂载,网站文件放宿主机 /opt/myweb,改文件刷新即生效,不用动容器

四、域名与 DNS

国内平台(阿里云)买域名流程:

  1. 注册账号 + 实名认证(支付宝最快)
  2. 搜索购买(.com 约 ¥70/年)
  3. 等待「注册局审核」(几小时)
  4. 不需要 ICP 备案——服务器在国外就绕开了备案

DNS 解析 = 一本分级的通讯录

输入 your-domain.com
  → 问 .xyz 总目录(注册局):这域名归谁管?
  → 得到:归 dns31/dns32.hichina.com 管
  → 问 dns31.hichina.com:IP 是多少?
  → 得到:你的服务器IP

注意:新域名即使审核通过,注册局"发布"到全球 DNS 还要等一段时间(我那次等了几小时)。可以用 nslookup 域名 8.8.8.8 轮询检测。

五、HTTPS 小绿锁

HTTPS = 加密 + 身份证明。证书就是"带公证章的身份证明",Let’s Encrypt 是免费公证处,certbot 是跑腿工具。

申请证书(webroot 方式):

docker run --rm \
  -v /opt/certbot-webroot:/var/www/certbot \
  -v /etc/letsencrypt:/etc/letsencrypt \
  certbot/certbot certonly --webroot -w /var/www/certbot \
  -d your-domain.com -d www.your-domain.com \
  --email you@example.com --agree-tos --no-eff-email

nginx 配置 443 + 强制跳转:

server {
    listen 80;
    server_name your-domain.com;
    location /.well-known/acme-challenge/ { root /var/www/certbot; }
    location / { return 301 https://$host$request_uri; }
}
server {
    listen 443 ssl;
    server_name your-domain.com;
    ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
    location / { root /usr/share/nginx/html; index index.html; }
}

自动续期(证书 90 天有效)——cron 每天凌晨 3 点检查续期:

crontab -e
# 加入一行:
0 3 * * * docker run --rm -v /opt/certbot-webroot:/var/www/certbot -v /etc/letsencrypt:/etc/letsencrypt certbot/certbot renew --quiet && docker exec myweb nginx -s reload

六、Docker Compose 化

把一长串 docker run 变成配置文件,以后一句 docker compose up -d 搞定:

services:
  web:
    image: nginx
    container_name: myweb
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /opt/myweb:/usr/share/nginx/html
      - /opt/nginx-conf:/etc/nginx/conf.d:ro
      - /opt/certbot-webroot:/var/www/certbot
      - /etc/letsencrypt:/etc/letsencrypt:ro

日常命令:docker compose ps(状态)、logs(日志)、restart(重启)、up -d(按配置重建)。

restart: unless-stopped 很重要:服务器重启后容器自动恢复。

七、多服务 + nginx 反向代理

加新服务的模式固定是四步:

  1. DNS:阿里云加 A 记录 服务名.域名 → 服务器IP
  2. compose:加一个服务(不暴露端口,只活在 Docker 内部网络)
  3. nginxconf.d/ 下新建独立 .conf,按 server_name 区分、proxy_pass 转发
  4. 证书certbot --expand 把新域名加进证书

nginx 反代核心(Kuma 为例):

server {
    listen 443 ssl;
    server_name kuma.your-domain.com;
    ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
    location / {
        proxy_pass http://uptime-kuma:3001;
        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_pass http://uptime-kuma:3001 —— compose 内部网络里用服务名当主机名,Docker 自动解析。

八、Hugo 博客

Hugo 是静态网站生成器:Markdown 文章 → 一次构建 → 一堆 HTML,nginx 直接托管,0 内存常驻

基本流程:

# 生成骨架
docker run --rm -u root -v /opt/hugo-blog:/hugo -w /hugo ghcr.io/gohugoio/hugo:latest new site .

# 装主题(Stack 或 PaperMod)
git clone --depth 1 https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod

# 写文章:content/posts/xxx.md
# 构建
docker run --rm -u root -v /opt/hugo-blog:/hugo -w /hugo ghcr.io/gohugoio/hugo:latest build

配置踩坑(TOML 语法):

# 错:先 [menu.main] 再 [[menu.main]] 会冲突
# 对:直接用数组元素
[[menus.main]]
    name = "首页"
    url = "/"
    weight = 1

本地写作工作流:在 Windows 本地写 md → 一键脚本同步 + 构建:

#!/bin/bash
set -e
scp -r content/* your-server:/opt/hugo-blog/content/
scp -r static/* your-server:/opt/hugo-blog/static/
ssh your-server 'docker run --rm -u root -v /opt/hugo-blog:/hugo -w /hugo ghcr.io/gohugoio/hugo:latest build'

九、评论系统 Waline(踩坑最多的一关)

为什么自托管:不依赖 GitHub/第三方,数据在自己手里,国内访问快。

Waline 正确的 compose 配置(v3 新版变量名):

  waline:
    image: lizheming/waline:latest
    container_name: waline
    restart: unless-stopped
    environment:
      - TZ=Asia/Shanghai
      - SQLITE_PATH=/app/data
      - JWT_TOKEN=你的随机密钥
      - SITE_NAME=我的博客
      - SITE_URL=https://blog.your-domain.com
      - SECURE_DOMAINS=blog.your-domain.com
      - AUTHOR_EMAIL=you@example.com
    volumes:
      - /opt/waline-data:/app/data

四个连环坑,一个都不能少:

  1. 挂载别盖程序目录:数据目录挂 /app/data,不是 /app(会盖掉程序报 MODULE_NOT_FOUND
  2. 环境变量版本不同:旧版 WALINE_* 前缀已废弃,新版用 SQLITE_PATH指目录)+ JWT_TOKEN(必填)
  3. Waline 不自动建表:必须下载官方模板放到数据目录:
    curl -L -o /opt/waline-data/waline.sqlite \
      https://raw.githubusercontent.com/walinejs/waline/main/assets/waline.sqlite
    
  4. 前端脚本要用 UMD 版@waline/client@v3dist/waline.js 是 ES Module,普通 <script> 会报 Unexpected token 'export',要改用 dist/waline.umd.js

博客接入(PaperMod 的 extend 钩子,插到每篇文章后):

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@waline/client@v3/dist/waline.css" />
<div id="waline"></div>
<script src="https://cdn.jsdelivr.net/npm/@waline/client@v3/dist/waline.umd.js"></script>
<script>
Waline.init({ el: '#waline', serverURL: 'https://comment.your-domain.com', lang: 'zh-CN' });
</script>

十、访问统计:不蒜子

一行脚本(PaperMod 页脚钩子 extend_footer.html):

<script async src="https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
<span>总访问量 <span id="busuanzi_value_site_pv"></span> 次 · 总访客 <span id="busuanzi_value_site_uv"></span> 人</span>

最终全家桶

子域名 服务 用途
your-domain.com 主站 跳转博客
blog.your-domain.com Hugo 博客 写作 + 评论 + 统计
kuma.your-domain.com Uptime Kuma 监控所有服务
memos.your-domain.com Memos 碎片笔记
comment.your-domain.com Waline 评论后端

全部 HTTPS,证书自动续期,Docker Compose 一键管理。

经验总结

  1. 安全加固先留退路:改配置前想好怎么回去
  2. 数据永远在宿主机:容器随便删,数据通过卷挂载保住
  3. 文档要看对版本:环境变量、配置格式会随版本变
  4. 静态网站最省资源:1G 内存跑一堆服务还绰绰有余
  5. 踩坑是最好的学习:这篇文章里的每个坑,都是亲手踩出来的