Deploying
此内容尚不支持你的语言。
# 生成 密钥ssh-keygen -t ed25519 -C "deploy" -f ~/.ssh/id_ed25519# 查看cat ~/.ssh/id_ed25519.pubvscode remote
Section titled “vscode remote”连接 阿里云 ECS
# Read more about SSH config files: https://linux.die.net/man/5/ssh_configHost aliyun HostName 公网IP User root# ssh root@47.98.xxx.xxxsudo useradd -m -s /bin/bash deploysudo passwd deployusermod -aG sudo deploy# 重启 sshd 服务sudo systemctl restart sshd非必须 安装
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 -yNginx config
Section titled “Nginx config”sudo nano /etc/nginx/sites-available/defaultserver { listen 80; server_name your-domain.com; return 301 https://$server_name$request_uri; # HTTP 重定向 HTTPS}# Default server configurationserver { 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; }}stop Nginx
Section titled “stop Nginx”sudo systemctl stop nginxdocker
Section titled “docker”安装
docker-compose
Section titled “docker-compose”docker-compose 用来用一个声明式文件(通常是 docker-compose.yml)定义并管理多个相关的容器服务(service)——把多容器应用(例如:web + 数据库 + 缓存)当作一个整体启动、停止、重建和网络互联
- 把多个 docker run 命令、网络、卷、环境变量等集中在一个文件里,便于复用与版本控- 制。
- 简化本地开发、测试与简单部署(只需一条命令就能启动整个应用栈)。
- 自动创建网络、挂载卷、处理依赖服务(如 db 在 web 之前启动)。
- 可在 CI/CD 或团队间共享相同的开发环境配置。
swap-deploy
Section titled “swap-deploy”docker-compose.yml
Section titled “docker-compose.yml”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: 3swap.sh
Section titled “swap.sh”#!/usr/bin/env bashset -euo pipefail
if [ "$#" -ne 1 ]; then echo "Usage: $0 <app1|app2>" exit 2fi
TARGET="$1"if [ "$TARGET" != "app1" ] && [ "$TARGET" != "app2" ]; then echo "Invalid target: $TARGET" exit 3fi
# map target to localhost portif [ "$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: $TARGETupstream 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 fifi# Replace atomicallysudo mv "$TMPFILE" "$UPSTREAM_FILE"sudo chown root:root "$UPSTREAM_FILE"sudo chmod 644 "$UPSTREAM_FILE"
# 2. 测试 nginx 配置并 reloadecho "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 5ficleanup_old.sh
Section titled “cleanup_old.sh”#!/usr/bin/env bashset -euo pipefail
if [ "$#" -lt 1 ]; then echo "Usage: $0 <app1|app2>" exit 2fi
OLD_SERVICE="$1"UPSTREAM_FILE="/etc/nginx/conf.d/upstream_backend.conf"
# Ensure upstream doesn't point to the OLD serviceif [ "$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 fielse if grep -q "127.0.0.1:3002" "$UPSTREAM_FILE"; then echo "Upstream still points to app2; aborting cleanup." exit 1 fifi
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" || truefi
echo "Removing compose service entry..."docker compose rm -f "$OLD_SERVICE" || true
echo "Cleanup of ${OLD_SERVICE} done."deploy.sh
Section titled “deploy.sh”#!/usr/bin/env bashset -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 "$@"nginx config
Section titled “nginx config”sudo nano /etc/nginx/sites-available/defaultserver { listen 80; server_name your-domain.com; return 301 https://$server_name$request_uri; # HTTP 重定向 HTTPS}# Default server configurationserver { 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 等操作。