Вопрос или проблема
Цель
С помощью веб-сервера Nginx запустите автономное приложение Node.js, которое обслуживает динамические страницы в ответ на API-запросы (например, ../tempestvue/.next/standalone/.next/server/app/api/weather/route.js
).
Проблема
Когда сервер Node.js работает через pm2
, конфигурация Nginx обслуживает только статическую страницу из проекта; трафик не достигает сервера. Результат одинаков через curl
или браузер.
Кроме того, при проверке страницы в Dev Tools->Network Tab
браузера я вижу 404 ошибки для таких путей, как: https://www.westwindwebworks.com/tempestvue/_next/static/chunks/webpack-d8cdd8b109dd43bc.js
.
Как обновить конфигурацию, чтобы достичь 404 активов?
Обновление 1:
В продакшене браузер достигает директории public
, и, похоже, ищет server.js на том же уровне, когда он на самом деле работает на уровень ниже:
/home/ec2-user/tempestvue/releases/20250128-013558/.next/standalone/server.js
Возвращает ли https://www.westwindwebworks.com/tempestvue/_next/static/css/79b4bd61dccf868b.css
404, потому что реальный путь к этому активу: https://www.westwindwebworks.com/tempestvue/_next/stanalone/static/css/79b4bd61dccf868b.css
?
basePath и next.config.mjs
import dotenv from "dotenv";
import path from "path";
const isProd = process.env.NODE_ENV === "production";
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
basePath: isProd ? "/tempestvue" : "", // Настройка basePath для продакшена
trailingSlash: true, // Рекомендуется для приложений с basePath
//reactStrictMode: false, // Отключите строгий режим React
env: {
BASE_PATH: process.env.BASE_PATH,
NEXT_PUBLIC_BASE_PATH: process.env.NEXT_PUBLIC_BASE_PATH,
REACT_APP_BASE_PATH: process.env.REACT_APP_BASE_PATH,
},
webpack: (config) => {
config.optimization.minimize = false; // Убедитесь в отсутствии минификации
config.externals = [...config.externals, { canvas: "canvas" }]; // Требуется для Chart.js
return config;
},
};
// Определение файла окружения
const envFile =
process.env.NODE_ENV === "production" ? ".env.production" : ".env.local";
console.log(`Using environment file: ${envFile}`);
// Загрузка .env.production или .env.local
dotenv.config({ path: path.resolve(process.cwd(), envFile) });
export default {
...nextConfig,
output: "standalone",
env: {
...Object.keys(process.env)
.filter((key) => key.startsWith("NEXT_PUBLIC_")) // Включены только публичные переменные
.reduce((env, key) => {
env[key] = process.env[key];
return env;
}, {}),
},
};
Фон
На сервере развёртывания структура директорий использует единую символическую ссылку в корневой папке проекта /tempestvue
, чтобы указывать на текущий выпуск.
/home/ec2-user/
└── tempestvue
├── 20250127-031305 -> /home/ec2-user/tempestvue/releases/20250127-031305
│ ├── ._public
│ ├── .next
│ │ ├── ._standalone
│ │ ├── ._static
│ │ ├── standalone
│ │ └── static
│ └── public
│ ├── ._TempestVue.png
│ ├── ._apple-icon copy.png
│ ├── ._favicon.svg
│ ├── ._globals.css
│ ├── ._icon.png
│ ├── ._manifest.json
│ ├── TempestVue.png
│ ├── apple-icon copy.png
│ ├── favicon.svg
│ ├── globals.css
│ ├── icon.png
│ └── manifest.json
└── releases
├── 20250126-011024
│ └── .next
├── 20250126-202835
│ └── .next
├── 20250126-220617
│ └── .next
├── 20250126-231144
│ └── .next
└── 20250127-031305
├── ._public
├── .next
└── public
Развёртывание
Сервер Node.js работает через pm2
здесь:
/home/ec2-user/tempestvue/releases/20250127-031305/.next/standalone/server.js
Конфигурация сервера
Примечание: Я пробовал несколько вариантов proxy_pass
безуспешно.
# Основной блок сервера для www
server {
server_name www.westwindwebworks.com;
root /usr/share/nginx/html;
# ------------------------------
# Добавить блок местоположения для TempestVue
# ------------------------------
location /tempestvue/ {
# 1) Переписывать /tempestvue/... на /..., чтобы Next видел все в
# корне проекта: /home/ec2-user/tempestvue/, а не в корне веба: /usr/share/nginx/html
rewrite ^/tempestvue/(.*) /$1 break;
# 2) Передать переписанный запрос вашему Node процессу
proxy_pass http://127.0.0.1:3000;
# proxy_pass http://127.0.0.1:3000/;
# proxy_pass http://127.0.0.1:3000/tempestvue;
# proxy_pass http://127.0.0.1:3000/tempestvue/;
proxy_http_version 1.1;
# Рекомендованные заголовки прокси:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
}
# Загрузка файлов конфигурации для стандартного блока сервера.
# include /etc/nginx/default.d/*.conf;
error_page 404 /404.html;
location = /404.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
listen 443 ssl; # IPv4 HTTPS
listen [::]:443 ssl; # IPv6 HTTPS
ssl_certificate /etc/letsencrypt/live/westwindwebworks.com/fullchain.pem; # управляется Certbot
ssl_certificate_key /etc/letsencrypt/live/westwindwebworks.com/privkey.pem; # управляется Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # управляется Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # управляется Certbot
}
Тестирование
[ec2-user@ip-172-31-24-45 tempestvue]$ curl -i https://www.westwindwebworks.com/tempestvue/ | html-beautify
возвращает статическую страницу из проекта:
Примечание: отредактировано для краткости, полный ответ в конце поста.
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 13307 0 13307 0 0 94041 0 --:--:-- --:--:-- --:--:-- 95050
HTTP/1.1 404 Not Found
Server: nginx/1.26.2
Date: Mon, 27 Jan 2025 12:39:03 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Accept-Encoding
X-Powered-By: Next.js
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/tempestvue/_next/static/css/79b4bd61dccf868b.css" data-precedence="next" />
<link rel="preload" as="script" fetchPriority="low" href="/tempestvue/_next/static/chunks/webpack-d8cdd8b109dd43bc.js" />
<script src="/tempestvue/_next/static/chunks/4bd1b696-1aaacd546f10f1e9.js" async=""></script>
<script src="/tempestvue/_next/static/chunks/517-2fa7b8a64c116b65.js" async=""></script>
<script src="/tempestvue/_next/static/chunks/main-app-dae125d39e3a4f3f.js" async=""></script>
<meta name="robots" content="noindex" />
<link rel="shortcut icon" href="/icon.png" />
<link rel="apple-touch-icon" href="/apple-icon.png" />
<link rel="icon" type="image/png" href="/icon.png" />
<title>404: Эта страница не найдена.</title>
<title>TempestVue</title>
<meta name="description" content="Панель визуализации погоды в реальном времени." />
<link rel="manifest" href="/manifest.json" />
<meta property="og:title" content="Tempest Weather | Sand Hills Beach, Scituate, MA" />
<meta property="og:description" content="Погодные условия в реальном времени и прогноз для Sand Hills Beach, Scituate, MA" />
<meta property="og:url" content="https://www.westwindwedworks.com/tempestvue/" />
<meta property="og:site_name" content="Tempest Weather" />
<meta property="og:locale" content="en_US" />
<meta property="og:image" content="https://www.westwindwedworks.com/tempestvue/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Tempest Weather | Sand Hills Beach, Scituate, MA" />
<meta name="twitter:description" content="Погодные условия в реальном времени и прогноз для Sand Hills Beach, Scituate, MA" />
<meta name="twitter:image" content="https://www.westwindwedworks.com/tempestvue/og-image.png" />
<meta name="twitter:image:width" content="1200" />
<meta name="twitter:image:height" content="630" />
<link rel="shortcut icon" href="/icon.png" />
<link rel="icon" href="/favicon-16x16.png" type="image/png" />
<link rel="icon" href="/favicon-32x32.png" type="image/png" />
<link rel="icon" href="/favicon-48x48.png" type="image/png" />
<link rel="apple-touch-icon" href="/apple-icon.png" type="image/png" />
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<script src="/tempestvue/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script>
</head>
<body>
...
</body>
С неправильным URL ~]$ curl -i https://www.westwindwebworks.com/tempestvu/ | html-beautify
возвращает стандартную 404 страницу Nginx из веб-корня /usr/share/nginx/html
:
[ec2-user@ip-172-31-24-45 ~]$ curl -i https://www.westwindwebworks.com/tempestvu/ | html-beautify
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 3650 100 3650 0 0 32791 0 --:--:-- --:--:-- --:--:-- 32882
HTTP/1.1 404 Not Found
Server: nginx/1.26.2
Date: Mon, 27 Jan 2025 12:57:47 GMT
Content-Type: text/html
Content-Length: 3650
Connection: keep-alive
ETag: "6793f381-e42"
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title>Страница не найдена</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style type="text/css">
...
</style>
</head>
<body>
<h1><strong>ошибка nginx!</strong></h1>
<div class="content">
<h3>Искомая страница не найдена.</h3>
....
</div>
</body>
Неотредактированный ответ для дальнейшего исследования
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/tempestvue/_next/static/css/79b4bd61dccf868b.css" data-precedence="next" />
<link rel="preload" as="script" fetchPriority="low" href="/tempestvue/_next/static/chunks/webpack-d8cdd8b109dd43bc.js" />
<script src="/tempestvue/_next/static/chunks/4bd1b696-1aaacd546f10f1e9.js" async=""></script>
<script src="/tempestvue/_next/static/chunks/517-2fa7b8a64c116b65.js" async=""></script>
<script src="/tempestvue/_next/static/chunks/main-app-dae125d39e3a4f3f.js" async=""></script>
<meta name="robots" content="noindex" />
<link rel="shortcut icon" href="/icon.png" />
<link rel="apple-touch-icon" href="/apple-icon.png" />
<link rel="icon" type="image/png" href="/icon.png" />
<title>404: Эта страница не найдена.</title>
<title>TempestVue</title>
<meta name="description" content="Панель визуализации погоды в реальном времени." />
<link rel="manifest" href="/manifest.json" />
<meta property="og:title" content="Tempest Weather | Sand Hills Beach, Scituate, MA" />
<meta property="og:description" content="Погодные условия в реальном времени и прогноз для Sand Hills Beach, Scituate, MA" />
<meta property="og:url" content="https://www.westwindwedworks.com/tempestvue/" />
<meta property="og:site_name" content="Tempest Weather" />
<meta property="og:locale" content="en_US" />
<meta property="og:image" content="https://www.westwindwedworks.com/tempestvue/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Tempest Weather | Sand Hills Beach, Scituate, MA" />
<meta name="twitter:description" content="Погодные условия в реальном времени и прогноз для Sand Hills Beach, Scituate, MA" />
<meta name="twitter:image" content="https://www.westwindwedworks.com/tempestvue/og-image.png" />
<meta name="twitter:image:width" content="1200" />
<meta name="twitter:image:height" content="630" />
<link rel="shortcut icon" href="/icon.png" />
<link rel="icon" href="/favicon-16x16.png" type="image/png" />
<link rel="icon" href="/favicon-32x32.png" type="image/png" />
<link rel="icon" href="/favicon-48x48.png" type="image/png" />
<link rel="apple-touch-icon" href="/apple-icon.png" type="image/png" />
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<script src="/tempestvue/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script>
</head>
<body class="bg-gradient-to-br from-gray-950 to-gray-900 text-white min-h-screen antialiased">
<header class="w-full h-14 md:h-16 lg:h-18 px-2 sm:px-4 md:px-6 lg:px-8 py-2 md:py-2.5 border-b border-white/10 backdrop-blur-md fixed top-0 z-50 bg-gradient-to-r from-blue-400/30 via-purple-400/30 to-blue-400/30">
<div class="max-w-7xl mx-auto h-full relative">
<div class="h-full flex items-center justify-center">
<h1 class="text-lg sm:text-xl md:text-2xl lg:text-3xl xl:text-4xl font-bold text-center tracking-tight"><span class="weather-text-gradient">TempestVue v1</span></h1>
</div>
</div>
</header>
<main class="pt-14 md:pt-16 lg:pt-18">
<div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center">
<div>
<style>
body {
color: #000;
background: #fff;
margin: 0
}
.next-error-h1 {
border-right: 1px solid rgba(0, 0, 0, .3)
}
@media (prefers-color-scheme:dark) {
body {
color: #fff;
background: #000
}
.next-error-h1 {
border-right: 1px solid rgba(255, 255, 255, .3)
}
}
</style>
<h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1>
<div style="display:inline-block">
<h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">Эта страница не найдена.</h2>
</div>
</div>
</div>
</main>
<script src="/tempestvue/_next/static/chunks/webpack-d8cdd8b109dd43bc.js" async=""></script>
<script>
(self.__next_f = self.__next_f || []).push([0])
</script>
<script>
self.__next_f.push([1, "1:\"$Sreact.fragment\"\n2:I[5244,[],\"\"]\n3:I[3866,[],\"\"]\n4:I[6213,[],\"OutletBoundary\"]\n6:I[6213,[],\"MetadataBoundary\"]\n8:I[6213,[],\"ViewportBoundary\"]\na:I[4835,[],\"\"]\n:HL[\"/tempestvue/_next/static/css/79b4bd61dccf868b.css\",\"style\"]\n"])
</script>
Вам необходимо настроить корневой URL вашего приложения в настройках приложения. В противном случае приложение будет генерировать недопустимые URL на страницах, что затем проявляется в 404 ошибках, которые вы видите.
То, что вы пытаетесь достичь, часто называют “размещением веб-приложения с префиксом URI” или “размещением веб-приложения базовом пути, отличном от корня”. Хотя эта задача может показаться простой, на практике она часто сложнее, чем кажется. Чтобы не повторяться, я рекомендую ознакомиться с этим ответом для всестороннего объяснения, почему это так.
Предполагая (судя по структуре каталогов вашего приложения), что ваше приложение использует фреймворк Next.js под капотом, вам нужно настроить параметр конфигурации basePath
. Если оно использует какую-то другую технологию, пожалуйста, предоставьте дополнительную информацию об этом.
Что я забыл упомянуть, так это то, что после настройки вашего веб-приложения с правильным базовым путем, вам не нужно изменять URI проксированного запроса. Вместо этого вы вкладываете ненужные усилия в удаление префикса URI /tempestvue
. В действительности, если мы начнем детально разбирать эту тему, есть два способа сделать это:
-
Правильный способ: Указание части URI для вышестоящего сервера в директиве
proxy_pass
с использованием следующей функциональности nginx:Если директива
proxy_pass
указана с URI, то при передаче запроса на сервер, часть нормализованного URI запроса, совпадающая с местоположением, заменяется на URI, указанный в директиве:location /name/ { proxy_pass http://127.0.0.1/remote/; }
Таким образом, с использованием следующей комбинации директив
location
иproxy_pass
:location /tempestvue/ { proxy_pass http://127.0.0.1:3000/;
Вы удаляете префикс
/tempestvue
из URI запроса, сохраняя при этом стандартное поведение неявной директивыproxy_redirect
:Стандартная замена, указанная параметром по умолчанию, использует параметры директив
location
иproxy_pass
. Таким образом, приведенные ниже конфигурации эквивалентны:location /one/ { proxy_pass http://upstream:port/two/; proxy_redirect default;
location /one/ { proxy_pass http://upstream:port/two/; proxy_redirect http://upstream:port/two/ /one/;
-
Неправильный способ: Использование директивы
rewrite ... break
:rewrite ^/tempestvue/(.*) /$1 break;
Таким образом, вы заставляете nginx игнорировать любую часть URI, указанную в директиве
proxy_pass
, и предотвращаете изменение ответа неявной директивойproxy_redirect default;
:В некоторых случаях часть URI запроса, которую необходимо заменить, не может быть определена:
- …
- Когда URI изменяется внутри проксируемого местоположения с использованием директивы
rewrite
, и та же конфигурация будет использоваться для обработки запроса (break
).
Однако, как уже упоминалось, как только ваше веб-приложение настроено на использование правильного базового пути, вам не нужно изменять проксированный запрос URI вообще:
location /tempestvue/ {
proxy_pass http://127.0.0.1:3000;
...
Дополнительный вопрос от автора:
Имеет ли разницу, если приложение было обслужено из веб-корня (например,
/usr/share/nginx/html/tempestvue
)? У меня есть другая целевая страница в веб-корне, поэтому я бы предпочёл запускать приложение из подкаталога.
С учетом вашей текущей конфигурации nginx вы проксировали любой запрос к вашему приложению, включая запросы на статические файлы, на ваш сервер Node.js, это не будет иметь разницы (однако, это будет правильным способом для полностью статических сборок приложений).
Вместо этого вы можете напрямую обслуживать статические файлы вашего приложения nginx, не включая Node.js, для некоторого повышения производительности:
location /tempestvue/ {
proxy_pass http://127.0.0.1:3000;
...
location /tempestvue/_next/static/ {
alias /home/ec2-user/tempestvue/20250127-031305/.next/static/;
}
}
.
Ответ или решение
Теория
Настройка Nginx для обслуживания приложения Node.js, находящегося вне веб-корня, может потребовать более глубокой конфигурации. В вашем случае приложение Next.js должно обрабатывать API-запросы и динамические страницы, но трафик не проходит через сервер Nginx, который в настоящий момент только обслуживает статическую страницу, а все остальные запросы возвращают 404 ошибки. Это свидетельствует о проблемах в маршрутизации и трансляции путей.
Основные проблемы могут быть связаны с неверной настройкой прокси-пути (proxy_pass
) или несоответствующей базовой директории (basePath
), что приводит к генерации неправильных URL-адресов. Ошибки 404 могут означать, что сервер Nginx не может найти статические ресурсы, расположенные на одном уровне, и вам нужно обновить конфигурацию, чтобы обеспечить корректную работу как статического, так и динамического контента.
Пример
Рассмотрим конфигурацию Nginx, которая могла бы решить ваши проблемы. Ваша текущая настройка, вероятно, выглядит следующим образом:
location /tempestvue/ {
rewrite ^/tempestvue/(.*) /$1 break;
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
}
Основная проблема здесь в том, что вы переписываете путь, использую директиву rewrite
, которая может изменять URI запроса и создавать сложности в маршрутизации. Более правильный подход — использовать proxy_pass
с URI:
location /tempestvue/ {
proxy_pass http://127.0.0.1:3000/;
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
}
Такой подход позволит корректно передавать URI сопровождаемый базовым префиксом, избегая необходимости в ненужном переписывании запросов.
Применение
Во-первых, убедитесь, что ваш next.config.mjs
корректно настроен и учитывает использование basePath
. Обновите директивы в Nginx, как показано выше, чтобы избегать применения rewrite
, что упрощает маршрутизацию и снижает риск ошибок, связанных с переписыванием путей.
Так же важно настроить Nginx для обслуживания статики напрямую без участия сервера Node.js, что поможет повысить производительность:
location /tempestvue/_next/static/ {
alias /home/ec2-user/tempestvue/20250127-031305/.next/static/;
}
Подобная настройка может особенно улучшить скорость отклика для статических файлов и снизить нагрузку на ваш сервер приложения.
Наконец, убедитесь, что Node.js сервер, работающий через pm2
, корректно обрабатывает API-запросы. Проверка правильности запуска и логов может указать на дополнительные проблемы с инициализацией или производительностью.
Таким образом, важно урегулировать настройку транспортировки путей в Nginx и учесть все элементы конфигурации, чтобы успешно задействовать приложение Node.js в условиях использования базового пути в виде подкаталога.