每个独立开发者都会经历这样一个阶段:项目越做越多,服务器越开越多,然后某天打开 AWS 账单,发现三个几乎没流量的应用每月要吃掉 $45。这就是我过去半年的真实写照——API 服务、定时任务、静态文档站点分别跑在各自的 EC2 t3.micro 上,典型的"过度设计税"。

为什么一开始用了三台 EC2

回看最初的架构决策,似乎每一步都"合理":

但三个 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 说实话是临界线。以下是几个关键应对:

对于个人项目和小团队工具来说,这台 $8 的 Lightsail 已经平稳跑了三个月,CPU 峰值 40%,内存稳定在 70% 附近。


从 $45 到 $8,不只是一次账单优化,更是一次架构上的"返璞归真"。很多时候,我们下意识地用更多的服务器来换取心智安宁,却忘了简单本身就是最好的可靠性。如果你也有几个散落各处的小应用,不妨试试把它们塞进一台机器——你可能只需要一个下午的 docker-compose 时间。