5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【2025】.NET Aspireで管理するReact + Vite + ASP.NET Core 【デプロイ】

Posted at

はじめに

この記事は【2025】.NET Aspireで管理するReact + Vite + ASP.NET Core 【ローカル環境】の続きです。

前回の記事で React + Vite + ASP.NET Core を.NET Aspire で管理しながら開発していくローカル環境は整いました。ではデプロイするときはどうしたら良いでしょうか。

最初に考える必要があるのはホスティング環境です。まず大きく二つの選択肢としてWebサーバーに直接配置するのか、コンテナで稼働させるのか方針が2種類あるでしょう。ローカル環境では React(Vite)とASP.NET Coreが完全に別に作成されているため、それを統合する必要があるのかないのかも考慮する必要があります。まとめると次の様になるでしょう。

  1. React(Vite)とASP.NET Coreをそれぞれ別のWebサーバーで稼働させる
  2. React(Vite)とASP.NET Coreをそれぞれのコンテナとして稼働させる
  3. React(Vite)とASP.NET Coreを1つにまとめて .NET Web Applicationとして稼働させる
    1. 1つにまとめてからコンテナとして稼働させる
    2. 1つにまとめてからWebサーバーにデプロイする

1. React(Vite)とASP.NET Coreをそれぞれ別のWebサーバーで稼働させる

React(Vite) と ASP.NET Core を別々の Web ホスティング環境にデプロイするのであれば、特に解説は要らないでしょう。それぞれデプロイすれば良いからです。ただしこの方式の場合、React(Vite)から ASP.NET Core へのアクセス時の URL を相対パスから絶対パスに書き換える必要があるのでvite.config.ts本番用に入れ替える必要があるのと、ASP.NET Core 側は React(Vite)をホスティングする環境からのアクセスを許可するように CORS 設定が必要であることに注意してください。

React(Vite)を Static File だけホスティングする環境(Azure なら Blob ストレージ、AWS なら S3 など)にデプロイし、ASP.NET Coreは別のWebサーバー環境にデプロイする方式も一般的ですが、注意点は同じです。

2. React(Vite)とASP.NET Coreをそれぞれのコンテナとして稼働させる

この方式が良いアーキテクチャかどうかはさておき、わかりやすい方式ではあります。それぞれをコンテナ化しますのでローカルでは Docker-composeなどのオーケストレーションツールで、本番環境では Kubernetes や各社クラウドのPaaSマネージドコンテナホスティングサービスで稼働させることになります。

まずは frontend の React(Vite) をコンテナ化するために Dockerfile を追加します。ReactViteAspNetCore.Frontend フォルダ直下に次のように作成します。

ReactViteAspNetCore.Frontend\Dockerfile
FROM node:20 AS build

WORKDIR /app

COPY package.json package.json
COPY package-lock.json package-lock.json

RUN npm install

COPY . .

RUN npm run build

FROM nginx:alpine

COPY --from=build /app/default.conf.template /etc/nginx/templates/default.conf.template
COPY --from=build /app/dist /usr/share/nginx/html

# Expose the default nginx port
EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

React(Vite)は Nginx で稼働させる設定です。Docker 版の Nginx 設定のためのファイル default.conf.template を作成しておく必要があります。

ReactViteAspNetCore.Frontend\defalut.conf.template
server {
    listen       ${VITE_PORT};
    listen  [::]:${VITE_PORT};
    server_name  localhost;

    access_log  /var/log/nginx/server.access.log  main;

    location / {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass ${services__backend__https__0};
        proxy_http_version 1.1;
        proxy_ssl_server_name on;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        rewrite ^/api(/.*)$ $1 break;
    }
}

