Skip to content

Deploying

Terminal window
# 生成 密钥
ssh-keygen -t ed25519 -C "deploy" -f ~/.ssh/id_ed25519
# 查看
cat ~/.ssh/id_ed25519.pub

连接 阿里云 ECS

~/.ssh/config
# Read more about SSH config files: https://linux.die.net/man/5/ssh_config
Host aliyun
HostName 公网IP
User root
# ssh root@47.98.xxx.xxx
Terminal window
sudo useradd -m -s /bin/bash deploy
sudo passwd deploy
usermod -aG sudo deploy
# 重启 sshd 服务
sudo systemctl restart sshd

非必须 安装

Terminal window
sudo apt update
# 安装 Certbot(Let's Encrypt 免费证书)
sudo apt install certbot python3-certbot-nginx -y
# 获取 SSL 证书
sudo certbot certonly --standalone -d your-domain.com # 替换域名
#
sudo apt install nginx -y
Terminal window
sudo nano /etc/nginx/sites-available/default
Terminal window
server {
listen 80;
server_name your-domain.com;
return 301 https://$server_name$request_uri; # HTTP 重定向 HTTPS
}
# Default server configuration
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
# 反向代理到 Docker 容器 (Next.js on 3000)
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade'; # 支持 WebSocket (Socket.IO)
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;
proxy_cache_bypass $http_upgrade;
# 超时设置 (可选, 防慢请求)
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 错误页 (可选, 自定义 404)
error_page 404 /404.html;
location = /404.html {
internal;
}
# 禁用 .htaccess (如果有 Apache 遗留)
location ~ /\.ht {
deny all;
}
}
Terminal window
sudo systemctl stop nginx

安装

Terminal window

docker-compose 用来用一个声明式文件(通常是 docker-compose.yml)定义并管理多个相关的容器服务(service)——把多容器应用(例如:web + 数据库 + 缓存)当作一个整体启动、停止、重建和网络互联

  • 把多个 docker run 命令、网络、卷、环境变量等集中在一个文件里,便于复用与版本控- 制。
  • 简化本地开发、测试与简单部署(只需一条命令就能启动整个应用栈)。
  • 自动创建网络、挂载卷、处理依赖服务(如 db 在 web 之前启动)。
  • 可在 CI/CD 或团队间共享相同的开发环境配置。
