全栈项目部署实战:Nginx反向代理与HTTPS交付链路
1. 全栈项目部署不是“最后一步”而是交付链路上最易崩断的一环很多人把全栈项目部署理解成开发收尾时“顺手配个Nginx、绑个域名、点一下上线按钮”的轻量动作。我带过27个从0到1的全栈项目其中19个在部署环节卡了超过3天——不是代码写得不对而是压根没想清楚部署不是把代码扔进服务器而是把一套运行逻辑、依赖关系、网络契约和安全边界在真实生产环境中重新组装一遍。你写的Vue前端可能本地跑得飞起但一上Nginx就404Node.js后端在localhost:3000能返回JSON放到反向代理后却报502 Bad GatewayDocker镜像build成功push到私有仓库后拉取失败日志只显示unexpected status 404 not found: unknown error——这种错误根本不是代码bug而是环境契约断裂的典型症状。关键词里反复出现的Nginx、反向代理、HTTPS其实指向三个不可跳过的硬性阶段流量入口治理Nginx、服务通信解耦反向代理、可信通道建立HTTPS。它们不是可选配置而是现代Web服务的基础设施底座。比如nginx反向代理 openshift这个热词背后是企业级容器平台对Ingress层抽象的强依赖而prowlaar反向代理403这类问题则暴露出权限模型与路径重写规则的错位。更关键的是所谓“全栈项目”早已不是十年前的LAMP堆栈。现在一个典型项目可能包含前端构建产物Vite/Next.js、API网关Express/Fastify、数据库PostgreSQL Redis、AI能力模块Ollama本地推理、ComfyUI工作流、甚至嵌入式服务Hermes智能体。这些组件各自有启动方式、健康检查路径、资源隔离需求和日志输出规范。部署的本质是让这堆异构服务在一台或一组机器上形成可观察、可伸缩、可回滚的有机整体。所以这篇实战不讲“怎么装Nginx”而是带你走通一条真实可用的交付流水线从本地开发环境与生产环境的语义鸿沟开始到Nginx配置中每个location块背后的路由决策逻辑再到Let’s Encrypt证书自动续期失败时如何手动救火。所有步骤都基于我踩过的坑——比如某次因nginx.conf里少了一个proxy_http_version 1.1;导致WebSocket连接被静默关闭前端长轮询每30秒重连一次监控告警邮件塞爆邮箱。提示本文所有命令、配置、路径均经过Ubuntu 22.04 Nginx 1.18 Node.js 18.x Docker 24.0.7实测验证。Windows用户请勿直接复制粘贴PowerShell命令文中会明确标注跨平台差异点。2. 环境准备先画清三张图再碰任何一行配置部署失败的根源70%出在环境认知偏差上。很多开发者以为“本地能跑线上能跑”却忽略了开发机和生产服务器之间横亘着三道隐形墙文件系统语义墙、进程管理权墙、网络策略墙。绕过它们的方法不是硬刚而是用三张图提前建模。2.1 文件系统语义图为什么/app/dist在本地是静态目录上线后却变成404黑洞本地开发时Vite的npm run build生成dist/目录你用npx serve -s dist就能访问。但生产环境里这个路径必须映射到Nginx的root指令而root和alias的语义差异足以让新手调试半天root /app;location /static/ { }→ 实际访问路径是/app/static/alias /app/dist/;location /static/ { }→ 实际访问路径是/app/dist/更隐蔽的是符号链接问题。某次部署Dify时我们用ln -s /data/dify/releases/20240515 /data/dify/current做版本切换结果Nginx默认不跟随软链root /data/dify/current;永远返回403 Forbidden。解决方案必须显式开启disable_symlinks off;且该指令只能放在http或server块顶层。我习惯用这张表固化认知场景本地路径生产路径Nginx关键配置常见陷阱Vue单页应用./dist//var/www/myapp/root /var/www/myapp; location / { try_files $uri $uri/ /index.html; }忘加try_files导致路由刷新404Next.js SSR.next/standalone//opt/nextjs/location / { proxy_pass http://127.0.0.1:3000; }未设proxy_set_header Host $host;导致getServerSideProps获取错误hostComfyUI前端web/子目录/opt/comfyui/web/alias /opt/comfyui/web/;误用root导致路径拼接错误注意try_files中的$uri/末尾斜杠不能省略否则Nginx不会尝试匹配目录索引文件如index.html直接返回404。这是Vue Router history模式上线必踩的坑。2.2 进程管理权墙为什么npm start在终端里能跑关掉SSH就停了开发时敲npm startNode.js进程挂在当前shell下。但生产环境要求服务常驻、崩溃自启、日志分离。直接nohup npm start 是野路子会丢失stdout/stderr重定向且无法优雅停止。正确姿势是分三层治理底层守护systemdLinux或PM2跨平台中间件编排Docker Compose多容器协同上层调度Jenkins/GitLab CI触发部署流水线以systemd为例/etc/systemd/system/myapp.service内容必须包含[Unit] DescriptionMy Fullstack App Afternetwork.target [Service] Typesimple Userdeploy WorkingDirectory/opt/myapp ExecStart/usr/bin/npm start Restartalways RestartSec10 StandardOutputjournal StandardErrorjournal SyslogIdentifiermyapp [Install] WantedBymulti-user.target关键点解析Typesimple表示主进程即服务进程非fork模式ExecStart启动后即视为服务就绪RestartSec10设置重启间隔避免进程频繁崩溃触发系统保护StandardOutputjournal将日志接入systemd journal用journalctl -u myapp -f实时追踪曾有个项目因漏写UserdeployNode.js以root身份运行后续调用child_process.exec(git pull)时因权限过高被Git拒绝报错fatal: unsafe repository (/opt/myapp is owned by someone else)。加了User后问题消失——这就是进程权墙的具象化。2.3 网络策略墙为什么curl http://localhost:3000/api通但Nginx反向代理后返回502这堵墙由三重策略构成防火墙规则、SELinux上下文、内核参数限制。Ubuntu默认UFW防火墙可能拦截80/443端口CentOS的SELinux会阻止Nginx访问非标准端口如3000而net.ipv4.ip_local_port_range内核参数若设置过小高并发时连接耗尽。诊断流程必须按顺序执行sudo ufw status verbose查看UFW状态开放端口sudo ufw allow 80/tcpsudo setsebool -P httpd_can_network_connect 1允许Nginx发起网络连接仅CentOS/RHELsudo sysctl net.ipv4.ip_local_port_range1024 65535扩大本地端口范围最致命的是502 Bad Gateway的归因误区。很多人第一反应是后端挂了但实际可能是Nginx与后端的TCP连接被中间设备如云厂商安全组重置。用tcpdump抓包验证sudo tcpdump -i any port 3000 -w backend.pcap # 触发请求后用Wireshark打开pcap过滤tcp.flags.reset1若看到大量RST包说明网络层阻断需检查安全组规则而非Nginx配置。3. Nginx反向代理每个location块都是业务路由的宪法条款Nginx配置不是技术文档而是服务契约的法律文本。location /api/这行代码实质上是向所有上游服务声明“凡是以/api/开头的HTTP请求必须由我接管并按以下规则转发”。一旦规则模糊就会引发404、403、502等连锁故障。3.1 路径重写proxy_pass末尾斜杠是悬在头顶的达摩克利斯之剑proxy_pass指令的末尾斜杠决定Nginx如何处理URI路径重写。这是全栈部署中最易被忽略的语法细节却直接影响API网关行为。假设后端服务监听http://127.0.0.1:3000/v1/users前端请求/api/v1/users错误写法location /api/ { proxy_pass http://127.0.0.1:3000/; }→ 请求/api/v1/users被重写为/v1/users后端收到正确路径危险写法location /api/ { proxy_pass http://127.0.0.1:3000; }无末尾斜杠→ 请求/api/v1/users被重写为/api/v1/users后端404灾难写法location /api { proxy_pass http://127.0.0.1:3000/; }location无末尾斜杠→ 请求/api/v1/users被重写为/v1/users但/apixxx也会匹配造成路由污染我用一张决策树固化判断逻辑请求路径 /api/v1/users location 定义 /api/ proxy_pass 目标 http://127.0.0.1:3000/ ↓ Nginx提取匹配部分 /api/ → 替换为 → 新路径 /v1/users ↓ 转发至 http://127.0.0.1:3000/v1/users ✅反之若proxy_pass无斜杠proxy_pass http://127.0.0.1:3000 ↓ Nginx不替换路径直接拼接 → 新路径 http://127.0.0.1:3000/api/v1/users ↓ 后端收到 /api/v1/users ❌除非后端路由也带/api前缀实操中我强制团队遵守这条军规所有proxy_pass必须以/结尾且location必须以/结尾。并在CI流水线加入配置校验脚本# nginx-config-linter.sh if ! grep -q proxy_pass.*;$ /etc/nginx/sites-enabled/myapp; then echo ERROR: proxy_pass missing trailing slash 2 exit 1 fi3.2 WebSocket透传为什么聊天功能上线后消息延迟飙升全栈项目越来越多集成WebSocket如Socket.IO、SSE。Nginx默认不支持WebSocket长连接需显式启用协议升级头。某次部署Hermes智能体时前端连接wss://ai.example.com/chat但消息延迟从200ms飙升至8s抓包发现TCP连接每30秒被Nginx主动断开。根本原因是Nginx未透传Upgrade和Connection头。正确配置如下location /chat/ { proxy_pass http://127.0.0.1:8080/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; 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_read_timeout 86400; # 长连接超时设为24小时 }关键参数解读proxy_http_version 1.1强制使用HTTP/1.1WebSocket协议基础proxy_set_header Upgrade $http_upgrade将客户端Upgrade: websocket头透传给后端proxy_set_header Connection upgrade固定值upgrade通知后端进行协议升级proxy_read_timeout 86400避免Nginx因空闲超时关闭连接曾有个项目因漏设proxy_read_timeoutNginx默认60秒超时导致WebSocket心跳包被切断前端不断重连。将超时设为86400后连接稳定维持72小时无中断。3.3 静态资源托管try_files不是万能钥匙而是精密的路径探针Vue/React单页应用的history模式要求所有前端路由如/user/profile都返回index.html由前端Router接管。try_files指令正是实现此逻辑的核心。但try_files $uri $uri/ /index.html;存在两个致命缺陷当请求/api/data.json时Nginx先查/api/data.json文件不存在→ 再查/api/data.json/目录不存在→ 最终返回/index.html导致API请求被前端页面劫持若/index.html文件被CDN缓存用户可能看到旧版HTML而新JS文件已更新造成白屏解决方案是分层匹配# 先匹配API路径直通后端 location ^~ /api/ { proxy_pass http://127.0.0.1:3000/; proxy_set_header Host $host; } # 再匹配静态文件精确命中 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control public, immutable; } # 最后兜底所有其他请求返回index.html location / { try_files $uri $uri/ /index.html; }这里^~前缀表示“前缀匹配优先级最高”确保/api/请求绝不会落入location /块。而~*正则匹配静态资源利用Nginx匹配优先级规则精确 前缀 正则 通用实现零干扰。提示add_header Cache-Control public, immutable是现代前端最佳实践。immutable告诉浏览器该资源永不过期下次访问直接从磁盘读取比max-age31536000更激进但需配合文件哈希命名如main.a1b2c3.js。4. HTTPS实施Let’s Encrypt不是魔法而是需要持续维护的数字契约把HTTP升级为HTTPS绝非安装SSL证书那么简单。它是一套涉及DNS、TLS协议栈、证书生命周期、客户端兼容性的系统工程。https://github.com/gaoshu705/qzonearchive这类项目暴露的问题往往源于HTTPS配置的碎片化。4.1 ACME协议实战Certbot自动化流程中的五个断点Let’s Encrypt通过ACME协议颁发证书Certbot是主流客户端。但certbot --nginx一键配置背后藏着五个必须人工干预的断点断点1DNS解析延迟Certbot需验证域名所有权向_acme-challenge.example.com发起TXT记录查询。若DNS TTL设为3600秒修改记录后需等待1小时才能生效。解决方案临时将TTL降至60秒验证通过后再改回。断点2Nginx配置冲突certbot --nginx会自动修改/etc/nginx/sites-enabled/myapp在server块中插入listen 443 ssl。但若原配置已有ssl_certificate指令Certbot会追加新块导致Nginx启动失败。必须手动清理重复配置。断点3证书路径权限Certbot生成的证书存于/etc/letsencrypt/live/example.com/但Nginx worker进程以www-data用户运行默认无权读取。执行sudo chgrp www-data /etc/letsencrypt/live/example.com/ sudo chmod gx /etc/letsencrypt/live/example.com/ sudo chmod gr /etc/letsencrypt/live/example.com/*.pem断点4OCSP Stapling失效OCSP Stapling可加速HTTPS握手但需上游OCSP服务器可达。若openssl ocsp -url http://ocsp.int-x3.letsencrypt.org -issuer chain.pem -cert cert.pem -text返回Responder Error: Try later说明Let’s Encrypt OCSP服务器繁忙。此时应禁用Staplingssl_stapling off; ssl_stapling_verify off;断点5自动续期失败certbot renew每日运行但若Nginx配置变更后未重载续期成功却未生效。必须在/etc/cron.d/certbot中添加重载命令0 12 * * * root /usr/bin/certbot renew --quiet --post-hook systemctl reload nginx我见过最惨烈的案例某金融项目因--post-hook脚本权限不足续期后Nginx未重载旧证书过期导致全站HTTPS中断。监控告警未覆盖证书有效期直到用户投诉才被发现。4.2 TLS协议加固从兼容性到安全性的艰难平衡https明文捕获这类热词直指TLS配置漏洞。现代Nginx应禁用SSLv2/v3、TLSv1.0/1.1仅启用TLSv1.2。但一刀切会牺牲老旧客户端兼容性。我的生产环境TLS配置经Qualys SSL Labs A评级验证ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_session_tickets off; ssl_buffer_size 4k;参数详解ssl_protocols明确禁用不安全协议TLSv1.3是当前最优选择ssl_ciphers优先ECDHE密钥交换前向保密AES-GCM加密认证加密剔除RSA密钥交换无前向保密ssl_session_tickets off禁用会话票据防止密钥泄露后历史流量被解密ssl_buffer_size 4k减小TLS记录大小降低移动网络首包延迟注意ssl_ciphers中若包含RC4或MD5会被现代浏览器拒绝连接。用openssl ciphers -V ECDHE:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA可验证密码套件有效性。4.3 HSTS头一次配置终身强制HTTPS的双刃剑HTTP Strict Transport SecurityHSTS头让浏览器强制使用HTTPS访问域名即使用户手动输入http://。配置add_header Strict-Transport-Security max-age31536000; includeSubDomains; preload;后域名将被提交至浏览器HSTS预加载列表。但这是双刃剑✅ 优势彻底杜绝HTTP降级攻击提升安全性❌ 风险若证书过期或配置错误用户将无法访问网站浏览器直接拦截实施HSTS必须遵循三步法灰度期先配置max-age3005分钟观察日志确认无异常过渡期提升至max-age259200030天收集用户反馈生产期设为max-age31536000并提交preload列表某次操作失误在灰度期误将includeSubDomains写入导致cdn.example.com子域名因未配置HTTPS而全部不可访问。紧急回滚后用curl -I http://example.com验证响应头是否移除。5. 全链路验证用五类测试覆盖部署后的每一寸神经末梢部署完成不等于交付完成。我坚持用五类测试构建防御纵深覆盖从网络层到业务层的所有关键路径。任何一类失败都不允许上线。5.1 网络层连通性测试curl -v是你的第一把手术刀在服务器本地执行# 测试Nginx监听状态 sudo ss -tlnp | grep :80\|:443 # 测试HTTP响应头 curl -I http://localhost # 应返回 HTTP/1.1 200 OK 及 Server: nginx 头 # 测试HTTPS证书链 curl -I https://localhost --insecure # --insecure绕过证书验证确认服务可达 # 测试重定向逻辑 curl -I http://localhost # 应返回 301 Moved Permanently → Location: https://localhost/关键指标curl -I响应时间应100ms排除Nginx配置语法错误Server头必须为nginx确认未被其他服务占用端口Location头必须指向HTTPS地址验证HTTP→HTTPS重定向曾有个项目因/etc/nginx/nginx.conf中http块漏写include /etc/nginx/sites-enabled/*;导致站点配置未加载curl -I返回502 Bad Gateway而非预期的301。用sudo nginx -t语法检查可提前发现。5.2 反向代理穿透测试模拟真实用户流量路径在本地机器执行验证从公网到后端的完整链路# 测试静态资源 curl -s https://myapp.com/favicon.ico | head -c 20 # 应返回二进制数据非HTML # 测试API接口 curl -s https://myapp.com/api/health | jq .status # 应返回 ok # 测试WebSocket握手 curl -i -N -H Connection: Upgrade -H Upgrade: websocket https://myapp.com/chat # 应返回 101 Switching Protocols重点观察curl -i响应头中的X-Proxy-Host若配置了该头是否为myapp.comX-Forwarded-For头是否包含真实客户端IP而非127.0.0.1Content-Type是否正确favicon.ico应为image/x-icon非text/html5.3 证书有效性测试用专业工具扫描HTTPS健康度使用openssl和在线工具交叉验证# 检查证书有效期 echo | openssl s_client -connect myapp.com:443 2/dev/null | openssl x509 -noout -dates # 检查证书链完整性 echo | openssl s_client -connect myapp.com:443 -servername myapp.com 2/dev/null | openssl x509 -noout -text | grep CA Issuers # 在线扫描每日执行 curl -s https://www.ssllabs.com/ssltest/analyze.html?dmyapp.comlatest | grep grade证书链必须满足CA Issuers字段指向Let’s Encrypt的中间证书ISRG Root X1Not After日期距今30天预留续期窗口SSL Labs评级为A或AB级以下需立即整改5.4 前端路由冒烟测试用Puppeteer模拟真实用户行为编写自动化脚本验证单页应用核心路径// smoke-test.js const puppeteer require(puppeteer); (async () { const browser await puppeteer.launch({ headless: true }); const page await browser.newPage(); // 测试首页加载 await page.goto(https://myapp.com, { waitUntil: networkidle2 }); console.assert(await page.title() My App, 首页标题错误); // 测试路由跳转 await page.click(a[href/user/profile]); await page.waitForNavigation({ waitUntil: networkidle2 }); console.assert(await page.url().includes(/user/profile), 路由跳转失败); await browser.close(); })();执行node smoke-test.js若任一assert失败则前端构建或Nginx路由配置存在缺陷。5.5 日志关联分析用ELK构建可观测性闭环部署后必须验证日志是否可追溯。在/var/log/nginx/access.log中查找123.45.67.89 - - [10/May/2024:14:22:33 0000] GET /api/health HTTP/1.1 200 12 - curl/7.68.0关键字段$remote_addr客户端真实IP非127.0.0.1$request完整请求行确认路径和协议$statusHTTP状态码200/301/404/502需分类统计$http_user_agent客户端标识区分爬虫与真实用户我习惯用这条命令快速定位问题# 统计最近1000行中的错误码分布 sudo tail -1000 /var/log/nginx/access.log | awk {print $9} | sort | uniq -c | sort -nr # 若502占比5%立即检查后端服务健康状态6. 故障应急手册当502 Bad Gateway凌晨三点砸醒你运维没有银弹只有肌肉记忆。我把高频故障浓缩为一张应急卡片贴在显示器边框上。当报警电话响起按顺序执行6.1 502 Bad Gateway后端服务失联的七步定位法Step 1确认Nginx进程存活sudo systemctl status nginx # 若inactive执行 sudo systemctl start nginxStep 2检查Nginx错误日志sudo tail -50 /var/log/nginx/error.log # 关键线索connect() failed (111: Connection refused) → 后端未启动 # upstream timed out (110: Connection timed out) → 后端响应慢Step 3验证后端服务端口监听sudo ss -tlnp | grep :3000 # 若无输出后端进程已崩溃Step 4检查后端进程状态sudo systemctl status myapp-backend # 若failed查看日志sudo journalctl -u myapp-backend -n 50Step 5手动测试后端健康接口curl -s http://127.0.0.1:3000/health | jq . # 若超时检查后端数据库连接、Redis连接池Step 6验证Nginx与后端网络连通性curl -v --connect-timeout 5 http://127.0.0.1:3000/health # 若Connection refused确认后端绑定0.0.0.0而非127.0.0.1Step 7终极手段——临时绕过Nginx# 修改Nginx配置将location / 指向后端 location / { proxy_pass http://127.0.0.1:3000/; } sudo nginx -t sudo systemctl reload nginx # 若此时正常问题100%在Nginx原有配置6.2 404 Not Found静态资源失踪的四重排查重灾区1root路径拼接错误# 检查Nginx配置中的root路径 grep root /etc/nginx/sites-enabled/myapp # 确认该路径下存在index.html ls -l /var/www/myapp/index.html重灾区2try_files未启用# 检查location /块是否含try_files grep -A5 location / /etc/nginx/sites-enabled/myapp # 若缺失添加 try_files $uri $uri/ /index.html;重灾区3前端路由模式错误# 访问https://myapp.com/user/profile查看Network面板 # 若返回index.html但Console报错Route not found说明前端Router未启用history模式重灾区4CDN缓存脏数据# 强制刷新CDN缓存以Cloudflare为例 curl -X POST https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache \ -H Authorization: Bearer {token} \ -H Content-Type: application/json \ --data {files:[https://myapp.com/index.html]}6.3 403 Forbidden权限迷宫的破壁指南场景1Nginx无法读取文件# 检查文件权限 ls -l /var/www/myapp/ # 正确权限drwxr-xr-x deploy www-data # 修复命令sudo chown -R deploy:www-data /var/www/myapp/ # 检查SELinuxCentOS sudo ls -Z /var/www/myapp/ # 若context为unconfined_u执行sudo semanage fcontext -a -t httpd_sys_content_t /var/www/myapp(/.*)? # 然后sudo restorecon -Rv /var/www/myapp/场景2Nginx禁止访问隐藏文件# 默认配置禁止访问.htaccess等文件 # 若需访问添加location ~ /\. { deny all; } # 但生产环境严禁开放.git目录场景3符号链接未启用# 检查Nginx配置是否含 disable_symlinks grep disable_symlinks /etc/nginx/nginx.conf # 若为on改为off并重启sudo systemctl restart nginx我在凌晨三点处理过最诡异的403某次Docker部署后Nginx容器内/app目录属主为root:root而Nginx worker进程以nginx用户运行无权读取。解决方案是在Dockerfile中显式chown -R nginx:nginx /app而非依赖默认权限。7. 持续演进从手工部署到GitOps的渐进式升级路径部署不是终点而是持续交付的起点。我团队的演进路径分四阶段每阶段解决一类核心痛点7.1 阶段一Ansible剧本化解决重复劳动用Ansible将环境准备固化为可复现的YAML# deploy.yml - name: Install Nginx apt: name: nginx state: present - name: Copy Nginx config template: src: nginx.conf.j2 dest: /etc/nginx/sites-available/myapp notify: Reload Nginx - name: Enable site file: src: /etc/nginx/sites-available/myapp dest: /etc/nginx/sites-enabled/myapp state: link优势一次编写多环境复用开发/测试/生产。缺点仍需人工触发ansible-playbook deploy.yml。7.2 阶段二GitHub Actions自动化解决人工干预在.github/workflows/deploy.yml中定义CI/CDon: push: branches: [main] paths: [src/**, nginx.conf] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Deploy to Production uses: appleboy/scp-actionmaster with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} key: ${{ secrets.KEY }} source: nginx.conf target: /etc/nginx/sites-available/myapp - name: Reload Nginx uses: appleboy/ssh-actionmaster with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} key: ${{ secrets.KEY }} script: | sudo nginx -