Конфигурация Nginx не может обслуживать приложение Node.js, не расположенное в корне веб-сервера.

Вопрос или проблема

Цель

С помощью веб-сервера 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 активов?

Браузер с вкладкой сети показывает пути 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,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;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 в условиях использования базового пути в виде подкаталога.

Оцените материал
Добавить комментарий

Капча загружается...