services:
app1:
image: mcc-next
ports:
- "127.0.0.1:3001:3000" # 仅绑定到 localhost:3001
healthcheck:
test: [ "CMD-SHELL", "curl -f http://127.0.0.1:3000/api/health || exit 1" ]
interval: 5s
timeout: 3s
retries: 3
app2:
image: mcc-next
ports:
- "127.0.0.1:3002:3000" # 仅绑定到 localhost:3002
healthcheck:
test: [ "CMD-SHELL", "curl -f http://127.0.0.1:3000/api/health || exit 1" ]
interval: 5s
timeout: 3s
retries: 3
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <app1|app2>"
exit 2
fi
TARGET="$1"
if [ "$TARGET" != "app1" ] && [ "$TARGET" != "app2" ]; then
echo "Invalid target: $TARGET"
exit 3
fi
# map target to localhost port
if [ "$TARGET" = "app1" ]; then
BACKEND_ADDR="127.0.0.1:3001"
else
BACKEND_ADDR="127.0.0.1:3002"
fi
# 1. 生成 upstream include 文件(需要 root 权限写入 /etc/nginx/conf.d/)
UPSTREAM_FILE="/etc/nginx/conf.d/upstream_backend.conf"
TMPFILE="$(mktemp)"
cat > "$TMPFILE" <<EOF
# Generated by swap.sh - active backend: $TARGET
upstream backend {
server $BACKEND_ADDR;
}
EOF
# idempotent 检测
# 在 swap.sh 写入 TMP 前加入:
if [ -f "$UPSTREAM_FILE" ]; then
if grep -q "$BACKEND_ADDR" "$UPSTREAM_FILE"; then
echo "Upstream already points to $TARGET ($BACKEND_ADDR). Nothing to do."
exit 0
fi
fi
# Replace atomically
sudo mv "$TMPFILE" "$UPSTREAM_FILE"
sudo chown root:root "$UPSTREAM_FILE"
sudo chmod 644 "$UPSTREAM_FILE"
# 2. 测试 nginx 配置并 reload
echo "Testing nginx configuration..."
if sudo nginx -t; then
echo "Reloading nginx..."
sudo systemctl reload nginx
echo "Switched traffic to $TARGET ($BACKEND_ADDR)"
else
echo "nginx config test failed; not reloading"
exit 5
fi
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -lt 1 ]; then
echo "Usage: $0 <app1|app2>"
exit 2
fi
OLD_SERVICE="$1"
UPSTREAM_FILE="/etc/nginx/conf.d/upstream_backend.conf"
# Ensure upstream doesn't point to the OLD service
if [ "$OLD_SERVICE" = "app1" ]; then
if grep -q "127.0.0.1:3001" "$UPSTREAM_FILE"; then
echo "Upstream still points to app1; aborting cleanup."
exit 1
fi
else
if grep -q "127.0.0.1:3002" "$UPSTREAM_FILE"; then
echo "Upstream still points to app2; aborting cleanup."
exit 1
fi
fi
echo "Stopping ${OLD_SERVICE} gracefully..."
docker compose stop --timeout 30 "$OLD_SERVICE" || true
CID="$(docker compose ps -q "$OLD_SERVICE" || true)"
if [ -n "$CID" ]; then
echo "Removing container $CID"
docker rm -f "$CID" || true
fi
echo "Removing compose service entry..."
docker compose rm -f "$OLD_SERVICE" || true
echo "Cleanup of ${OLD_SERVICE} done."
#!/usr/bin/env bash
set -euo pipefail
# 配置区域(根据你的实际路径/端口调整)
UPSTREAM_FILE="/etc/nginx/conf.d/upstream_backend.conf"
APP1_PORT="127.0.0.1:3001"
APP2_PORT="127.0.0.1:3002"
APP1_SERVICE="app1"
APP2_SERVICE="app2"
LOCKFILE="/var/lock/deploy_replace.lock"
# 检测当前 active(返回 app1 或 app2)
detect_active() {
if [ -f "$UPSTREAM_FILE" ]; then
if grep -q "$APP1_PORT" "$UPSTREAM_FILE"; then
echo "app1"
return 0
elif grep -q "$APP2_PORT" "$UPSTREAM_FILE"; then
echo "app2"
return 0
else
# 无法识别
echo "unknown"
return 1
fi
else
echo "missing"
return 2
fi
}
# 简单健康检查函数
health_check() {
local addr="$1" # e.g. 127.0.0.1:3002
local retries="${2:-15}"
local sleep_s="${3:-2}"
local ok=1
for i in $(seq 1 $retries); do
if curl -fsS --max-time 3 "http://$addr/api/health" >/dev/null 2>&1; then
ok=0
break
fi
printf " health attempt %s/%s failed, sleep %ss...\n" "$i" "$retries" "$sleep_s"
sleep "$sleep_s"
done
return $ok
}
# 主流程
main() {
# 防止并发部署
exec 9>"$LOCKFILE"
if ! flock -n 9 ; then
echo "Another deploy is running. Exiting."
exit 5
fi
CURRENT=$(detect_active) || true
if [ "$CURRENT" = "missing" ]; then
echo "Warning: upstream file missing; assuming app1 active"
CURRENT="app1"
elif [ "$CURRENT" = "unknown" ]; then
echo "Warning: cannot detect active backend from $UPSTREAM_FILE; assuming app1 active"
CURRENT="app1"
fi
if [ "$CURRENT" = "app1" ]; then
INACTIVE="app2"
INACTIVE_PORT="$APP2_PORT"
else
INACTIVE="app1"
INACTIVE_PORT="$APP1_PORT"
fi
echo "Current active: $CURRENT"
echo "Will deploy to inactive: $INACTIVE (addr: $INACTIVE_PORT)"
echo "starting $INACTIVE..."
docker compose up -d --no-deps --no-build "$INACTIVE"
echo "Waiting for $INACTIVE health..."
if ! health_check "${INACTIVE_PORT}" 15 2; then
echo "Health check failed for $INACTIVE; stopping it and aborting."
docker compose stop "$INACTIVE" || true
exit 10
fi
echo "Health OK. Swapping traffic to $INACTIVE..."
sudo ./swap.sh "$INACTIVE" || { echo "swap failed"; exit 11; }
echo "Cleaning up old ($CURRENT) gracefully..."
sudo ./cleanup_old.sh "$CURRENT" 300 || echo "cleanup may have issues, check logs"
echo "Deployment to $INACTIVE finished. Active is now $INACTIVE."
}
main "$@"
Terminal window
sudo nano /etc/nginx/sites-available/default
Terminal window
server {
listen 80;
server_name your-domain.com;
return 301 https://$server_name$request_uri; # HTTP 重定向 HTTPS
}
# Default server configuration
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
# 反向代理到 Docker 容器 (Next.js on 3000)
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade'; # 支持 WebSocket (Socket.IO)
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;
proxy_cache_bypass $http_upgrade;
# 超时设置 (可选, 防慢请求)
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
  • 在 CI(GitHub Actions)上构建镜像并把镜像保存为 tar;
  • 用 SSH 将 tar 传到服务器(scp/rsync);
  • 在服务器上载入镜像(docker load),并用 docker compose 启动新服务(不重建镜像),然后运行 swap/cleanup 脚本完成替换部署;
  • 使用 Secrets 保存私钥与主机信息、并建议为 deploy 用户配置受限 sudo 权限以允许 reload nginx 等操作。
t1