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

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

上一篇把 API 镜像推送到了 Docker Hub。这一篇转到 Ubuntu VPS:拉取镜像,运行 API 和 PostgreSQL,再让 Nginx 接管公网流量和 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 证书。域名必须正确解析到 VPS,公网也要能访问 80 端口,HTTP-01 验证才能通过。

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

完成仓库设置后,安装 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

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

安全提醒: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,端口仅在 Compose 内部网络中可见;Nginx 才通过 ports 向公网开放 80/443。数据库保存在命名 volume 中。除非确实要删除数据库,否则不要执行 docker compose down -v


四、HTTP、Nginx 与证书

4.1 首次 HTTP 配置

第一次启动时还没有证书,先创建一份只监听 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 -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 镜像

发布新版本时,先修改 Compose 文件中的 API 镜像标签,再执行:

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 健康检查告警