一台 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
国内平台(阿里云)买域名流程:
- 注册账号 + 实名认证(支付宝最快)
- 搜索购买(
.com约 ¥70/年) - 等待「注册局审核」(几小时)
- 不需要 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 反向代理
加新服务的模式固定是四步:
- DNS:阿里云加 A 记录
服务名.域名 → 服务器IP - compose:加一个服务(不暴露端口,只活在 Docker 内部网络)
- nginx:
conf.d/下新建独立.conf,按server_name区分、proxy_pass转发 - 证书:
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
四个连环坑,一个都不能少:
- 挂载别盖程序目录:数据目录挂
/app/data,不是/app(会盖掉程序报MODULE_NOT_FOUND) - 环境变量版本不同:旧版
WALINE_*前缀已废弃,新版用SQLITE_PATH(指目录)+JWT_TOKEN(必填) - Waline 不自动建表:必须下载官方模板放到数据目录:
curl -L -o /opt/waline-data/waline.sqlite \ https://raw.githubusercontent.com/walinejs/waline/main/assets/waline.sqlite - 前端脚本要用 UMD 版:
@waline/client@v3的dist/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 一键管理。
经验总结
- 安全加固先留退路:改配置前想好怎么回去
- 数据永远在宿主机:容器随便删,数据通过卷挂载保住
- 文档要看对版本:环境变量、配置格式会随版本变
- 静态网站最省资源:1G 内存跑一堆服务还绰绰有余
- 踩坑是最好的学习:这篇文章里的每个坑,都是亲手踩出来的