この Nginx の設定で重要なのはlocation /api/ {で始まっている proxy 設定です。vite.config.tsで設定した proxy 設定と同じにしてあります。

ではReactViteAspNetCore.Frontendにて次のコマンドで Docker Image を作成します。

docker build --force-rm -t reactviteaspnetcore-frontend:latest .

docker build コマンドで作成するイメージはデプロイ先によって --platform オプションを付与する必要があります。

docker build --force-rm -t reactviteaspnetcore-frontend:latest --platform linux/amd64 .

次に backend の ASP.NET Core をコンテナ化します。Dockerfile を作成する場所はプロジェクトのルート、ReactViteAspNetCoreフォルダにします。backend プロジェクトはログ・監視用の ServiceDefaults プロジェクトを参照しているため、backend プロジェクトをビルドするためには ReactViteAspNetCore.BackendフォルダだけではなくReactViteAspNetCore.ServiceDefaultsフォルダも Docker ビルド時に必要です。

ReactViteAspNetCore\Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /app

# Copy everything
COPY ReactViteAspNetCore.Backend/ ./ReactViteAspNetCore.Backend/
COPY ReactViteAspNetCore.ServiceDefaults/ ./ReactViteAspNetCore.ServiceDefaults/

WORKDIR /app/ReactViteAspNetCore.Backend
RUN dotnet restore
RUN dotnet publish -o out

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:9.0

# 成果物をコピー
COPY --from=build /app/ReactViteAspNetCore.Backend/out .

ENTRYPOINT ["dotnet", "ReactViteAspNetCore.Backend.dll"]

Docker イメージを作成します。

docker build --force-rm -t reactviteaspnetcore-backend:latest .

これで frontend と backend もコンテナができました。二つをリレーションさせてローカルで実行させるために docker-compose.yaml ファイルを作成します。

ReactViteAspNetCore\docker-compose.yaml
services:
  docker-compose-artifacts-dashboard:
    image: "mcr.microsoft.com/dotnet/nightly/aspire-dashboard:latest"
    ports:
      - "18888"
      - "18889"
    networks:
      - "aspire"
    restart: "always"
  backend:
    image: "reactviteaspnetcore-backend:latest"
    environment:
      OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES: "true"
      OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES: "true"
      OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY: "in_memory"
      ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true"
      HTTP_PORTS: "8080"
      OTEL_EXPORTER_OTLP_ENDPOINT: "http://docker-compose-artifacts-dashboard:18889"
      OTEL_EXPORTER_OTLP_PROTOCOL: "grpc"
      OTEL_SERVICE_NAME: "backend"
    ports:
      - "8080"
    networks:
      - "aspire"
  frontend:
    image: "reactviteaspnetcore-frontend:latest"
    environment:
      NODE_ENV: "production"
      VITE_PORT: "8000"
      services__backend__http__0: "http://backend:8080"
      services__backend__https__0: "http://backend:8080"
      OTEL_EXPORTER_OTLP_ENDPOINT: "http://docker-compose-artifacts-dashboard:18889"
      OTEL_EXPORTER_OTLP_PROTOCOL: "grpc"
      OTEL_SERVICE_NAME: "frontend"
    ports:
      - "8000"
    depends_on:
      backend:
        condition: "service_started"
    networks:
      - "aspire"
networks:
  aspire:
    driver: "bridge"

せっかくなので .NET Aspire ダッシュボードも稼働させる設定にしてあります。では docker-composeを稼働させます。

docker compose up

無事に動いたことを確認したら、あとはこの二つのイメージを Container Registry に Push して、本番環境へデプロイします。デプロイしたあと、必要な環境変数をセットアップする必要があることにご注意ください。環境変数は上記 docker-compose.yamlenvironmentをご確認ください。React(Vite)用の環境変数は frontend の箇所を、ASP.NET Core用の環境変数は backendです。また environmentのうち、OTEL_で始まっている変数名は OpentTelemetry 用なので必須ではありません。

3. React(Vite)とASP.NET Coreを1つにまとめて .NET Web Applicationとして稼働させる

React(Vite)だけのコンテナをわざわざ用意するのはちょっと微妙、という場合も多いでしょう。その時は React(Vite)をビルドした結果を ASP.NET Coreの方に静的ファイルとして配置することで対処します。

1つにまとめる前に

Web サーバーに対して直接ビルド結果をデプロイする場合について構成します。これはReact(Vite)のビルド結果を ASP.NET Coreにまとめることを意味しています。React(Vite)のビルド結果は静的ファイルですから、それをASP.NET Coreでホスティングするだけのことです。

その前にローカルでもデプロイ先でも動作するよう、 backend 側のエンドポイントを全て/api配下に変更して frontend の設定もそれに合わせます。なぜこの修正が必要なのか少し解説します。

これまではローカル開発環境では次の流れで URL は扱っていました。

frotnend( /api/ weatherforcast ) -> Viteサーバー Proxy( /weatherforecast ) -> backend

frontendである React(Vite)で backend へのアクセス URL が増えるたびに vite.config.tsに proxy 設定を増やさなければならないのが嫌だったので、 backend へアクセスする時は必ず先頭に /api を付与することにして、実際のアクセスをする前に Proxy で/api を無理やり削除するように vite.config.tsに rewrite 設定を入れていました。

ReactViteAspNetCore\vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: process.env.services__backend__https__0 || process.env.services__backend__http__0,
        changeOrigin: true,
        secure: false,
        rewrite: (path) => path.replace(/^\/api/, ''),
      }
    }
  },
})

