Lesson 0007:部署——把你的容器送上公网

前两课你学会了 Docker 和 CI/CD。但 CI/CD 流水线的终点——部署——还没讲。这课就是把你的容器真正跑在服务器上,让全世界能访问

部署全景图

一个典型的单服务器部署架构:

Internet → 80/443 → Nginx(反代)→ :3000 → Docker 容器(你的 App)
                      ↓ 静态文件
                      /var/www/html/
                      ↓ SSL
                      Let's Encrypt 证书,自动续期

你不需要理解所有组件才能开始。这节课的目标是:能手动把 Docker 容器部署到一台 Linux 服务器上。自动化(Terraform、Ansible)是后面的课。

第一步:准备服务器

你需要一台带公网 IP 的 Linux 服务器。选项:

方案费用适合
AWS EC2 t2.micro免费 12 个月学习 + 小项目
阿里云 ECS~¥50/月国内访问快
DigitalOcean Droplet$6/月最简单,5 分钟创建
Hetzner VPS~€4/月性价比最高
本地虚拟机(本课用)免费不花钱就能练
不花钱怎么练?用你本机的 Docker 跑一个「假服务器」:docker run -d --name fake-server -p 2222:22 -p 8080:80 ubuntu:22.04 tail -f /dev/null,然后 docker exec -it fake-server bash 进去。这就是你的「远程服务器」——所有命令跟 SSH 到真服务器一样,只是用 docker exec 代替 ssh

第二步:SSH——你登服务器的唯一方式

你已经有 SSH 密钥了(~/.ssh/id_ed25519)。部署流程如下:

# 1. 把公钥复制到服务器(一次性)
ssh-copy-id user@your-server-ip
# 等价于手动:cat ~/.ssh/id_ed25519.pub >> 服务器的 ~/.ssh/authorized_keys

# 2. 之后直接免密登录
ssh user@your-server-ip

# 3. 在服务器上装 Docker(一次性)
ssh user@your-server-ip << 'EOF'
  curl -fsSL https://get.docker.com | sh
  sudo usermod -aG docker $USER
  # 重新登录后 docker 命令不需要 sudo
EOF
SSH Key 权限必须是 600(只有你自己能读写),否则 SSH 拒绝使用。如果你遇到 Permissions 0644 are too open 错误:chmod 600 ~/.ssh/id_ed25519

第三步:把应用弄到服务器上

最简单的部署方式:在服务器上 git clone 然后 docker compose up

创建一个生产环境的 compose 文件:

# docker-compose.prod.yml(放在项目根目录)
services:
  app:
    build: .
    restart: always                 # 挂了自动重启
    ports:
      - "127.0.0.1:3000:3000"      # 只监听本地!不让外部直接访问
    environment:
      - NODE_ENV=production
    env_file:
      - .env.production             # 敏感配置独立文件(不提交到 git)
    logging:
      driver: "json-file"
      options:
        max-size: "10m"             # 限制日志大小
        max-file: "3"               # 只保留 3 个日志文件

  # 如果有数据库:
  # db:
  #   image: postgres:16-alpine
  #   restart: always
  #   volumes:
  #     - pgdata:/var/lib/postgresql/data
  #   environment:
  #     - POSTGRES_PASSWORD=${DB_PASSWORD}
  #   ports:
  #     - "127.0.0.1:5432:5432"

# volumes:
#   pgdata:

关键设计决策:

第四步:Nginx 反向代理

Nginx 站在你的应用前面,负责:接收外部 HTTP/HTTPS 请求 → 转发给你的应用容器 → 返回响应。

# 在服务器上装 Nginx
sudo apt update && sudo apt install -y nginx

# 配置反向代理:/etc/nginx/sites-available/myapp
sudo tee /etc/nginx/sites-available/myapp << 'EOF'
server {
    listen 80;
    server_name your-domain.com;           # 改成你的域名或 IP

    location / {
        proxy_pass http://127.0.0.1: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;
    }

    # 如果有静态文件:
    # location /static/ {
    #     alias /var/www/myapp/static/;
    #     expires 30d;
    #     add_header Cache-Control "public, immutable";
    # }
}
EOF

