remote-deploy.sh 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  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. server_names="$PRIMARY_DOMAIN"
  56. if [[ "${ENABLE_FUTURE_DOMAIN:-0}" == 1 ]]; then
  57. resolved=$(getent ahostsv4 "$FUTURE_DOMAIN" | awk 'NR==1 {print $1}')
  58. if [[ "$resolved" != 47.93.193.127 ]]; then
  59. echo "$FUTURE_DOMAIN 尚未解析到 47.93.193.127,拒绝启用。" >&2
  60. exit 1
  61. fi
  62. server_names="$PRIMARY_DOMAIN $FUTURE_DOMAIN"
  63. fi
  64. http_block=$(mktemp)
  65. cat >"$http_block" <<EOF
  66. server {
  67. listen 80;
  68. server_name $server_names;
  69. location ^~ /.well-known/acme-challenge/ {
  70. root $ACME_ROOT;
  71. default_type text/plain;
  72. try_files \$uri =404;
  73. }
  74. location / {
  75. proxy_pass http://xinghen-app:3001;
  76. proxy_set_header Host \$host;
  77. proxy_set_header X-Real-IP \$remote_addr;
  78. proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
  79. proxy_set_header X-Forwarded-Proto \$scheme;
  80. client_max_body_size 100m;
  81. }
  82. }
  83. EOF
  84. install_nginx_block "$http_block"
  85. rm -f "$http_block"
  86. certbot certonly --webroot -w "$ACME_ROOT" \
  87. --cert-name "$PRIMARY_DOMAIN" -d "$PRIMARY_DOMAIN" \
  88. --non-interactive --agree-tos --register-unsafely-without-email \
  89. --keep-until-expiring
  90. cert_name="$PRIMARY_DOMAIN"
  91. if [[ "${ENABLE_FUTURE_DOMAIN:-0}" == 1 ]]; then
  92. certbot certonly --webroot -w "$ACME_ROOT" \
  93. --cert-name "$PRIMARY_DOMAIN" -d "$PRIMARY_DOMAIN" -d "$FUTURE_DOMAIN" \
  94. --non-interactive --agree-tos --register-unsafely-without-email --expand
  95. server_names="$PRIMARY_DOMAIN $FUTURE_DOMAIN"
  96. fi
  97. tls_block=$(mktemp)
  98. cat >"$tls_block" <<EOF
  99. server {
  100. listen 80;
  101. server_name $server_names;
  102. location ^~ /.well-known/acme-challenge/ {
  103. root $ACME_ROOT;
  104. default_type text/plain;
  105. try_files \$uri =404;
  106. }
  107. location / { return 301 https://\$host\$request_uri; }
  108. }
  109. server {
  110. listen 443 ssl;
  111. server_name $server_names;
  112. ssl_certificate /etc/letsencrypt/live/$cert_name/fullchain.pem;
  113. ssl_certificate_key /etc/letsencrypt/live/$cert_name/privkey.pem;
  114. ssl_protocols TLSv1.2 TLSv1.3;
  115. location / {
  116. proxy_pass http://xinghen-app:3001;
  117. proxy_set_header Host \$host;
  118. proxy_set_header X-Real-IP \$remote_addr;
  119. proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
  120. proxy_set_header X-Forwarded-Proto \$scheme;
  121. proxy_read_timeout 120s;
  122. client_max_body_size 100m;
  123. }
  124. }
  125. EOF
  126. install_nginx_block "$tls_block"
  127. rm -f "$tls_block"
  128. install -d -m 755 /etc/letsencrypt/renewal-hooks/deploy
  129. cat >/etc/letsencrypt/renewal-hooks/deploy/reload-xinghen-nginx.sh <<EOF
  130. #!/usr/bin/env bash
  131. docker exec $PROXY_CONTAINER nginx -t && docker exec $PROXY_CONTAINER nginx -s reload
  132. EOF
  133. chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-xinghen-nginx.sh
  134. healthy=0
  135. for _ in {1..15}; do
  136. if curl --noproxy '*' --fail --silent --max-time 5 \
  137. --resolve "$PRIMARY_DOMAIN:443:127.0.0.1" "https://$PRIMARY_DOMAIN/" >/dev/null; then
  138. healthy=1
  139. break
  140. fi
  141. sleep 1
  142. done
  143. if [[ "$healthy" != 1 ]]; then
  144. echo "HTTPS 健康检查失败:$PRIMARY_DOMAIN" >&2
  145. exit 1
  146. fi
  147. docker compose -p xinghen -f docker-compose.prod.yml ps