前两课你学会了 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 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/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
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:
关键设计决策:
restart: always — 容器挂掉或服务器重启后自动拉起。生产必加127.0.0.1:3000:3000 — 端口只绑在 localhost。外部流量必须走 Nginx(更安全)env_file — 敏感配置(数据库密码、API key)放在 .env.production,这个文件不要提交 git,手动 scp 到服务器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
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)最终调用的也是类似的脚本。
用 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
手动部署跑通了。下一课我们把基础设施管理也自动化——Infrastructure as Code (Terraform)。用代码定义云资源,一行 terraform apply 创建全套服务器 + 网络 + 防火墙。
docker exec 换成 ssh 就行。