今回、ローカルでの使い勝手はそのままに、本番環境では1つにまとめても動作するようにしなければなりません。そのためには次の様に変更が必要です。

frotnend( /api/ weatherforcast ) -> Viteサーバー Proxy( /api/ /weatherforecast ) -> backend

どちらも頭に/apiがついていれば良いわけです。ごく普通の形式にするだけです。では修正しましょう。

まずは backend からです。backend への修正では、ついでに React(Vite)のビルド結果である静的ファイルをホスティングする設定も追加します。

ReactViteAspNetCore.Backend\Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

builder.AddServiceDefaults();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

+ app.UseFileServer();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

+ var apiGroup = app.MapGroup("/api");

- app.MapGet("/weatherforecast", () =>
+ apiGroup.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast");

app.Run();

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

app.UseFileServer();によって静的ファイルのホスティングがwwwrootに対して設定されて、既定のドキュメントとしてindex.htmlが呼び出されます。

app.UseFileServer() を使う代わりに次の様に実装することもできます。

app.UseDefaultFiles();
app.UseStaticFiles();

この場合は実装の順番が大事です。UseStaticFiles()より先にUseDefaultFiles()を呼び出す必要があります。

/weathreforecast で公開していたエンドポイントを /api/weathreforecastに変更するため、MapGroupを使っています。Backend の修正はここまでです。

次に vite.config.tsを修正して、/apiを削除していた rewite 設定を削除します。

ReactViteAspNetCore.Frontend\vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: process.env.services__backend__https__0 || process.env.services__backend__http__0,
        changeOrigin: true,
        secure: false,
-       rewrite: (path) => path.replace(/^\/api/, ''),
      }
    }
  },
})

修正はここまでです。React(Vite)を手動でビルドします。ReactViteAspNetCore.Frontendディレクトリで次のコマンドを実行します。

npm run build

ルート直下にdistディレクトリが作成されます。このdist中身だけReactViteAspNetCore.Backendの中のwwwrootディレクトリにコピーします。
もちろんwwwrootディレクトリは自分で作成します。

稼働確認をしてみましょう。ReactViteAspNetCore.AppHostディレクトリでdotnet run 起動して .NET Aspire ダッシュボードから frontend を確認しても、ReactViteAspNetCore.Backendディレクトリでdotnet run起動してから確認しても、エラーなくデータは取得できているはずです。

手動コピーをしたくない

これで React(Vite)のビルド結果を ASP.NET Core の wwwroot ディレクトリにコピーすれば問題なく動作します。しかしリリースのたびに手動でnpm run buildの上でdistフォルダの中をコピーするのは事故の元です。3つの方法をご紹介します。

MSBuild 時にコピーする

1つは.csprojにカスタムタスクを追加する方法です。まずnpm run buildを実行して次にdistフォルダの中身をwwwrootへコピーする、つまり先ほどの手動操作を自動化しただけです。