# 启用配置
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default   # 删掉默认站点
sudo nginx -t                              # 测试配置语法
sudo systemctl reload nginx                # 重载配置

第五步:HTTPS(Let's Encrypt 免费证书)

# 有域名的话,一行命令搞定 HTTPS
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com
# 证书自动续期:certbot 装好后会自动加 cron/systemd timer
sudo certbot renew --dry-run   # 测试续期是否正常

完整部署脚本

把以上步骤串成一个脚本——这是你以后每次部署跑的东西:

#!/bin/bash
# deploy.sh —— 在你的本地机器上跑,自动部署到远程服务器
set -euo pipefail

SERVER="user@your-server-ip"
APP_DIR="/opt/myapp"

echo "=== Deploying to $SERVER ==="

# 1. Sync code to server
rsync -avz --exclude 'node_modules' --exclude '.git' ./ $SERVER:$APP_DIR/

# 2. Build and restart on server
ssh $SERVER << 'ENDSSH'
  set -e
  cd /opt/myapp
  docker compose -f docker-compose.prod.yml down   # 停旧容器
  docker compose -f docker-compose.prod.yml up -d --build  # 构建并启动新容器
  docker image prune -f                             # 清理旧镜像
ENDSSH

echo "=== Deploy complete ==="
echo "Check: curl http://your-server-ip"

每次部署:./deploy.sh。你的 CI/CD 流水线(Lesson 0006)最终调用的也是类似的脚本。

rsync vs git pull:两种方式都行。rsync 更快(只传变化的文件),适合从 CI 机器部署。git pull 更简单,适合手动部署。选一个就行。

动手练习:在本地模拟一次部署

用 Docker 作为「远程服务器」,走一遍完整流程:

# === 你的「服务器」 ===
# 启动一个 Ubuntu 容器模拟远程服务器
docker run -d --name deploy-target \
  -p 2222:22 -p 8080:80 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  ubuntu:22.04 sleep infinity

# 进「服务器」装 Docker
docker exec deploy-target bash -c '
  apt update && apt install -y curl
  curl -fsSL https://get.docker.com | sh
'

# === 你的「应用」 ===
# 在 /tmp 创建一个简单应用(复用 Lesson 0005 的 demo)
cd /tmp && rm -rf deploy-demo && mkdir deploy-demo && cd deploy-demo

cat > server.js << 'JS'
const http = require("http");
const server = http.createServer((req, res) => {
  res.writeHead(200, {"Content-Type": "text/html"});
  res.end(`<h1>Deployed!</h1><p>Server time: ${new Date().toISOString()}</p>`);
});
server.listen(3000, () => console.log("Listening on :3000"));
JS

echo '{"name":"deploy-demo","private":true}' > package.json

cat > Dockerfile << 'DF'
FROM node:22-alpine
WORKDIR /app
COPY package.json server.js ./
EXPOSE 3000
CMD ["node", "server.js"]
DF

cat > docker-compose.yml << 'DC'
services:
  app:
    build: .
    restart: always
    ports:
      - "127.0.0.1:3000:3000"
DC

# 在「服务器」里启动应用
# (简化版:直接 docker exec 进去操作,模拟 SSH)
docker cp . deploy-target:/opt/app
docker exec deploy-target bash -c '
  cd /opt/app && docker compose up -d --build
'

# 验证
docker exec deploy-target curl -s http://127.0.0.1:3000
# 输出: 

Deployed!

Server time: 2026-07-19T...

# 清理 docker rm -f deploy-target

小测验

Q1:为什么生产环境端口要绑 127.0.0.1:3000:3000 而不是 3000:3000
Q2:restart: always 的作用是什么?
Q3:Nginx 在部署架构中的作用是什么?
Q4:SSH 密钥权限必须是 600,否则会怎样?

推荐资源

下一步

手动部署跑通了。下一课我们把基础设施管理也自动化——Infrastructure as Code (Terraform)。用代码定义云资源,一行 terraform apply 创建全套服务器 + 网络 + 防火墙。

没有云服务器?先把本地 Docker 模拟流程跑通。原理完全一样——真正的服务器只是多了公网 IP 和域名。等你拿到一台 VPS,把 docker exec 换成 ssh 就行。
← Lesson 0006 · 术语表 · Mission