单台 VPS 部署 API(下):拉取镜像、PostgreSQL、Nginx 与 HTTPS

上一篇已经把 API 镜像推送到 Docker Hub。这一篇在 Ubuntu VPS 上完成部署:先准备 Docker 和运行配置,再启动 API 与 PostgreSQL;确认 HTTP 访问正常后,配置 Nginx 和 HTTPS,最后设置证书续期与镜像更新。

1
Internet → Nginx :80/:443 → API :3000(内部网络) → PostgreSQL :5432(内部网络)

一、准备 VPS 和 Docker

开始前需要准备:

  • Ubuntu LTS VPS 与 sudo 权限
  • 已指向 VPS 的域名(示例 api.example.com)
  • 已开放 TCP 80/443
  • Docker Hub 私有镜像
  • API 的 GET /health 健康检查端点

Let’s Encrypt 不为裸 IP 签发这里使用的普通 HTTPS 证书。域名必须正确解析到 VPS,公网也要能访问 80 端口,HTTP-01 验证才能通过。

安装 Docker 前,先按照 Docker Engine 在 Ubuntu 上使用官方仓库安装 配置 Docker 官方 apt 仓库。Ubuntu 默认软件源不包含 Docker CE,直接执行 apt install docker-ce 通常会看到 Unable to locate package。

完成仓库设置后,安装 Docker:

1
2
3
4
5
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
docker version
docker compose version

把当前用户加入 docker 组后,需要重新登录 SSH 才会生效:

1
2
sudo usermod -aG docker "$USER"
exit

重新登录后运行 docker ps,确认当前用户可以访问 Docker。

docker 组几乎拥有 root 级别的主机控制能力,只把可信用户加入该组。

二、准备部署文件和镜像

2.1 创建部署目录

1
2
3
sudo mkdir -p /opt/backend/nginx/conf.d /opt/backend/certbot/www /opt/backend/certbot/conf
sudo chown -R "$USER":"$USER" /opt/backend
cd /opt/backend

2.2 配置环境变量

创建 .env:

1
2
3
4
POSTGRES_USER=app_user
POSTGRES_PASSWORD=REPLACE_WITH_A_LONG_RANDOM_PASSWORD
POSTGRES_DB=app_db
DATABASE_URL=postgresql://app_user:***@postgres:5432/app_db
1
chmod 600 .env

2.3 登录并拉取镜像

使用 Docker Hub Personal Access Token 登录,然后拉取上一篇发布的镜像:

1
2
docker login
docker pull YOUR_DOCKERHUB_USER/backend-api:1.0.0

登录信息保存在当前用户的 ~/.docker/config.json 中。不要提交这个文件,也不要把其中的内容复制到脚本里。

三、定义 Compose 服务

在 /opt/backend 创建 docker-compose.yml,定义 API、PostgreSQL、Nginx 和 Certbot:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
services:
api:
image: YOUR_DOCKERHUB_USER/backend-api:1.0.0
restart: unless-stopped
environment:
NODE_ENV: production
DATABASE_URL: ${DATABASE_URL}
expose:
- "3000"
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
expose:
- "5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
nginx:
image: nginx:1.27-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certbot/www:/var/www/certbot:ro
- ./certbot/conf:/etc/letsencrypt:ro
depends_on:
- api
certbot:
image: certbot/certbot:latest
volumes:
- ./certbot/www:/var/www/certbot
- ./certbot/conf:/etc/letsencrypt
volumes:
postgres_data:

API 和 PostgreSQL 使用 expose,端口只在 Compose 内部网络中可见;Nginx 通过 ports 向公网开放 80 和 443。数据库保存在命名 volume 中。除非确实要删除数据库,否则不要执行 docker compose down -v。

四、先验证 HTTP,再启用 HTTPS

4.1 配置 HTTP 入口

首次启动时还没有证书,先创建一份只监听 HTTP 的 nginx/conf.d/api.conf。保留 ACME 验证路径,Certbot 会通过它完成 HTTP-01 验证:

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name api.example.com;
location /.well-known/acme-challenge/ { root /var/www/certbot; }
location / {
proxy_pass http://api:3000;
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;
}
}

把示例域名换成自己的域名,然后启动服务并检查健康端点:

1
2
3
docker compose pull
docker compose up -d
curl -i http://api.example.com/health

4.2 申请证书

1
2
3
4
docker compose run --rm certbot certonly \
--webroot --webroot-path=/var/www/certbot \
--email YOUR_EMAIL@example.com --agree-tos --no-eff-email \
-d api.example.com

4.3 切换到 HTTPS

证书签发成功后,把 nginx/conf.d/api.conf 更新为 HTTPS 配置,并让 HTTP 请求跳转到 HTTPS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
listen 80;
server_name api.example.com;
location /.well-known/acme-challenge/ { root /var/www/certbot; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://api:3000;
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;
}
}

HTTP 配置中仍需保留 ACME 路径,否则续期验证可能失败。ssl_certificate 后面没有冒号。

重新加载 Nginx:

1
2
3
4
docker compose exec nginx nginx -t
docker compose exec nginx nginx -s reload
curl -I http://api.example.com/health
curl -i https://api.example.com/health

五、维护证书和 API 版本

5.1 设置 Certbot 自动续期

运行 crontab -e,加入下面这行,让 Certbot 定期检查并续期证书:

1
17 3 * * * cd /opt/backend && docker compose run --rm certbot renew --quiet && docker compose exec -T nginx nginx -s reload

演练续期:

1
docker compose run --rm certbot renew --dry-run

5.2 发布 API 新版本

发布新版本时,先把 Compose 文件中的 API 镜像标签改为新版本,再拉取镜像并重建 API 容器:

1
2
3
4
docker compose pull api
docker compose up -d api
docker compose logs --tail=100 api
curl -fsS https://api.example.com/health

需要回滚时,把标签改回旧版本,再重新启动 API。


总结

现在,API 和 PostgreSQL 运行在 Compose 内部网络中,公网请求由 Nginx 转发,Certbot 负责续期证书。这套配置可以作为单机部署的起点;数据库备份和日志管理还需要单独配置。

后续还可以补充:

  • PostgreSQL 异地备份策略
  • Docker 日志轮转配置
  • Docker Compose 健康检查告警