remote-deploy.sh 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. #!/usr/bin/env bash
  2. set -Eeuo pipefail
  3. APP_DIR=/opt/xinghen
  4. PROXY_CONTAINER=meishi_ccdw_life-proxy-1
  5. NGINX_CONFIG=/opt/meishi_ccdw_life/nginx.conf
  6. PRIMARY_DOMAIN=xinghen.lessncosmos.com
  7. FUTURE_DOMAIN=xinghen.work
  8. ACME_ROOT=/etc/letsencrypt/acme-challenge
  9. MARKER_BEGIN='# BEGIN XINGHEN MANAGED BLOCK'
  10. MARKER_END='# END XINGHEN MANAGED BLOCK'
  11. cd "$APP_DIR"
  12. if [[ -f .deploy/app-image.tar.gz ]]; then
  13. gzip -dc .deploy/app-image.tar.gz | docker load
  14. rm -f .deploy/app-image.tar.gz
  15. fi
  16. if [[ ! -f .env ]]; then
  17. umask 077
  18. cat >.env <<EOF
  19. POSTGRES_PASSWORD=$(openssl rand -hex 24)
  20. ADMIN_PHONE=18888888888
  21. ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)
  22. EOF
  23. fi
  24. docker compose -p xinghen -f docker-compose.prod.yml up -d --remove-orphans
  25. install_nginx_block() {
  26. local block_file=$1
  27. local staged
  28. local backup
  29. staged=$(mktemp)
  30. backup="${NGINX_CONFIG}.backup-xinghen-$(date +%Y%m%d%H%M%S)"
  31. cp -a "$NGINX_CONFIG" "$backup"
  32. awk -v begin="$MARKER_BEGIN" -v end="$MARKER_END" '
  33. $0 == begin {skip=1; next}
  34. $0 == end {skip=0; next}
  35. !skip {lines[++n]=$0}
  36. END {
  37. last=n
  38. while (last > 0 && lines[last] ~ /^[[:space:]]*$/) last--
  39. if (lines[last] !~ /^}$/) exit 2
  40. for (i=1; i<last; i++) print lines[i]
  41. }
  42. ' "$NGINX_CONFIG" >"$staged"
  43. printf '\n%s\n' "$MARKER_BEGIN" >>"$staged"
  44. cat "$block_file" >>"$staged"
  45. printf '%s\n\n}\n' "$MARKER_END" >>"$staged"
  46. docker run --rm --network meishi_ccdw_life_default \
  47. -v "$staged:/etc/nginx/nginx.conf:ro" \
  48. -v /etc/letsencrypt:/etc/letsencrypt:ro \
  49. docker.m.daocloud.io/library/nginx:alpine nginx -t
  50. cp "$staged" "$NGINX_CONFIG"
  51. docker exec "$PROXY_CONTAINER" nginx -t
  52. docker exec "$PROXY_CONTAINER" nginx -s reload
  53. rm -f "$staged"
  54. }
  55. http_block=$(mktemp)
  56. cat >"$http_block" <<EOF
  57. server {
  58. listen 80;
  59. server_name $PRIMARY_DOMAIN;
  60. location ^~ /.well-known/acme-challenge/ {
  61. root $ACME_ROOT;
  62. default_type text/plain;
  63. try_files \$uri =404;
  64. }
  65. location / {
  66. proxy_pass http://xinghen-app:3001;
  67. proxy_set_header Host \$host;
  68. proxy_set_header X-Real-IP \$remote_addr;
  69. proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
  70. proxy_set_header X-Forwarded-Proto \$scheme;
  71. client_max_body_size 100m;
  72. }
  73. }
  74. EOF
  75. install_nginx_block "$http_block"
  76. rm -f "$http_block"
  77. certbot certonly --webroot -w "$ACME_ROOT" \
  78. --cert-name "$PRIMARY_DOMAIN" -d "$PRIMARY_DOMAIN" \
  79. --non-interactive --agree-tos --register-unsafely-without-email \
  80. --keep-until-expiring
  81. server_names="$PRIMARY_DOMAIN"
  82. cert_name="$PRIMARY_DOMAIN"
  83. if [[ "${ENABLE_FUTURE_DOMAIN:-0}" == 1 ]]; then
  84. resolved=$(getent ahostsv4 "$FUTURE_DOMAIN" | awk 'NR==1 {print $1}')
  85. if [[ "$resolved" != 47.93.193.127 ]]; then
  86. echo "$FUTURE_DOMAIN 尚未解析到 47.93.193.127,拒绝启用。" >&2
  87. exit 1
  88. fi
  89. certbot certonly --webroot -w "$ACME_ROOT" \
  90. --cert-name "$PRIMARY_DOMAIN" -d "$PRIMARY_DOMAIN" -d "$FUTURE_DOMAIN" \
  91. --non-interactive --agree-tos --register-unsafely-without-email --expand
  92. server_names="$PRIMARY_DOMAIN $FUTURE_DOMAIN"
  93. fi
  94. tls_block=$(mktemp)
  95. cat >"$tls_block" <<EOF
  96. server {
  97. listen 80;
  98. server_name $server_names;
  99. location ^~ /.well-known/acme-challenge/ {
  100. root $ACME_ROOT;
  101. default_type text/plain;
  102. try_files \$uri =404;
  103. }
  104. location / { return 301 https://\$host\$request_uri; }
  105. }
  106. server {
  107. listen 443 ssl;
  108. server_name $server_names;
  109. ssl_certificate /etc/letsencrypt/live/$cert_name/fullchain.pem;
  110. ssl_certificate_key /etc/letsencrypt/live/$cert_name/privkey.pem;
  111. ssl_protocols TLSv1.2 TLSv1.3;
  112. location / {
  113. proxy_pass http://xinghen-app:3001;
  114. proxy_set_header Host \$host;
  115. proxy_set_header X-Real-IP \$remote_addr;
  116. proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
  117. proxy_set_header X-Forwarded-Proto \$scheme;
  118. proxy_read_timeout 120s;
  119. client_max_body_size 100m;
  120. }
  121. }
  122. EOF
  123. install_nginx_block "$tls_block"
  124. rm -f "$tls_block"
  125. install -d -m 755 /etc/letsencrypt/renewal-hooks/deploy
  126. cat >/etc/letsencrypt/renewal-hooks/deploy/reload-xinghen-nginx.sh <<EOF
  127. #!/usr/bin/env bash
  128. docker exec $PROXY_CONTAINER nginx -t && docker exec $PROXY_CONTAINER nginx -s reload
  129. EOF
  130. chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-xinghen-nginx.sh
  131. healthy=0
  132. for _ in {1..15}; do
  133. if curl --noproxy '*' --fail --silent --max-time 5 \
  134. --resolve "$PRIMARY_DOMAIN:443:127.0.0.1" "https://$PRIMARY_DOMAIN/" >/dev/null; then
  135. healthy=1
  136. break
  137. fi
  138. sleep 1
  139. done
  140. if [[ "$healthy" != 1 ]]; then
  141. echo "HTTPS 健康检查失败:$PRIMARY_DOMAIN" >&2
  142. exit 1
  143. fi
  144. docker compose -p xinghen -f docker-compose.prod.yml ps