はじめに
この記事は【2025】.NET Aspireで管理するReact + Vite + ASP.NET Core 【ローカル環境】の続きです。
前回の記事で React + Vite + ASP.NET Core を.NET Aspire で管理しながら開発していくローカル環境は整いました。ではデプロイするときはどうしたら良いでしょうか。
最初に考える必要があるのはホスティング環境です。まず大きく二つの選択肢としてWebサーバーに直接配置するのか、コンテナで稼働させるのか方針が2種類あるでしょう。ローカル環境では React(Vite)とASP.NET Coreが完全に別に作成されているため、それを統合する必要があるのかないのかも考慮する必要があります。まとめると次の様になるでしょう。
- React(Vite)とASP.NET Coreをそれぞれ別のWebサーバーで稼働させる
- React(Vite)とASP.NET Coreをそれぞれのコンテナとして稼働させる
- React(Vite)とASP.NET Coreを1つにまとめて .NET Web Applicationとして稼働させる
- 1つにまとめてからコンテナとして稼働させる
- 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
フォルダ直下に次のように作成します。
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
を作成しておく必要があります。
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 ビルド時に必要です。
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 ファイルを作成します。
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.yaml
の environment
をご確認ください。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 設定を入れていました。
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)のビルド結果である静的ファイルをホスティングする設定も追加します。
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 設定を削除します。
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
へコピーする、つまり先ほどの手動操作を自動化しただけです。
<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の場合はこんな感じになります。
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へデプロイするならこんな感じのものを追加します。
- 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 を開いた概要ページの上段にある「発行プロファイルのダウンロード」リンクから行ってください。
もし発行プロファイルのダウンロード」リンクが無効になっている場合、基本認証が無効になっています。左リンクの「構成」をクリックして、「SCM の発行資格情報」を ON にしてください。
Dockerfile でイメージ化するときにまとめる
アプリケーションを1つのコンテナで稼働させるのであれば、Dockerfileのマルチステージビルドを使って npm run build
と dotnet publish
それぞれで実行した結果をまとめるのが最も簡単でしょう。つまり「2. React(Vite)とASP.NET Coreをそれぞれのコンテナとして稼働させる」でご紹介した二つのDockerfileを一つにまとめます。
Dockerfile は ルート直下に作成します。今回は Dockerfile 名を変えたかったので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 が生成した結果が正しいかどうかを判断するのは人間ですので、めげずに情報発信していきたいものですね。