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

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

上一篇将镜像推送到了 Docker Hub。本篇将部署到 Ubuntu VPS,完成从镜像拉取到 HTTPS 证书配置的完整链路。

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

一、前置条件与 Docker

准备以下资源:

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

Let’s Encrypt 不能为裸 IP 签发普通 HTTPS 证书;DNS 和公网 80 是 HTTP-01 验证必要条件。

安装 Docker 前,必须先按 Docker Engine 在 Ubuntu 上使用官方仓库安装 的步骤设置 Docker 的官方 apt 仓库。若直接执行 apt install docker-ce,通常会出现 Unable to locate package,因为 Ubuntu 默认软件源不包含 Docker CE。

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

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 登录后生效):

sudo usermod -aG docker "$USER"
exit

重新 SSH 登录后运行 docker ps 验证。

安全提醒:docker 组具有近似 root 的主机控制能力,只应加入可信用户。


二、部署目录、环境变量与拉取镜像

2.1 创建目录结构

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

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
chmod 600 .env

2.3 拉取镜像

使用 Docker Hub Personal Access Token 登录并拉取:

docker login
docker pull YOUR_DOCKERHUB_USER/backend-api:1.0.0

认证存于当前用户的 ~/.docker/config.json,不要提交或复制到脚本中。


三、docker-compose.yml

创建 /opt/backend/docker-compose.yml

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(内部网络可见);只有 Nginx 用 ports 发布 80/443。数据库存于命名 volume,不要执行 docker compose down -v,除非确定要删除数据库。


四、HTTP、Nginx 与证书

4.1 首次 HTTP 配置

首次没有证书,创建 nginx/conf.d/api.conf

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;
    }
}

替换域名并启动:

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

4.2 申请 SSL 证书

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

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;
    }
}

注意:必须保留 ACME 路径,否则证书续期可能失败。ssl_certificate没有冒号

重新加载 Nginx:

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

五、自动续期与更新

5.1 Certbot 自动续期

编辑 crontab(crontab -e)加入:

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

演练续期:

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

5.2 更新 API 镜像

更新镜像时修改 API 标签:

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

回滚操作:改回旧标签并重新启动 API。


总结

本文完成了「单台 VPS 部署 API」的下半部分:从 Ubuntu VPS 的 Docker 安装、环境变量配置、Docker Compose 多服务编排,到 Nginx 反向代理与 Let’s Encrypt HTTPS 证书的申请、配置和自动续期。至此,整个「本地开发 → Docker Hub 镜像发布 → VPS 部署」的完整链路已全部打通。

后续建议增加:

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