ReactViteAspNetCore.Backend\ReactViteAspNetCore.Backend.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\ReactViteAspNetCore.ServiceDefaults\ReactViteAspNetCore.ServiceDefaults.csproj" />
  </ItemGroup>

+   <!-- Frontend を npm run build する-->
+   <Target Name="BuildFrontend" AfterTargets="Build;Publish" Condition=" '$(Configuration)' == 'Release' ">
+     <Message Importance="high" Text="Building frontend..." />
+     <Exec WorkingDirectory="..\ReactViteAspNetCore.Frontend" Command="npm install" />
+     <Exec WorkingDirectory="..\ReactViteAspNetCore.Frontend" Command="npm run build" />
+   </Target>

+   <!-- ReleaseBuild or Publish時:出力ディレクトリのwwwrootにコピー -->
+   <Target Name="CopyFrontendToPublish" AfterTargets="BuildFrontend" Condition="'$(Configuration)' == 'Release'">
+     <Message Importance="high" Text="CopyFrontendToPublish: Copying frontend build output to publish directory..." />
+     <PropertyGroup>
+       <FrontendDir>..\ReactViteAspNetCore.Frontend\dist\</FrontendDir>
+       <BackendWwwRoot>$(PublishDir)\wwwroot\</BackendWwwRoot>
+     </PropertyGroup>
+     <ItemGroup>
+       <FrontendFiles Include="$(FrontendDir)**\*.*" />
+     </ItemGroup>
+     <MakeDir Directories="$(BackendWwwRoot)" Condition="!Exists('$(BackendWwwRoot)')" />
+     <Message Importance="high" Text="Copy to $(BackendWwwRoot)..." />
+     <Copy SourceFiles="@(FrontendFiles)" DestinationFolder="$(BackendWwwRoot)%(RecursiveDir)" />
+   </Target>
</Project>

dotnet publishを実行するとbin/Release/net9.0/publishに発行結果が格納されます。そこにnpm run buildでビルドした結果(distフォルダ)をwwwrootフォルダにコピーしているだけです。

これで React(Vite)のビルド結果も ASP.NET Core側にまとまるので、Webサーバーに直接デプロイあるいはASP.NET Coreプロジェクトをコンテナ化してデプロイ、どちらも可能になっています。

CI/CD ツールで1つにまとめる

プロジェクトファイルにカスタムタスクを追加するのはちょっとハードルが高く、メンテナンスが不安になるかもしれません(難易度は高くありませんよ!)。もう一つの方法は Jenkins や GitHub Actions などの CI ツールを使用することでしょう。
GitHub Actionの場合はこんな感じになります。

deploy.yml
name: Build React and ASP.Net Core app

env:
    DOTNET_VERSION: "9" # set this to the .NET Core version to use

on:
    push:
        branches: ["main"]
    workflow_dispatch:

permissions:
    contents: read

jobs:
    build-donet:
        runs-on: ubuntu-latest

        steps:
            - uses: actions/checkout@v5

            - name: Set up .NET Core
              uses: actions/setup-dotnet@v5
              with:
                  dotnet-version: ${{ env.DOTNET_VERSION }}

            - name: Set up dependency caching for faster builds
              uses: actions/cache@v4
              with:
                  path: ~/.nuget/packages
                  key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
                  restore-keys: |
                      ${{ runner.os }}-nuget-

            - name: Build with dotnet
              run: cd ReactViteAspNetCore.Backend &&
                  dotnet build --configuration Release

            - name: dotnet publish
              run: cd ReactViteAspNetCore.Backend &&
                  dotnet publish -c Release -o ${{env.DOTNET_ROOT}}/myapp
            - name: Use Node.js Ver.20
              uses: actions/setup-node@v4
              with:
                  node-version: 20.x
                  cache: "npm"
                  cache-dependency-path: ReactViteAspNetCore.Frontend/package-lock.json
            - run: cd ReactViteAspNetCore.Frontend && npm ci
            - run: cd ReactViteAspNetCore.Frontend && npm run build --if-present
            - name: Copy build output to backend wwwroot
              run: cd ReactViteAspNetCore.Frontend && cp -a dist/. ${{env.DOTNET_ROOT}}/myapp/wwwroot/

            - name: Upload artifact for deployment job
              uses: actions/upload-artifact@v4
              with:
                  name: .net-app
                  path: ${{env.DOTNET_ROOT}}/myapp

    deploy:
        permissions:
            contents: none
        runs-on: ubuntu-latest
        needs: build-donet
        environment:
            name: "Development"
            url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}

        steps:
            - name: Download artifact from build job
              uses: actions/download-artifact@v5
              with:
                  name: .net-app

