[{"data":1,"prerenderedAt":1323},["ShallowReactive",2],{"page-en-\u002Fblog\u002Fnginx-reverse-proxy-ssl":3},{"id":4,"title":5,"body":6,"date":1310,"description":1311,"extension":1312,"meta":1313,"navigation":110,"path":1314,"seo":1315,"stem":1316,"tags":1317,"translation":1321,"__hash__":1322},"pages_en\u002Fen\u002Fblog\u002Fnginx-reverse-proxy-ssl.md","Setting Up an Nginx Reverse Proxy with Let's Encrypt SSL",{"type":7,"value":8,"toc":1298},"minimark",[9,14,27,40,51,55,165,168,231,245,249,264,492,540,554,558,638,653,676,680,683,732,735,753,757,760,975,990,994,1000,1077,1092,1096,1172,1176,1269,1273,1294],[10,11,13],"h2",{"id":12},"introduction","Introduction",[15,16,17,18,22,23,26],"p",{},"Instead of exposing your app directly on ",[19,20,21],"code",{},":3000"," or ",[19,24,25],{},":8080",", putting a reverse proxy in front of it is the standard approach for both security and operations. In that role Nginx:",[28,29,30,34,37],"ul",{},[31,32,33],"li",{},"listens on ports 80\u002F443 and forwards requests to the app behind it,",[31,35,36],{},"terminates SSL\u002FTLS, so your app keeps speaking plain HTTP,",[31,38,39],{},"handles static files, gzip compression and multiple domains.",[15,41,42,43,46,47,50],{},"In this post we'll publish an app running on ",[19,44,45],{},"localhost:3000"," at ",[19,48,49],{},"app.example.com"," over HTTPS. Commands are for Ubuntu\u002FDebian; AlmaLinux\u002FRHEL differences are noted.",[10,52,54],{"id":53},"_1-installing-nginx","1. Installing Nginx",[56,57,62],"pre",{"className":58,"code":59,"language":60,"meta":61,"style":61},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# Ubuntu \u002F Debian\nsudo apt update && sudo apt install -y nginx\n\n# AlmaLinux \u002F RHEL\nsudo dnf install -y nginx\n\nsudo systemctl enable --now nginx\nsudo systemctl status nginx\n","bash","",[19,63,64,73,105,112,118,132,137,153],{"__ignoreMap":61},[65,66,69],"span",{"class":67,"line":68},"line",1,[65,70,72],{"class":71},"sHwdD","# Ubuntu \u002F Debian\n",[65,74,76,80,84,87,91,94,96,99,102],{"class":67,"line":75},2,[65,77,79],{"class":78},"sBMFI","sudo",[65,81,83],{"class":82},"sfazB"," apt",[65,85,86],{"class":82}," update",[65,88,90],{"class":89},"sMK4o"," &&",[65,92,93],{"class":78}," sudo",[65,95,83],{"class":82},[65,97,98],{"class":82}," install",[65,100,101],{"class":82}," -y",[65,103,104],{"class":82}," nginx\n",[65,106,108],{"class":67,"line":107},3,[65,109,111],{"emptyLinePlaceholder":110},true,"\n",[65,113,115],{"class":67,"line":114},4,[65,116,117],{"class":71},"# AlmaLinux \u002F RHEL\n",[65,119,121,123,126,128,130],{"class":67,"line":120},5,[65,122,79],{"class":78},[65,124,125],{"class":82}," dnf",[65,127,98],{"class":82},[65,129,101],{"class":82},[65,131,104],{"class":82},[65,133,135],{"class":67,"line":134},6,[65,136,111],{"emptyLinePlaceholder":110},[65,138,140,142,145,148,151],{"class":67,"line":139},7,[65,141,79],{"class":78},[65,143,144],{"class":82}," systemctl",[65,146,147],{"class":82}," enable",[65,149,150],{"class":82}," --now",[65,152,104],{"class":82},[65,154,156,158,160,163],{"class":67,"line":155},8,[65,157,79],{"class":78},[65,159,144],{"class":82},[65,161,162],{"class":82}," status",[65,164,104],{"class":82},[15,166,167],{},"If a firewall is enabled, allow 80 and 443:",[56,169,171],{"className":58,"code":170,"language":60,"meta":61,"style":61},"# Ubuntu (ufw)\nsudo ufw allow 'Nginx Full'\n\n# AlmaLinux (firewalld)\nsudo firewall-cmd --permanent --add-service=http --add-service=https\nsudo firewall-cmd --reload\n",[19,172,173,178,197,201,206,222],{"__ignoreMap":61},[65,174,175],{"class":67,"line":68},[65,176,177],{"class":71},"# Ubuntu (ufw)\n",[65,179,180,182,185,188,191,194],{"class":67,"line":75},[65,181,79],{"class":78},[65,183,184],{"class":82}," ufw",[65,186,187],{"class":82}," allow",[65,189,190],{"class":89}," '",[65,192,193],{"class":82},"Nginx Full",[65,195,196],{"class":89},"'\n",[65,198,199],{"class":67,"line":107},[65,200,111],{"emptyLinePlaceholder":110},[65,202,203],{"class":67,"line":114},[65,204,205],{"class":71},"# AlmaLinux (firewalld)\n",[65,207,208,210,213,216,219],{"class":67,"line":120},[65,209,79],{"class":78},[65,211,212],{"class":82}," firewall-cmd",[65,214,215],{"class":82}," --permanent",[65,217,218],{"class":82}," --add-service=http",[65,220,221],{"class":82}," --add-service=https\n",[65,223,224,226,228],{"class":67,"line":134},[65,225,79],{"class":78},[65,227,212],{"class":82},[65,229,230],{"class":82}," --reload\n",[232,233,234],"blockquote",{},[15,235,236,237,239,240,244],{},"Before continuing, ",[19,238,49],{}," needs a DNS ",[241,242,243],"strong",{},"A record"," pointing to the server's IP. Let's Encrypt verifies that you own the domain through it.",[10,246,248],{"id":247},"_2-the-reverse-proxy-block","2. The reverse proxy block",[15,250,251,252,255,256,259,260,263],{},"On Ubuntu, site files live in ",[19,253,254],{},"\u002Fetc\u002Fnginx\u002Fsites-available\u002F"," and are linked into ",[19,257,258],{},"sites-enabled\u002F",". On AlmaLinux, simply create ",[19,261,262],{},"\u002Fetc\u002Fnginx\u002Fconf.d\u002Fapp.conf",".",[56,265,269],{"className":266,"code":267,"language":268,"meta":61,"style":61},"language-nginx shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# \u002Fetc\u002Fnginx\u002Fsites-available\u002Fapp.example.com\nserver {\n    listen 80;\n    server_name app.example.com;\n\n    location \u002F {\n        proxy_pass http:\u002F\u002F127.0.0.1:3000;\n        proxy_http_version 1.1;\n\n        proxy_set_header Host              $host;\n        proxy_set_header X-Real-IP         $remote_addr;\n        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n\n        # WebSocket support\n        proxy_set_header Upgrade    $http_upgrade;\n        proxy_set_header Connection \"upgrade\";\n\n        proxy_read_timeout 60s;\n    }\n\n    client_max_body_size 20M;\n}\n","nginx",[19,270,271,276,286,298,307,311,322,332,342,347,364,379,394,409,414,420,435,448,453,464,470,475,486],{"__ignoreMap":61},[65,272,273],{"class":67,"line":68},[65,274,275],{"class":71},"# \u002Fetc\u002Fnginx\u002Fsites-available\u002Fapp.example.com\n",[65,277,278,282],{"class":67,"line":75},[65,279,281],{"class":280},"spNyl","server",[65,283,285],{"class":284},"sTEyZ"," {\n",[65,287,288,291,295],{"class":67,"line":107},[65,289,290],{"class":89},"    listen ",[65,292,294],{"class":293},"sbssI","80",[65,296,297],{"class":89},";\n",[65,299,300,303,305],{"class":67,"line":114},[65,301,302],{"class":89},"    server_name ",[65,304,49],{"class":284},[65,306,297],{"class":89},[65,308,309],{"class":67,"line":120},[65,310,111],{"emptyLinePlaceholder":110},[65,312,313,316,319],{"class":67,"line":134},[65,314,315],{"class":280},"    location",[65,317,318],{"class":78}," \u002F ",[65,320,321],{"class":284},"{\n",[65,323,324,327,330],{"class":67,"line":139},[65,325,326],{"class":89},"        proxy_pass ",[65,328,329],{"class":284},"http:\u002F\u002F127.0.0.1:3000",[65,331,297],{"class":89},[65,333,334,337,340],{"class":67,"line":155},[65,335,336],{"class":89},"        proxy_http_version ",[65,338,339],{"class":293},"1.1",[65,341,297],{"class":89},[65,343,345],{"class":67,"line":344},9,[65,346,111],{"emptyLinePlaceholder":110},[65,348,350,353,356,359,362],{"class":67,"line":349},10,[65,351,352],{"class":89},"        proxy_set_header ",[65,354,355],{"class":284},"Host              ",[65,357,358],{"class":89},"$",[65,360,361],{"class":284},"host",[65,363,297],{"class":89},[65,365,367,369,372,374,377],{"class":67,"line":366},11,[65,368,352],{"class":89},[65,370,371],{"class":284},"X-Real-IP         ",[65,373,358],{"class":89},[65,375,376],{"class":284},"remote_addr",[65,378,297],{"class":89},[65,380,382,384,387,389,392],{"class":67,"line":381},12,[65,383,352],{"class":89},[65,385,386],{"class":284},"X-Forwarded-For   ",[65,388,358],{"class":89},[65,390,391],{"class":284},"proxy_add_x_forwarded_for",[65,393,297],{"class":89},[65,395,397,399,402,404,407],{"class":67,"line":396},13,[65,398,352],{"class":89},[65,400,401],{"class":284},"X-Forwarded-Proto ",[65,403,358],{"class":89},[65,405,406],{"class":284},"scheme",[65,408,297],{"class":89},[65,410,412],{"class":67,"line":411},14,[65,413,111],{"emptyLinePlaceholder":110},[65,415,417],{"class":67,"line":416},15,[65,418,419],{"class":71},"        # WebSocket support\n",[65,421,423,425,428,430,433],{"class":67,"line":422},16,[65,424,352],{"class":89},[65,426,427],{"class":284},"Upgrade    ",[65,429,358],{"class":89},[65,431,432],{"class":284},"http_upgrade",[65,434,297],{"class":89},[65,436,438,440,443,446],{"class":67,"line":437},17,[65,439,352],{"class":89},[65,441,442],{"class":284},"Connection ",[65,444,445],{"class":82},"\"upgrade\"",[65,447,297],{"class":89},[65,449,451],{"class":67,"line":450},18,[65,452,111],{"emptyLinePlaceholder":110},[65,454,456,459,462],{"class":67,"line":455},19,[65,457,458],{"class":89},"        proxy_read_timeout ",[65,460,461],{"class":293},"60s",[65,463,297],{"class":89},[65,465,467],{"class":67,"line":466},20,[65,468,469],{"class":284},"    }\n",[65,471,473],{"class":67,"line":472},21,[65,474,111],{"emptyLinePlaceholder":110},[65,476,478,481,484],{"class":67,"line":477},22,[65,479,480],{"class":89},"    client_max_body_size ",[65,482,483],{"class":293},"20M",[65,485,297],{"class":89},[65,487,489],{"class":67,"line":488},23,[65,490,491],{"class":284},"}\n",[56,493,495],{"className":58,"code":494,"language":60,"meta":61,"style":61},"sudo ln -s \u002Fetc\u002Fnginx\u002Fsites-available\u002Fapp.example.com \u002Fetc\u002Fnginx\u002Fsites-enabled\u002F\nsudo nginx -t                 # test the configuration\nsudo systemctl reload nginx   # reload without downtime\n",[19,496,497,513,526],{"__ignoreMap":61},[65,498,499,501,504,507,510],{"class":67,"line":68},[65,500,79],{"class":78},[65,502,503],{"class":82}," ln",[65,505,506],{"class":82}," -s",[65,508,509],{"class":82}," \u002Fetc\u002Fnginx\u002Fsites-available\u002Fapp.example.com",[65,511,512],{"class":82}," \u002Fetc\u002Fnginx\u002Fsites-enabled\u002F\n",[65,514,515,517,520,523],{"class":67,"line":75},[65,516,79],{"class":78},[65,518,519],{"class":82}," nginx",[65,521,522],{"class":82}," -t",[65,524,525],{"class":71},"                 # test the configuration\n",[65,527,528,530,532,535,537],{"class":67,"line":107},[65,529,79],{"class":78},[65,531,144],{"class":82},[65,533,534],{"class":82}," reload",[65,536,519],{"class":82},[65,538,539],{"class":71},"   # reload without downtime\n",[15,541,542,543,546,547,550,551,263],{},"The ",[19,544,545],{},"X-Forwarded-*"," headers let the app know the real client IP and that the request came in over HTTPS. Spring Boot honors them with ",[19,548,549],{},"server.forward-headers-strategy=framework",", Express with ",[19,552,553],{},"app.set('trust proxy', 1)",[10,555,557],{"id":556},"_3-ssl-certificate-with-certbot","3. SSL certificate with Certbot",[56,559,561],{"className":58,"code":560,"language":60,"meta":61,"style":61},"# Ubuntu \u002F Debian\nsudo apt install -y certbot python3-certbot-nginx\n\n# AlmaLinux \u002F RHEL (requires EPEL)\nsudo dnf install -y epel-release\nsudo dnf install -y certbot python3-certbot-nginx\n\nsudo certbot --nginx -d app.example.com\n",[19,562,563,567,583,587,592,605,619,623],{"__ignoreMap":61},[65,564,565],{"class":67,"line":68},[65,566,72],{"class":71},[65,568,569,571,573,575,577,580],{"class":67,"line":75},[65,570,79],{"class":78},[65,572,83],{"class":82},[65,574,98],{"class":82},[65,576,101],{"class":82},[65,578,579],{"class":82}," certbot",[65,581,582],{"class":82}," python3-certbot-nginx\n",[65,584,585],{"class":67,"line":107},[65,586,111],{"emptyLinePlaceholder":110},[65,588,589],{"class":67,"line":114},[65,590,591],{"class":71},"# AlmaLinux \u002F RHEL (requires EPEL)\n",[65,593,594,596,598,600,602],{"class":67,"line":120},[65,595,79],{"class":78},[65,597,125],{"class":82},[65,599,98],{"class":82},[65,601,101],{"class":82},[65,603,604],{"class":82}," epel-release\n",[65,606,607,609,611,613,615,617],{"class":67,"line":134},[65,608,79],{"class":78},[65,610,125],{"class":82},[65,612,98],{"class":82},[65,614,101],{"class":82},[65,616,579],{"class":82},[65,618,582],{"class":82},[65,620,621],{"class":67,"line":139},[65,622,111],{"emptyLinePlaceholder":110},[65,624,625,627,629,632,635],{"class":67,"line":155},[65,626,79],{"class":78},[65,628,579],{"class":82},[65,630,631],{"class":82}," --nginx",[65,633,634],{"class":82}," -d",[65,636,637],{"class":82}," app.example.com\n",[15,639,640,641,644,645,648,649,652],{},"Certbot verifies the domain, stores the certificate under ",[19,642,643],{},"\u002Fetc\u002Fletsencrypt\u002Flive\u002Fapp.example.com\u002F",", and adds the ",[19,646,647],{},"listen 443 ssl"," block and the HTTP → HTTPS redirect to your Nginx config itself. For several domains, repeat ",[19,650,651],{},"-d",":",[56,654,656],{"className":58,"code":655,"language":60,"meta":61,"style":61},"sudo certbot --nginx -d example.com -d www.example.com\n",[19,657,658],{"__ignoreMap":61},[65,659,660,662,664,666,668,671,673],{"class":67,"line":68},[65,661,79],{"class":78},[65,663,579],{"class":82},[65,665,631],{"class":82},[65,667,634],{"class":82},[65,669,670],{"class":82}," example.com",[65,672,634],{"class":82},[65,674,675],{"class":82}," www.example.com\n",[10,677,679],{"id":678},"_4-automatic-renewal","4. Automatic renewal",[15,681,682],{},"Let's Encrypt certificates are valid for 90 days. The Certbot package installs a systemd timer for renewal:",[56,684,686],{"className":58,"code":685,"language":60,"meta":61,"style":61},"systemctl list-timers | grep certbot\nsudo certbot renew --dry-run    # test renewal\nsudo certbot certificates       # certificates and expiry dates\n",[19,687,688,705,720],{"__ignoreMap":61},[65,689,690,693,696,699,702],{"class":67,"line":68},[65,691,692],{"class":78},"systemctl",[65,694,695],{"class":82}," list-timers",[65,697,698],{"class":89}," |",[65,700,701],{"class":78}," grep",[65,703,704],{"class":82}," certbot\n",[65,706,707,709,711,714,717],{"class":67,"line":75},[65,708,79],{"class":78},[65,710,579],{"class":82},[65,712,713],{"class":82}," renew",[65,715,716],{"class":82}," --dry-run",[65,718,719],{"class":71},"    # test renewal\n",[65,721,722,724,726,729],{"class":67,"line":107},[65,723,79],{"class":78},[65,725,579],{"class":82},[65,727,728],{"class":82}," certificates",[65,730,731],{"class":71},"       # certificates and expiry dates\n",[15,733,734],{},"On AlmaLinux the timer may be disabled by default:",[56,736,738],{"className":58,"code":737,"language":60,"meta":61,"style":61},"sudo systemctl enable --now certbot-renew.timer\n",[19,739,740],{"__ignoreMap":61},[65,741,742,744,746,748,750],{"class":67,"line":68},[65,743,79],{"class":78},[65,745,144],{"class":82},[65,747,147],{"class":82},[65,749,150],{"class":82},[65,751,752],{"class":82}," certbot-renew.timer\n",[10,754,756],{"id":755},"_5-configuration-after-ssl","5. Configuration after SSL",[15,758,759],{},"It's good practice to add a few security and performance settings to the file Certbot edited:",[56,761,763],{"className":266,"code":762,"language":268,"meta":61,"style":61},"server {\n    listen 443 ssl;\n    http2 on;\n    server_name app.example.com;\n\n    ssl_certificate     \u002Fetc\u002Fletsencrypt\u002Flive\u002Fapp.example.com\u002Ffullchain.pem;\n    ssl_certificate_key \u002Fetc\u002Fletsencrypt\u002Flive\u002Fapp.example.com\u002Fprivkey.pem;\n    include \u002Fetc\u002Fletsencrypt\u002Foptions-ssl-nginx.conf;\n\n    add_header Strict-Transport-Security \"max-age=31536000\" always;\n    add_header X-Content-Type-Options nosniff;\n    add_header X-Frame-Options SAMEORIGIN;\n\n    gzip on;\n    gzip_types text\u002Fplain text\u002Fcss application\u002Fjson application\u002Fjavascript image\u002Fsvg+xml;\n\n    location \u002F {\n        proxy_pass http:\u002F\u002F127.0.0.1:3000;\n        # ... the proxy_set_header lines from before\n    }\n}\n\nserver {\n    listen 80;\n    server_name app.example.com;\n    return 301 https:\u002F\u002F$host$request_uri;\n}\n",[19,764,765,771,783,788,796,800,810,820,830,834,850,859,868,872,877,887,891,899,907,912,916,920,924,930,939,948,970],{"__ignoreMap":61},[65,766,767,769],{"class":67,"line":68},[65,768,281],{"class":280},[65,770,285],{"class":284},[65,772,773,775,778,781],{"class":67,"line":75},[65,774,290],{"class":89},[65,776,777],{"class":293},"443",[65,779,780],{"class":284}," ssl",[65,782,297],{"class":89},[65,784,785],{"class":67,"line":107},[65,786,787],{"class":89},"    http2 on;\n",[65,789,790,792,794],{"class":67,"line":114},[65,791,302],{"class":89},[65,793,49],{"class":284},[65,795,297],{"class":89},[65,797,798],{"class":67,"line":120},[65,799,111],{"emptyLinePlaceholder":110},[65,801,802,805,808],{"class":67,"line":134},[65,803,804],{"class":89},"    ssl_certificate ",[65,806,807],{"class":284},"    \u002Fetc\u002Fletsencrypt\u002Flive\u002Fapp.example.com\u002Ffullchain.pem",[65,809,297],{"class":89},[65,811,812,815,818],{"class":67,"line":139},[65,813,814],{"class":89},"    ssl_certificate_key ",[65,816,817],{"class":284},"\u002Fetc\u002Fletsencrypt\u002Flive\u002Fapp.example.com\u002Fprivkey.pem",[65,819,297],{"class":89},[65,821,822,825,828],{"class":67,"line":155},[65,823,824],{"class":89},"    include ",[65,826,827],{"class":284},"\u002Fetc\u002Fletsencrypt\u002Foptions-ssl-nginx.conf",[65,829,297],{"class":89},[65,831,832],{"class":67,"line":344},[65,833,111],{"emptyLinePlaceholder":110},[65,835,836,839,842,845,848],{"class":67,"line":349},[65,837,838],{"class":89},"    add_header ",[65,840,841],{"class":284},"Strict-Transport-Security ",[65,843,844],{"class":82},"\"max-age=31536000\"",[65,846,847],{"class":284}," always",[65,849,297],{"class":89},[65,851,852,854,857],{"class":67,"line":366},[65,853,838],{"class":89},[65,855,856],{"class":284},"X-Content-Type-Options nosniff",[65,858,297],{"class":89},[65,860,861,863,866],{"class":67,"line":381},[65,862,838],{"class":89},[65,864,865],{"class":284},"X-Frame-Options SAMEORIGIN",[65,867,297],{"class":89},[65,869,870],{"class":67,"line":396},[65,871,111],{"emptyLinePlaceholder":110},[65,873,874],{"class":67,"line":411},[65,875,876],{"class":89},"    gzip on;\n",[65,878,879,882,885],{"class":67,"line":416},[65,880,881],{"class":89},"    gzip_types ",[65,883,884],{"class":284},"text\u002Fplain text\u002Fcss application\u002Fjson application\u002Fjavascript image\u002Fsvg+xml",[65,886,297],{"class":89},[65,888,889],{"class":67,"line":422},[65,890,111],{"emptyLinePlaceholder":110},[65,892,893,895,897],{"class":67,"line":437},[65,894,315],{"class":280},[65,896,318],{"class":78},[65,898,321],{"class":284},[65,900,901,903,905],{"class":67,"line":450},[65,902,326],{"class":89},[65,904,329],{"class":284},[65,906,297],{"class":89},[65,908,909],{"class":67,"line":455},[65,910,911],{"class":71},"        # ... the proxy_set_header lines from before\n",[65,913,914],{"class":67,"line":466},[65,915,469],{"class":284},[65,917,918],{"class":67,"line":472},[65,919,491],{"class":284},[65,921,922],{"class":67,"line":477},[65,923,111],{"emptyLinePlaceholder":110},[65,925,926,928],{"class":67,"line":488},[65,927,281],{"class":280},[65,929,285],{"class":284},[65,931,933,935,937],{"class":67,"line":932},24,[65,934,290],{"class":89},[65,936,294],{"class":293},[65,938,297],{"class":89},[65,940,942,944,946],{"class":67,"line":941},25,[65,943,302],{"class":89},[65,945,49],{"class":284},[65,947,297],{"class":89},[65,949,951,955,958,961,963,965,967],{"class":67,"line":950},26,[65,952,954],{"class":953},"s7zQu","    return",[65,956,957],{"class":293}," 301",[65,959,960],{"class":284}," https:\u002F\u002F",[65,962,358],{"class":89},[65,964,361],{"class":284},[65,966,358],{"class":89},[65,968,969],{"class":284},"request_uri;\n",[65,971,973],{"class":67,"line":972},27,[65,974,491],{"class":284},[232,976,977],{},[15,978,542,979,982,983,986,987,263],{},[19,980,981],{},"http2 on;"," directive arrived in Nginx 1.25.1. On older versions write ",[19,984,985],{},"listen 443 ssl http2;","; check your version with ",[19,988,989],{},"nginx -v",[10,991,993],{"id":992},"_6-several-apps-one-server","6. Several apps, one server",[15,995,996,997,999],{},"To route different domains to different ports on the same server, write a separate ",[19,998,281],{}," block for each. Splitting by path under a single domain also works:",[56,1001,1003],{"className":266,"code":1002,"language":268,"meta":61,"style":61},"location \u002Fapi\u002F {\n    proxy_pass http:\u002F\u002F127.0.0.1:8080\u002F;   # trailing \u002F → \u002Fapi\u002Fusers → \u002Fusers\n}\n\nlocation \u002F {\n    root \u002Fvar\u002Fwww\u002Ffrontend;\n    try_files $uri $uri\u002F \u002Findex.html;    # SPA fallback\n}\n",[19,1004,1005,1015,1029,1033,1037,1045,1055,1073],{"__ignoreMap":61},[65,1006,1007,1010,1013],{"class":67,"line":68},[65,1008,1009],{"class":280},"location",[65,1011,1012],{"class":78}," \u002Fapi\u002F ",[65,1014,321],{"class":284},[65,1016,1017,1020,1023,1026],{"class":67,"line":75},[65,1018,1019],{"class":89},"    proxy_pass ",[65,1021,1022],{"class":284},"http:\u002F\u002F127.0.0.1:8080\u002F",[65,1024,1025],{"class":89},";",[65,1027,1028],{"class":71},"   # trailing \u002F → \u002Fapi\u002Fusers → \u002Fusers\n",[65,1030,1031],{"class":67,"line":107},[65,1032,491],{"class":284},[65,1034,1035],{"class":67,"line":114},[65,1036,111],{"emptyLinePlaceholder":110},[65,1038,1039,1041,1043],{"class":67,"line":120},[65,1040,1009],{"class":280},[65,1042,318],{"class":78},[65,1044,321],{"class":284},[65,1046,1047,1050,1053],{"class":67,"line":134},[65,1048,1049],{"class":89},"    root ",[65,1051,1052],{"class":284},"\u002Fvar\u002Fwww\u002Ffrontend",[65,1054,297],{"class":89},[65,1056,1057,1060,1063,1065,1068,1070],{"class":67,"line":139},[65,1058,1059],{"class":89},"    try_files $",[65,1061,1062],{"class":284},"uri ",[65,1064,358],{"class":89},[65,1066,1067],{"class":284},"uri\u002F \u002Findex.html",[65,1069,1025],{"class":89},[65,1071,1072],{"class":71},"    # SPA fallback\n",[65,1074,1075],{"class":67,"line":155},[65,1076,491],{"class":284},[15,1078,1079,1080,1083,1084,1087,1088,1091],{},"The trailing ",[19,1081,1082],{},"\u002F"," on ",[19,1085,1086],{},"proxy_pass"," matters: with it the matched ",[19,1089,1090],{},"\u002Fapi\u002F"," prefix is stripped, without it the request is forwarded as is.",[10,1093,1095],{"id":1094},"_7-everyday-commands","7. Everyday commands",[56,1097,1099],{"className":58,"code":1098,"language":60,"meta":61,"style":61},"sudo nginx -t                         # syntax check\nsudo nginx -T                         # dump the full merged configuration\nsudo systemctl reload nginx\nsudo tail -f \u002Fvar\u002Flog\u002Fnginx\u002Faccess.log\nsudo tail -f \u002Fvar\u002Flog\u002Fnginx\u002Ferror.log\ncurl -I https:\u002F\u002Fapp.example.com       # response headers\n",[19,1100,1101,1112,1124,1134,1147,1158],{"__ignoreMap":61},[65,1102,1103,1105,1107,1109],{"class":67,"line":68},[65,1104,79],{"class":78},[65,1106,519],{"class":82},[65,1108,522],{"class":82},[65,1110,1111],{"class":71},"                         # syntax check\n",[65,1113,1114,1116,1118,1121],{"class":67,"line":75},[65,1115,79],{"class":78},[65,1117,519],{"class":82},[65,1119,1120],{"class":82}," -T",[65,1122,1123],{"class":71},"                         # dump the full merged configuration\n",[65,1125,1126,1128,1130,1132],{"class":67,"line":107},[65,1127,79],{"class":78},[65,1129,144],{"class":82},[65,1131,534],{"class":82},[65,1133,104],{"class":82},[65,1135,1136,1138,1141,1144],{"class":67,"line":114},[65,1137,79],{"class":78},[65,1139,1140],{"class":82}," tail",[65,1142,1143],{"class":82}," -f",[65,1145,1146],{"class":82}," \u002Fvar\u002Flog\u002Fnginx\u002Faccess.log\n",[65,1148,1149,1151,1153,1155],{"class":67,"line":120},[65,1150,79],{"class":78},[65,1152,1140],{"class":82},[65,1154,1143],{"class":82},[65,1156,1157],{"class":82}," \u002Fvar\u002Flog\u002Fnginx\u002Ferror.log\n",[65,1159,1160,1163,1166,1169],{"class":67,"line":134},[65,1161,1162],{"class":78},"curl",[65,1164,1165],{"class":82}," -I",[65,1167,1168],{"class":82}," https:\u002F\u002Fapp.example.com",[65,1170,1171],{"class":71},"       # response headers\n",[10,1173,1175],{"id":1174},"_8-troubleshooting","8. Troubleshooting",[1177,1178,1179,1192],"table",{},[1180,1181,1182],"thead",{},[1183,1184,1185,1189],"tr",{},[1186,1187,1188],"th",{},"Error",[1186,1190,1191],{},"Cause \u002F Fix",[1193,1194,1195,1209,1220,1233,1246,1254],"tbody",{},[1183,1196,1197,1203],{},[1198,1199,1200],"td",{},[19,1201,1202],{},"502 Bad Gateway",[1198,1204,1205,1206],{},"The backend isn't running or the port is wrong; check with ",[19,1207,1208],{},"curl localhost:3000",[1183,1210,1211,1214],{},[1198,1212,1213],{},"502 on AlmaLinux while the app runs",[1198,1215,1216,1217],{},"SELinux blocks Nginx network connections: ",[19,1218,1219],{},"sudo setsebool -P httpd_can_network_connect 1",[1183,1221,1222,1227],{},[1198,1223,1224],{},[19,1225,1226],{},"504 Gateway Timeout",[1198,1228,1229,1230],{},"The app responds slowly; raise ",[19,1231,1232],{},"proxy_read_timeout",[1183,1234,1235,1240],{},[1198,1236,1237],{},[19,1238,1239],{},"413 Request Entity Too Large",[1198,1241,1242,1243],{},"Raise ",[19,1244,1245],{},"client_max_body_size",[1183,1247,1248,1251],{},[1198,1249,1250],{},"Certbot validation fails",[1198,1252,1253],{},"Wrong DNS A record or port 80 closed",[1183,1255,1256,1263],{},[1198,1257,1258,1259,1262],{},"The app generates ",[19,1260,1261],{},"http:\u002F\u002F"," redirects",[1198,1264,1265,1268],{},[19,1266,1267],{},"X-Forwarded-Proto"," is missing or the app doesn't trust proxy headers",[10,1270,1272],{"id":1271},"conclusion","Conclusion",[15,1274,1275,1276,1279,1280,1283,1284,1287,1288,1290,1291,263],{},"Nginx + Certbot is the most common way to put any app on HTTPS in a few minutes. Things to remember: ",[19,1277,1278],{},"nginx -t"," after every change, ",[19,1281,1282],{},"reload"," instead of ",[19,1285,1286],{},"restart",", pass the ",[19,1289,545],{}," headers to the app, and confirm renewal works once with ",[19,1292,1293],{},"certbot renew --dry-run",[1295,1296,1297],"style",{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"title":61,"searchDepth":75,"depth":75,"links":1299},[1300,1301,1302,1303,1304,1305,1306,1307,1308,1309],{"id":12,"depth":75,"text":13},{"id":53,"depth":75,"text":54},{"id":247,"depth":75,"text":248},{"id":556,"depth":75,"text":557},{"id":678,"depth":75,"text":679},{"id":755,"depth":75,"text":756},{"id":992,"depth":75,"text":993},{"id":1094,"depth":75,"text":1095},{"id":1174,"depth":75,"text":1175},{"id":1271,"depth":75,"text":1272},"2026-10-28","Putting a Node.js or Spring Boot app behind Nginx, getting a free SSL certificate with Certbot, and setting up automatic renewal.","md",{},"\u002Fen\u002Fblog\u002Fnginx-reverse-proxy-ssl",{"title":5,"description":1311},"en\u002Fblog\u002Fnginx-reverse-proxy-ssl",[268,1318,1319,1320],"linux","devops","guide","\u002Fblog\u002Fnginx-reverse-proxy-ssl","vFnM38AEU3mJh2DsJFP8TR2o5A1aSaEWbbRn0mfo2ls",1793147243542]