每个独立开发者都会经历这样一个阶段:项目越做越多,服务器越开越多,然后某天打开 AWS 账单,发现三个几乎没流量的应用每月要吃掉 $45。这就是我过去半年的真实写照——API 服务、定时任务、静态文档站点分别跑在各自的 EC2 t3.micro 上,典型的"过度设计税"。
为什么一开始用了三台 EC2
回看最初的架构决策,似乎每一步都"合理":
- API 服务(Express.js)——需要稳定的计算资源,单独一台实例。
- 定时任务(Python cron jobs)——考虑到可能的内存峰值,不敢和 API 混跑。
- 静态站点(文档站)——为了 SSL 证书管理的"便利",也用了一台 nginx 实例。
但三个 t3.micro 按需实例加起来就是每月 $45(含 EBS 存储),而三个应用的总 CPU 占用率连 15% 都不到。问题不是服务器太小,而是隔离过度。
Docker Compose + Nginx 反向代理的架构
重构的目标很明确:一台机器承载所有服务。选型上,AWS Lightsail 的 $8/mo 套餐(1 vCPU、1GB RAM、40GB SSD、2TB 流量)刚好够用。架构设计如下:
┌─────────────────────────────────────┐
│ Lightsail Instance │
│ ┌───────────────────────────────┐ │
│ │ Nginx (reverse proxy) │ │
│ │ :80 → redirect → :443 │ │
│ │ api.example.com → :3001 │ │
│ │ docs.example.com → :3002 │ │
│ └───────────────────────────────┘ │
│ ┌─────────┐ ┌─────────┐ ┌───────┐ │
│ │ API │ │ Docs │ │ Cron │ │
│ │ :3001 │ │ :3002 │ │ │ │
│ │ Node.js │ │ Nginx │ │Python │ │
│ └─────────┘ └─────────┘ └───────┘ │
│ ┌───────────────────────────────┐ │
│ │ Docker Compose Bridge │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
核心配置文件 docker-compose.yml 的骨架:
version: '3.8'
services:
nginx-proxy:
image: nginx:alpine
ports: ["80:80", "443:443"]
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./ssl:/etc/nginx/ssl
depends_on: [api, docs]
api:
build: ./api
expose: ["3001"]
environment:
- NODE_ENV=production
docs:
image: nginx:alpine
expose: ["3002"]
volumes:
- ./docs-build:/usr/share/nginx/html
cron:
build: ./cron
restart: unless-stopped
Let's Encrypt 证书自动续期的配置
多域名的 HTTPS 在没有 certbot 的环境下怎么搞?用了 acme.sh 配合 DNS API(Cloudflare),完全绕开了 HTTP 验证的端口占用问题:
# 首次签发
acme.sh --issue --dns dns_cf \
-d api.example.com \
-d docs.example.com
# 安装证书到 nginx 目录
acme.sh --install-cert -d api.example.com \
--key-file /etc/nginx/ssl/key.pem \
--fullchain-file /etc/nginx/ssl/cert.pem \
--reloadcmd "docker exec nginx-proxy nginx -s reload"
acme.sh 自带 cron 自动续期,无需额外的定时器配置。证书到期前 30 天自动触发续签,零运维负担。
使用 Watchtower 做容器自动更新
我把应用镜像推送到 Docker Hub 后,在 compose 中加入 watchtower 来实现滚动更新:
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --interval 3600 api docs cron
restart: unless-stopped
每小时检查一次指定容器的镜像更新,有新版就自动 pull 并重启。配合 depends_on 和 healthcheck,可以做到近乎无感的滚动更新。
日志集中管理方案
多容器环境下的日志如果各自为政,排查问题会是一场噩梦。我采用 Docker 的 json-file logging driver + Loki + Grafana 的轻量方案:
# docker-compose.yml 中统一日志配置
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
api:
logging: *default-logging
# ...
对于不太重要的定时任务,直接 docker logs 查看就够。关键是设好了日志轮转,防止一个报错循环吃光 40GB 硬盘——这是在免费容器上血淋淋的教训(这事下篇 blog 会展开讲)。
成本对比:$45/mo → $8/mo
| 项目 | 旧方案 | 新方案 |
|---|---|---|
| 计算 | 3× t3.micro ($25.20) | 1× Lightsail ($8.00) |
| EBS 存储 | 3× 30GB (~$12.00) | 包含在套餐内 |
| 流量 | 按量 (~$8.00) | 2TB 套餐内 |
| 合计 | ~$45.00/mo | $8.00/mo |
降幅 82%,年省 $444。而且 Lightsail 的 $8 套餐包含了固定流量配额,不像 EC2 那样每次被人扫端口都要为出站流量买单。
Lightsail 的性能瓶颈和应对策略
1GB RAM 说实话是临界线。以下是几个关键应对:
- Node.js 限制 heap:
--max-old-space-size=256,留出余量给其他进程。 - Python cron 串行化:避免多个定时任务同时触发造成内存尖峰。
- Swap 兜底:开启了 2GB swap 作为安全垫。虽然 SSD 上的 swap 速度感人,但比 OOM Kill 好得多。
- Nginx 微调:
worker_connections 512,减少内存占用。
对于个人项目和小团队工具来说,这台 $8 的 Lightsail 已经平稳跑了三个月,CPU 峰值 40%,内存稳定在 70% 附近。
从 $45 到 $8,不只是一次账单优化,更是一次架构上的"返璞归真"。很多时候,我们下意识地用更多的服务器来换取心智安宁,却忘了简单本身就是最好的可靠性。如果你也有几个散落各处的小应用,不妨试试把它们塞进一台机器——你可能只需要一个下午的 docker-compose 时间。