・・・

最後の deploy ジョブでは Artifact ダウンロードまでしか実装してませんので、この直後にお好きな場所へデプロイする処理を追加、あるいはコンテナ化する処理を追加の上でさらにデプロイ処理を実装することになるでしょう。
例えばコンテナ化せずに直接 Azure WebAppsへデプロイするならこんな感じのものを追加します。

deploy.yml
            - name: Deploy to Azure Web App
              id: deploy-to-webapp
              uses: azure/webapps-deploy@v2
              with:
                  app-name: YOUR_APP_NAME
                  publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
                  package: .

AZURE_WEBAPP_PUBLISH_PROFILE は Repository 側にシークレットとして事前に登録しておく必要があります。発行プロファイルの取得は Azure Portalで作成した WebApp を開いた概要ページの上段にある「発行プロファイルのダウンロード」リンクから行ってください。

GitHub シークレットを構成する

もし発行プロファイルのダウンロード」リンクが無効になっている場合、基本認証が無効になっています。左リンクの「構成」をクリックして、「SCM の発行資格情報」を ON にしてください。

Dockerfile でイメージ化するときにまとめる

アプリケーションを1つのコンテナで稼働させるのであれば、Dockerfileのマルチステージビルドを使って npm run builddotnet publishそれぞれで実行した結果をまとめるのが最も簡単でしょう。つまり「2. React(Vite)とASP.NET Coreをそれぞれのコンテナとして稼働させる」でご紹介した二つのDockerfileを一つにまとめます。

Dockerfile は ルート直下に作成します。今回は Dockerfile 名を変えたかったのでOneImage.Dockerfileとしました。

ReactViteAspNetCore\OneImage.Dockerfile
# Build frontend
FROM node:20-alpine AS frontend-build
WORKDIR /app

COPY ReactViteAspNetCore.Frontend/package*.json ./
RUN npm install
COPY ReactViteAspNetCore.Frontend/ ./
RUN npm run build

# Build backend
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS backend-build
WORKDIR /app

# Copy backend and service defaults
COPY ReactViteAspNetCore.Backend/ ./ReactViteAspNetCore.Backend/
COPY ReactViteAspNetCore.ServiceDefaults/ ./ReactViteAspNetCore.ServiceDefaults/

WORKDIR /app/ReactViteAspNetCore.Backend
RUN dotnet restore
RUN dotnet publish -o out

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:9.0

# 成果物をコピー
COPY --from=frontend-build /app/dist/ ./wwwroot/

# 成果物をコピー
COPY --from=backend-build /app/ReactViteAspNetCore.Backend/out .

ENTRYPOINT ["dotnet", "ReactViteAspNetCore.Backend.dll"]

Dockerfileの書き方に慣れている人であれば、別に難しいところはないでしょう。次のコマンドでビルドし、実行してみます。

docker build --force-rm -t reactviteaspnetcore:latest -f OneImage.Dockerfile  .
docker run --rm -p 8080:8080 reactviteaspnetcore:latest

http://localhost:8080 にアクセスすれば、問題なく画面表示されてデータも取得できるはずです。

最後に

ホスティング要件によって幾つかの選択肢があることをご紹介しました。もしかしたら今時はこういう情報を見なくても AI がほとんど解決してくれるのかもしれません。でも AI が生成した結果が正しいかどうかを判断するのは人間ですので、めげずに情報発信していきたいものですね。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?