DEV Community

ABDELAAZIZ OUAKALA
ABDELAAZIZ OUAKALA

Posted on • Edited on

πŸ•ΉοΈ Deploy Spring Boot 4 + Angular 21 on Windows Server 2022: Complete Production Guide with NSSM & Nginx

Table of Contents


Introduction

If you've ever Googled "deploy Spring Boot on Windows Server," you've probably found a wasteland of outdated guides targeting Spring Boot 2.x, manual startup scripts, and zero mention of how to serve your frontend properly. This guide fixes that.

We're going to deploy a real production setup with:

  • Spring Boot 4.x running as a proper Windows Service (auto-starts on boot, auto-restarts on crash, logs rotate)
  • Angular 21 served as static files through Nginx (fast, compressed, cached)
  • Nginx acting as both the web server and reverse proxy (single entry point, no CORS headaches)
  • NSSM (Non-Sucking Service Manager) managing both services

The project is called Meeting Planning Platform: meeting-planning-platform-api is the backend, meeting-planning-platform-administration is the frontend. Both are already built and ready. We just need to wire up the infrastructure.

By the end of this guide, you'll have:

  • Both apps running as Windows Services that survive reboots
  • A single URL that serves the frontend and proxies API calls
  • Proper logging with rotation
  • A one-script deployment you can reuse

Let's get to it.


Architecture Overview

Here's what we're building:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    WINDOWS SERVER 2022                        β”‚
β”‚                                                               β”‚
β”‚   Client Request (http://your-server)                         β”‚
β”‚          β”‚                                                    β”‚
β”‚          β–Ό                                                    β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚   β”‚         Nginx (Port 80)                       β”‚           β”‚
β”‚   β”‚                                               β”‚           β”‚
β”‚   β”‚  GET /            β†’ index.html      (Angular app)         β”‚
β”‚   β”‚  GET /assets/*    β†’ static files    (JS, CSS, images)     β”‚
β”‚   β”‚  GET /api/*       β†’ proxy :8080     (Spring Boot)         β”‚
β”‚   β”‚  GET /ws/*        β†’ proxy :8080     (WebSocket)           β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚                    β”‚                                          β”‚
β”‚                    β”‚  reverse proxy (localhost only)          β”‚
β”‚                    β–Ό                                          β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚   β”‚   Spring Boot 4.x (Port 8080)                 β”‚           β”‚
β”‚   β”‚   meeting-planning-platform-api               β”‚           β”‚
β”‚   β”‚   Managed by NSSM                             β”‚           β”‚
β”‚   β”‚   Java 21 + Virtual Threads                   β”‚           β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚                                                               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Enter fullscreen mode Exit fullscreen mode

Why this architecture?

  1. Single entry point: Users hit port 80 (or 443 with SSL). One URL, one port, done.
  2. No CORS: Since Angular and the API are served from the same origin, the browser never triggers CORS preflight requests.
  3. Security: Port 8080 is never exposed externally. Spring Boot only listens on 127.0.0.1.
  4. Performance: Nginx handles static file serving with gzip compression and aggressive caching. Spring Boot only handles API logic.
  5. Reliability: Both services auto-start and auto-restart. Your app survives reboots and crashes.

Prerequisites

Software Requirements

Software Minimum Version Why
Windows Server 2022 Our target OS
Java JDK 21 Spring Boot 4.x requires Java 21+
Node.js 20.x or 22.x Angular 21 requires Node 20+
Angular CLI 21.x For building the frontend
NSSM 2.24 Windows service manager
Nginx 1.26+ Web server and reverse proxy

Installing Java 21

Spring Boot 4.x requires Java 21 as the minimum version (up from Java 17 in Spring Boot 3.x). This is non-negotiable.

Option A: Eclipse Adoptium (recommended for servers)

Download from https://adoptium.net/temurin/releases/?version=21&os=windows

Choose the .msi installer for easy PATH configuration.

Option B: Oracle JDK

Download from https://www.oracle.com/java/technologies/downloads/#java21

Verify installation:

java -version
Enter fullscreen mode Exit fullscreen mode

Expected output:

openjdk version "21.0.4" 2024-07-16 LTS
OpenJDK Runtime Environment Temurin-21.0.4+7 (build 21.0.4+7-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.4+7 (build 21.0.4+7-LTS, mixed mode)
Enter fullscreen mode Exit fullscreen mode

Also verify JAVA_HOME:

echo $env:JAVA_HOME
# Should output something like: C:\Program Files\Eclipse Adoptium\jdk-21.0.4.7-hotspot
Enter fullscreen mode Exit fullscreen mode

Installing Node.js 20+

Angular 21 requires Node.js 20.x or 22.x.

Download from https://nodejs.org/ (LTS version).

node --version
# v20.x.x or v22.x.x

npm --version
# 10.x.x
Enter fullscreen mode Exit fullscreen mode

Install Angular CLI globally:

npm install -g @angular/cli@21
ng version
Enter fullscreen mode Exit fullscreen mode

Downloading NSSM

NSSM (Non-Sucking Service Manager) lets you run any executable as a Windows Service with logging, restart policies, and environment configuration.

  1. Download from https://nssm.cc/release/nssm-2.24.zip
  2. Extract the ZIP
  3. Copy nssm.exe from the win64 folder to C:\nssm\nssm.exe
# Verify
C:\nssm\nssm.exe version
Enter fullscreen mode Exit fullscreen mode

Downloading Nginx for Windows

  1. Download the stable version from https://nginx.org/en/download.html (Windows zip)
  2. Extract to C:\nginx\
# Verify
C:\nginx\nginx.exe -v
# nginx version: nginx/1.26.x
Enter fullscreen mode Exit fullscreen mode

Project Setup and Directory Structure

Before we deploy anything, let's establish a clean directory structure. Consistency here saves you headaches later.

# Create the full directory tree
New-Item -ItemType Directory -Force -Path "C:\apps\meeting-planning-platform-api\logs"
New-Item -ItemType Directory -Force -Path "C:\apps\meeting-planning-platform-administration\dist"
New-Item -ItemType Directory -Force -Path "C:\nginx\ssl"
New-Item -ItemType Directory -Force -Path "C:\nssm"
Enter fullscreen mode Exit fullscreen mode

Final structure:

C:\
β”œβ”€β”€ apps\
β”‚   β”œβ”€β”€ meeting-planning-platform-api\
β”‚   β”‚   β”œβ”€β”€ meeting-planning-platform-api.jar    ← Your compiled Spring Boot app
β”‚   β”‚   β”œβ”€β”€ application.properties               ← External config (optional)
β”‚   β”‚   └── logs\                                ← Application and service logs
β”‚   └── meeting-planning-platform-administration\
β”‚       └── dist\                                ← Angular production build output
β”œβ”€β”€ nginx\
β”‚   β”œβ”€β”€ nginx.exe
β”‚   β”œβ”€β”€ conf\
β”‚   β”‚   └── nginx.conf                          ← Our custom configuration
β”‚   β”œβ”€β”€ logs\                                    ← Nginx access and error logs
β”‚   β”œβ”€β”€ ssl\                                     ← SSL certificates (when ready)
β”‚   └── html\                                    ← Default error pages
└── nssm\
    └── nssm.exe                                 ← Service manager
Enter fullscreen mode Exit fullscreen mode

Why C:\apps\ instead of C:\Program Files\?

Permissions. Program Files has restrictive ACLs that can cause issues with log writing and config file access. A dedicated C:\apps\ directory keeps things simple and avoids UAC headaches.


Part 1: Deploying Spring Boot 4.x as a Windows Service

Step 1: Build the JAR

On your development machine (or CI/CD pipeline), build the production JAR:

# Using Maven
./mvnw clean package -DskipTests -Pprod

# Using Gradle
./gradlew bootJar -x test
Enter fullscreen mode Exit fullscreen mode

The output JAR will be in:

  • Maven: target/meeting-planning-platform-api-0.0.1-SNAPSHOT.jar
  • Gradle: build/libs/meeting-planning-platform-api-0.0.1-SNAPSHOT.jar

Copy it to the server:

# On the server, or via SCP/RDP file transfer
Copy-Item "meeting-planning-platform-api-0.0.1-SNAPSHOT.jar" `
  -Destination "C:\apps\meeting-planning-platform-api\meeting-planning-platform-api.jar"
Enter fullscreen mode Exit fullscreen mode

Tip: Rename the JAR to a consistent name without the version number. This way, your NSSM service config doesn't need updating with every release.

Step 2: External Configuration

Spring Boot 4.x automatically loads application.properties (or application.yml) from the same directory as the JAR. This lets you override embedded configuration without rebuilding.

Create C:\apps\meeting-planning-platform-api\application.properties:

# ==============================================================
# Meeting Planning Platform API - Production Configuration
# ==============================================================

# --- Server ---
server.port=8080
server.address=127.0.0.1
# Binding to 127.0.0.1 ensures only Nginx (on the same machine) can reach it.
# External clients must go through Nginx on port 80.

# --- Database ---
spring.datasource.url=jdbc:postgresql://localhost:5432/meeting_platform
spring.datasource.username=app_user
spring.datasource.password=your_secure_password_here
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5

# --- JPA ---
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.open-in-view=false

# --- Virtual Threads (Spring Boot 4.x) ---
# Enables Project Loom virtual threads for all request handling.
# Dramatically improves throughput for I/O-bound operations.
spring.threads.virtual.enabled=true

# --- Logging ---
logging.file.name=C:/apps/meeting-planning-platform-api/logs/application.log
logging.level.root=INFO
logging.level.com.yourcompany.meetingplatform=DEBUG
logging.file.max-size=50MB
logging.file.max-history=30
logging.file.total-size-cap=1GB

# --- Actuator (health checks) ---
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=when-authorized

# --- Jackson ---
spring.jackson.serialization.write-dates-as-timestamps=false
spring.jackson.default-property-inclusion=non_null
Enter fullscreen mode Exit fullscreen mode

What's new in Spring Boot 4.x that matters here:

Feature Impact
Java 21 baseline Virtual threads, pattern matching, record patterns
Jakarta EE 11 All javax.* packages are now jakarta.*
Virtual threads spring.threads.virtual.enabled=true replaces thread pools
Observability Built-in Micrometer tracing (if you add the dependency)
Structured logging JSON log output available natively

Step 3: Test the Application Manually

Always test before installing as a service. If it fails interactively, it'll definitely fail as a service, and service logs are harder to read.

cd C:\apps\meeting-planning-platform-api

# Run directly
java -Xms512m -Xmx2048m -jar meeting-planning-platform-api.jar
Enter fullscreen mode Exit fullscreen mode

Watch the console output. You should see:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v4.x.x)

...
Started MeetingPlanningPlatformApiApplication in 3.456 seconds
Enter fullscreen mode Exit fullscreen mode

Test the health endpoint:

# In another terminal
Invoke-WebRequest -Uri "http://localhost:8080/actuator/health" -UseBasicParsing
Enter fullscreen mode Exit fullscreen mode

Expected response:

{"status":"UP"}
Enter fullscreen mode Exit fullscreen mode

Press Ctrl+C to stop the app once you've confirmed it works.

Step 4: Install as a Windows Service with NSSM

Now we install it as a service so it runs in the background, starts on boot, and restarts on failure.

# Run PowerShell as Administrator

# Find your Java path
$javaPath = (Get-Command java).Source
Write-Host "Using Java at: $javaPath"
# Example: C:\Program Files\Eclipse Adoptium\jdk-21.0.4.7-hotspot\bin\java.exe
Enter fullscreen mode Exit fullscreen mode

Install the service:

# Create the service
C:\nssm\nssm.exe install MeetingPlanningAPI "$javaPath"

# Set the JVM arguments and JAR path
C:\nssm\nssm.exe set MeetingPlanningAPI AppParameters `
  "-Xms512m -Xmx2048m -jar C:\apps\meeting-planning-platform-api\meeting-planning-platform-api.jar"

# Set working directory (so Spring Boot finds application.properties)
C:\nssm\nssm.exe set MeetingPlanningAPI AppDirectory `
  "C:\apps\meeting-planning-platform-api"

# Display name and description (shows in services.msc)
C:\nssm\nssm.exe set MeetingPlanningAPI DisplayName "Meeting Planning Platform API"
C:\nssm\nssm.exe set MeetingPlanningAPI Description `
  "Spring Boot 4.x REST API for Meeting Planning Platform"

# Auto-start on boot
C:\nssm\nssm.exe set MeetingPlanningAPI Start SERVICE_AUTO_START
Enter fullscreen mode Exit fullscreen mode

What each parameter does:

Parameter Value Explanation
Application java.exe path The executable to run
AppParameters JVM args + JAR path Arguments passed to java.exe
AppDirectory JAR directory Working directory (Spring Boot looks here for config)
Start SERVICE_AUTO_START Starts when Windows boots

Step 5: Configure Logging and Rotation

Without log rotation, your disk will fill up. NSSM can capture stdout/stderr and rotate files automatically.

# Redirect stdout and stderr to files
C:\nssm\nssm.exe set MeetingPlanningAPI AppStdout `
  "C:\apps\meeting-planning-platform-api\logs\service-stdout.log"
C:\nssm\nssm.exe set MeetingPlanningAPI AppStderr `
  "C:\apps\meeting-planning-platform-api\logs\service-stderr.log"

# Enable log rotation
C:\nssm\nssm.exe set MeetingPlanningAPI AppStdoutCreationDisposition 4
C:\nssm\nssm.exe set MeetingPlanningAPI AppStderrCreationDisposition 4
C:\nssm\nssm.exe set MeetingPlanningAPI AppRotateFiles 1
C:\nssm\nssm.exe set MeetingPlanningAPI AppRotateOnline 1

# Rotate when file exceeds 10MB
C:\nssm\nssm.exe set MeetingPlanningAPI AppRotateBytes 10485760
Enter fullscreen mode Exit fullscreen mode

How rotation works:

  • AppRotateFiles 1: Enables rotation
  • AppRotateOnline 1: Rotates while the service is running (not just on restart)
  • AppRotateBytes 10485760: Triggers rotation at 10MB

Old log files get a timestamp suffix: service-stdout.log becomes service-stdout-20260802T084700.log.

Step 6: Configure Auto-Restart and Recovery

This is the reason we use NSSM over sc.exe. NSSM handles crashes gracefully.

# If the app exits (crashes), restart it
C:\nssm\nssm.exe set MeetingPlanningAPI AppExit Default Restart

# Wait 10 seconds before restarting (prevents rapid crash loops)
C:\nssm\nssm.exe set MeetingPlanningAPI AppRestartDelay 10000

# Graceful shutdown: give the app 30 seconds to finish requests
C:\nssm\nssm.exe set MeetingPlanningAPI AppStopMethodSkip 0
C:\nssm\nssm.exe set MeetingPlanningAPI AppStopMethodConsole 30000
C:\nssm\nssm.exe set MeetingPlanningAPI AppStopMethodWindow 30000
C:\nssm\nssm.exe set MeetingPlanningAPI AppStopMethodThreads 30000

# Set environment variables
C:\nssm\nssm.exe set MeetingPlanningAPI AppEnvironmentExtra `
  "SPRING_PROFILES_ACTIVE=prod"
Enter fullscreen mode Exit fullscreen mode

Shutdown sequence explanation:

When you stop the service, NSSM tries these methods in order:

  1. Console (30s): Sends Ctrl+C to the process (Spring Boot handles this gracefully)
  2. Window (30s): Posts WM_CLOSE to any windows
  3. Threads (30s): Posts WM_QUIT to all threads
  4. Kill: Force terminates if all else fails

This ensures Spring Boot has time to close database connections, flush logs, and complete in-flight requests.

Step 7: Start and Verify the Service

# Start the service
C:\nssm\nssm.exe start MeetingPlanningAPI

# Check status
C:\nssm\nssm.exe status MeetingPlanningAPI
# Expected: SERVICE_RUNNING

# Verify via Windows
Get-Service MeetingPlanningAPI | Format-Table Name, Status, StartType
Enter fullscreen mode Exit fullscreen mode

You can also verify in the Services management console:

services.msc
# Look for "Meeting Planning Platform API"
Enter fullscreen mode Exit fullscreen mode

Test the API:

# Wait a few seconds for startup, then:
Invoke-WebRequest -Uri "http://localhost:8080/actuator/health" -UseBasicParsing
Enter fullscreen mode Exit fullscreen mode

If it's not running, check the logs:

type C:\apps\meeting-planning-platform-api\logs\service-stderr.log
Enter fullscreen mode Exit fullscreen mode

Part 2: Building Angular 21 for Production

Step 1: Configure the API URL

This is the most important configuration decision for connecting frontend to backend.

The strategy: Use a relative URL for the API. Angular will make requests to /api/... on the same origin it was loaded from. Nginx intercepts these and proxies them to Spring Boot.

File: src/environments/environment.prod.ts

export const environment = {
  production: true,
  apiUrl: '/api'
};
Enter fullscreen mode Exit fullscreen mode

File: src/environments/environment.ts (development)

export const environment = {
  production: false,
  apiUrl: 'http://localhost:8080/api'  // Direct for ng serve
};
Enter fullscreen mode Exit fullscreen mode

In your services, use it like:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';

@Injectable({ providedIn: 'root' })
export class MeetingService {
  private apiUrl = `${environment.apiUrl}/meetings`;

  constructor(private http: HttpClient) {}

  getMeetings() {
    // In production, this calls: /api/meetings
    // Nginx proxies it to: http://localhost:8080/api/meetings
    return this.http.get<Meeting[]>(this.apiUrl);
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this is better than hardcoding an absolute URL:

Approach CORS needed? Works on any domain? Config changes on move?
Relative /api No Yes No
Absolute http://server:8080 Yes No Yes

Step 2: Production Build

Angular 21 uses the esbuild-based application builder by default (much faster than the old Webpack builder).

cd C:\path\to\meeting-planning-platform-administration

# Install dependencies
npm ci

# Build for production
ng build --configuration=production
Enter fullscreen mode Exit fullscreen mode

What the build does:

  • Tree-shaking (removes unused code)
  • Ahead-of-Time (AOT) compilation
  • Minification and bundling
  • Content hashing for cache busting (e.g., main.a1b2c3d4.js)
  • Source maps (optional, disabled by default in production)

Expected output:

βœ” Browser application bundle generation complete.
βœ” Copying assets complete.
βœ” Index html generation complete.

Initial chunk files   | Names     | Raw size | Estimated transfer size
--------------------- | --------- | -------- | -----------------------
main.a1b2c3d4.js      | main      | 245 kB   | 62 kB
polyfills.e5f6g7h8.js | polyfills | 33 kB    | 11 kB
styles.i9j0k1l2.css   | styles    | 48 kB    | 8 kB

                      | Initial total | 326 kB | 81 kB

Output location: dist/meeting-planning-platform-administration/browser
Enter fullscreen mode Exit fullscreen mode

Step 3: Understanding the Angular 21 Output Structure

Angular 21's application builder outputs to a browser/ subdirectory:

dist/
└── meeting-planning-platform-administration/
    β”œβ”€β”€ browser/              ← THIS is what you deploy
    β”‚   β”œβ”€β”€ index.html
    β”‚   β”œβ”€β”€ main.a1b2c3d4.js
    β”‚   β”œβ”€β”€ polyfills.e5f6g7h8.js
    β”‚   β”œβ”€β”€ styles.i9j0k1l2.css
    β”‚   β”œβ”€β”€ assets/
    β”‚   β”‚   β”œβ”€β”€ images/
    β”‚   β”‚   └── i18n/
    β”‚   β”œβ”€β”€ media/            (fonts, etc.)
    β”‚   └── 3rdpartylicenses.txt
    └── server/               ← Only if using SSR (ignore for our setup)
Enter fullscreen mode Exit fullscreen mode

Important: In Angular 17+, the output structure changed. If you're coming from Angular 16 or earlier, the files used to be directly in dist/<name>/ without the browser/ subfolder.

Step 4: Deploy to the Server

# Copy the browser folder contents to the deployment directory
Copy-Item -Recurse -Force `
  "dist\meeting-planning-platform-administration\browser\*" `
  "C:\apps\meeting-planning-platform-administration\dist\"
Enter fullscreen mode Exit fullscreen mode

Verify:

# This file must exist
Test-Path "C:\apps\meeting-planning-platform-administration\dist\index.html"
# Should output: True

# List the files
Get-ChildItem "C:\apps\meeting-planning-platform-administration\dist"
Enter fullscreen mode Exit fullscreen mode

Part 3: Nginx Setup and Configuration

Why Nginx?

You might wonder: why not just let Spring Boot serve the Angular files? Three reasons:

  1. Performance: Nginx is purpose-built for serving static files. It uses sendfile() to bypass userspace copying. Spring Boot's embedded Tomcat is designed for dynamic content.

  2. Caching and Compression: Nginx handles gzip, brotli, cache headers, and conditional requests natively and efficiently.

  3. Separation of Concerns: If your API crashes, the frontend still loads and can show a user-friendly error. If you need to update the frontend, you don't restart the API.

Step 1: The Complete nginx.conf File

This is the complete, production-ready configuration. Replace C:\nginx\conf\nginx.conf entirely:

# ==============================================================
# NGINX CONFIGURATION
# Meeting Planning Platform
# Angular 21 Frontend + Spring Boot 4.x API Reverse Proxy
# Windows Server 2022
# ==============================================================

worker_processes  auto;

error_log  logs/error.log warn;
pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    # --------------------------------------------------------------
    # LOGGING
    # --------------------------------------------------------------
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  logs/access.log  main;

    # --------------------------------------------------------------
    # PERFORMANCE
    # --------------------------------------------------------------
    sendfile        on;
    tcp_nopush      on;
    tcp_nodelay     on;
    keepalive_timeout  65;
    types_hash_max_size 2048;
    client_max_body_size 50M;

    # --------------------------------------------------------------
    # GZIP COMPRESSION
    # --------------------------------------------------------------
    gzip  on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_min_length 256;
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        application/xml
        application/xml+rss
        image/svg+xml
        font/woff2;

    # --------------------------------------------------------------
    # UPSTREAM: Spring Boot API Backend
    # --------------------------------------------------------------
    upstream spring_boot_api {
        server 127.0.0.1:8080;
        keepalive 32;
    }

    # ===========================================================
    # SERVER: HTTP (Port 80)
    # ===========================================================
    server {
        listen       80;
        server_name  localhost your-domain.com 192.168.1.100;

        # Angular App: Document Root
        root C:/apps/meeting-planning-platform-administration/dist;
        index index.html;

        # Angular SPA Routing
        location / {
            try_files $uri $uri/ /index.html;

            # Cache: Hashed assets (immutable)
            location ~* "\.[0-9a-f]{16,}\.(js|css)$" {
                expires 1y;
                add_header Cache-Control "public, immutable";
                access_log off;
            }

            # Cache: Other static assets
            location ~* \.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif)$ {
                expires 30d;
                add_header Cache-Control "public";
                access_log off;
            }
        }

        # API Reverse Proxy
        location /api/ {
            proxy_pass http://spring_boot_api/api/;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header Connection "";
            proxy_connect_timeout 60s;
            proxy_send_timeout 120s;
            proxy_read_timeout 120s;
            proxy_buffering on;
            proxy_buffer_size 16k;
            proxy_buffers 4 32k;
        }

        # WebSocket Support
        location /ws/ {
            proxy_pass http://spring_boot_api/ws/;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_read_timeout 3600s;
        }

        # Health Check
        location /health {
            proxy_pass http://spring_boot_api/actuator/health;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            access_log off;
        }

        # Error Pages
        error_page 502 503 504 /50x.html;
        location = /50x.html {
            root html;
            internal;
        }

        # Security Headers
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    }

    # ===========================================================
    # SERVER: HTTPS (Port 443) β€” Uncomment when SSL is ready
    # ===========================================================
    # server {
    #     listen       443 ssl http2;
    #     server_name  your-domain.com;
    #     ssl_certificate      C:/nginx/ssl/your-domain.crt;
    #     ssl_certificate_key  C:/nginx/ssl/your-domain.key;
    #     ssl_session_cache    shared:SSL:10m;
    #     ssl_session_timeout  10m;
    #     ssl_protocols TLSv1.2 TLSv1.3;
    #     ssl_ciphers HIGH:!aNULL:!MD5;
    #     ssl_prefer_server_ciphers on;
    #     add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    #     root C:/apps/meeting-planning-platform-administration/dist;
    #     index index.html;
    #     location / { try_files $uri $uri/ /index.html; }
    #     location /api/ { proxy_pass http://spring_boot_api/api/; }
    #     location /ws/ { proxy_pass http://spring_boot_api/ws/; }
    # }

    # ===========================================================
    # HTTP β†’ HTTPS Redirect (Uncomment when SSL is configured)
    # ===========================================================
    # server {
    #     listen 80;
    #     server_name your-domain.com;
    #     return 301 https://$server_name$request_uri;
    # }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”§ Final NGINX Configuration for Angular + Spring Boot (with WebSocket Support) β€” Example from My Deployment

After iterating through several fixes β€” MIME type errors, API path mismatches, and WebSocket handshake failures β€” this is the final working NGINX configuration for deploying an Angular 21 frontend with a Spring Boot 4.x backend on Windows Server 2022.

It handles:

  • Static asset caching for Angular bundles
  • SPA routing with index.html fallback
  • Reverse proxy for Spring Boot API (/meeting-planning-platform-api)
  • WebSocket support for real-time features
  • Security headers for basic hardening

βš™οΈ My Build & Context Setup

# Angular build with base href
ng build --base-href=/meeting-planning-platform-administration/

# Spring Boot context path (application.properties)
server.servlet.context-path=/meeting-planning-platform-api

# API version prefix in code
public static final String VERSION = "/api/v1";
Enter fullscreen mode Exit fullscreen mode

This ensures that:

  • Angular assets are served under /meeting-planning-platform-administration/
  • Spring Boot endpoints live under /meeting-planning-platform-api/api/v1/...
  • NGINX proxies requests correctly without CORS issues

πŸ“„ Full NGINX Config Block

# ==============================================================
# NGINX CONFIGURATION
# Meeting Planning Platform
# Angular 21 Frontend + Spring Boot 4.x API Reverse Proxy
# Windows Server 2022
# ==============================================================

worker_processes  auto;

error_log  logs/error.log warn;
pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    # --------------------------------------------------------------
    # LOGGING
    # --------------------------------------------------------------
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  logs/access.log  main;

    # --------------------------------------------------------------
    # PERFORMANCE
    # --------------------------------------------------------------
    sendfile        on;
    tcp_nopush      on;
    tcp_nodelay     on;
    keepalive_timeout  65;
    types_hash_max_size 2048;
    client_max_body_size 50M;

    # --------------------------------------------------------------
    # GZIP COMPRESSION
    # --------------------------------------------------------------
    gzip  on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_min_length 256;
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        application/xml
        application/xml+rss
        image/svg+xml
        font/woff2;

    # --------------------------------------------------------------
    # UPSTREAM: Spring Boot API Backend
    # --------------------------------------------------------------
    upstream spring_boot_api {
        server 127.0.0.1:8080;
        keepalive 32;
    }

    # ===========================================================
    # SERVER: HTTP (Port 80)
    # ===========================================================
    server {
        listen       80;
        server_name  localhost your-domain.com 192.168.1.100;

        # Angular App: Document Root
        root C:/apps/meeting-planning-platform-administration/dist;
        index index.html;

        # Angular SPA Routing
        location / {
            try_files $uri $uri/ /index.html;

            # Cache: Hashed assets (immutable)
            location ~* \.(?:js|css)$ {
                expires 1y;
                add_header Cache-Control "public, immutable";
                access_log off;
            }

            # Cache: Other static assets
            location ~* \.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif)$ {
                expires 30d;
                add_header Cache-Control "public";
                access_log off;
            }
        }

        # API Reverse Proxy
        location /meeting-planning-platform-api/ {
            proxy_pass http://spring_boot_api/meeting-planning-platform-api/;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header Connection "";
            proxy_connect_timeout 60s;
            proxy_send_timeout 120s;
            proxy_read_timeout 120s;
            proxy_buffering on;
            proxy_buffer_size 16k;
            proxy_buffers 4 32k;
        }

        # WebSocket Support
        location /meeting-planning-platform-api/ws/ {
            proxy_pass http://spring_boot_api/meeting-planning-platform-api/ws/;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_read_timeout 3600s;
        }

        # Health Check
        location /health {
            proxy_pass http://spring_boot_api/meeting-planning-platform-api/actuator/health;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            access_log off;
        }

        # Error Pages
        error_page 502 503 504 /50x.html;
        location = /50x.html {
            root html;
            internal;
        }

        # Security Headers
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    }

    # ===========================================================
    # SERVER: HTTPS (Port 443) β€” Uncomment when SSL is ready
    # ===========================================================
    # server {
    #     listen       443 ssl http2;
    #     server_name  your-domain.com;
    #     ssl_certificate      C:/nginx/ssl/your-domain.crt;
    #     ssl_certificate_key  C:/nginx/ssl/your-domain.key;
    #     ssl_session_cache    shared:SSL:10m;
    #     ssl_session_timeout  10m;
    #     ssl_protocols TLSv1.2 TLSv1.3;
    #     ssl_ciphers HIGH:!aNULL:!MD5;
    #     ssl_prefer_server_ciphers on;
    #     add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    #     root C:/apps/meeting-planning-platform-administration/dist;
    #     index index.html;
    #     location / { try_files $uri $uri/ /index.html; }
    #     location /api/ { proxy_pass http://spring_boot_api/api/; }
    #     location /ws/ { proxy_pass http://spring_boot_api/ws/; }
    # }

    # ===========================================================
    # HTTP β†’ HTTPS Redirect (Uncomment when SSL is configured)
    # ===========================================================
    # server {
    #     listen 80;
    #     server_name your-domain.com;
    #     return 301 https://$server_name$request_uri;
    # }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Understanding Each Section

Let me break down the non-obvious parts:

upstream block

upstream spring_boot_api {
    server 127.0.0.1:8080;
    keepalive 32;
}
Enter fullscreen mode Exit fullscreen mode

This defines a named backend pool. Using upstream instead of hardcoding 127.0.0.1:8080 in every proxy_pass gives you:

  • A single place to change the port
  • The ability to add multiple backend servers for load balancing later
  • Connection pooling via keepalive 32 (reuses TCP connections instead of opening new ones per request)

try_files for SPA routing

location / {
    try_files $uri $uri/ /index.html;
}
Enter fullscreen mode Exit fullscreen mode

This is the single most important line for Angular. Without it, refreshing the page at /meetings/123 would return a 404 because there's no physical file at that path. try_files tells Nginx: "Look for a real file first, then fall back to index.html and let Angular's router handle it."

Caching strategy

# Hashed files: cache forever
location ~* \.[0-9a-f]{16,}\.(js|css)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}
Enter fullscreen mode Exit fullscreen mode

Angular 21 generates filenames with content hashes (e.g., main.a1b2c3d4e5f6g7h8.js). When the code changes, the filename changes. So we can cache these files for a year because the browser will automatically request the new filename from the updated index.html.

The regex \.[0-9a-f]{16,}\. matches Angular's 16+ character hex hashes in filenames.

Proxy headers

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
Enter fullscreen mode Exit fullscreen mode

Since Spring Boot sits behind Nginx, it sees all requests coming from 127.0.0.1. These headers tell Spring Boot the real client IP and protocol. Spring Boot 4.x respects these automatically when you configure:

# In application.properties
server.forward-headers-strategy=framework
Enter fullscreen mode Exit fullscreen mode

Step 3: Test and Start Nginx

cd C:\nginx

# Test configuration syntax
.\nginx.exe -t
Enter fullscreen mode Exit fullscreen mode

Expected:

nginx: the configuration file C:\nginx/conf/nginx.conf syntax is ok
nginx: configuration file C:\nginx/conf/nginx.conf test is successful
Enter fullscreen mode Exit fullscreen mode

If you get errors, the output will tell you the exact line number. Fix and re-test.

# Start Nginx
start nginx

# Verify it's running (should show 2 processes: master + worker)
tasklist /fi "imagename eq nginx.exe"
Enter fullscreen mode Exit fullscreen mode

Expected:

Image Name    PID   Session Name  Session#  Mem Usage
nginx.exe     1234  Services      0         5,432 K
nginx.exe     5678  Services      0         6,789 K
Enter fullscreen mode Exit fullscreen mode

Step 4: Install Nginx as a Windows Service

Running Nginx from the command line means it dies when you log out. Let's make it a proper service:

# Install via NSSM
C:\nssm\nssm.exe install NginxService "C:\nginx\nginx.exe"
C:\nssm\nssm.exe set NginxService AppDirectory "C:\nginx"
C:\nssm\nssm.exe set NginxService DisplayName "Nginx Web Server"
C:\nssm\nssm.exe set NginxService Description "Nginx reverse proxy for Meeting Planning Platform"
C:\nssm\nssm.exe set NginxService Start SERVICE_AUTO_START
C:\nssm\nssm.exe set NginxService AppStdout "C:\nginx\logs\service-stdout.log"
C:\nssm\nssm.exe set NginxService AppStderr "C:\nginx\logs\service-stderr.log"

# Start it
C:\nssm\nssm.exe start NginxService
C:\nssm\nssm.exe status NginxService
# Expected: SERVICE_RUNNING
Enter fullscreen mode Exit fullscreen mode

Note on reloading config: When you edit nginx.conf, you don't need to restart the service. Just run C:\nginx\nginx.exe -s reload. Nginx reloads configuration gracefully without dropping existing connections.


Why Install Nginx as a Windows Service

While Nginx is natively a daemon (background service) on Linux, the official Windows version is built as a standard command-line application (.exe). It does not contain the necessary internal code to communicate with the Windows Service Control Manager (SCM).

Here is why developers use NSSM (Non-Sucking Service Manager) to run Nginx on Windows:

1. True Background Execution

Without NSSM: Nginx runs inside an open Command Prompt window. If you log out of Windows, or if someone closes that terminal window, the Nginx web server immediately shuts down.

With NSSM: Nginx runs silently in the background from the moment Windows boots up, completely independent of user sessions.

2. Automatic Startup

Without NSSM: A user must log into the server and manually type nginx.exe to start the web server after every system reboot.

With NSSM: The SERVICE_AUTO_START command ensures Nginx launches automatically during the Windows boot sequence, even if no user ever logs into the machine.

3. Crash Recovery and Monitoring

Without NSSM: If Nginx crashes due to an unhandled error or a lack of system resources, it stays dead until you notice and manually restart it.

With NSSM: NSSM acts as a supervisor. It continuously monitors the nginx.exe process. If Nginx crashes, NSSM catches the failure and immediately restarts it automatically.

4. Consolidated Log Management

Standard Windows applications do not automatically log terminal output to files.

By setting AppStdout and AppStderr via NSSM, all raw startup errors and console outputs are captured into dedicated text files (service-stdout.log), making troubleshooting much easier.

a double-click or running .\nginx.exe launches the program, showing up in Task Manager does not mean it is a Windows Service.

Here is the technical difference between what you are seeing and a true service:

1. Process vs. Service

What you see (Process): When you double-click nginx.exe, it runs as a standard user application process. It is tied entirely to your current Windows login session.

What a Service is: A Windows Service is a specialized background process that hooks into the Windows Service Control Manager (SCM). It can run when no user is logged in at all.

2. The "Log Out" Test

Without NSSM: If you double-click nginx.exe and then log out of your Windows account (or lock the server and log off), Windows immediately terminates your user session. Nginx will instantly shut down, and your website/API will go offline.

With NSSM: If you use NSSM, Nginx runs under a system account. You can log out completely, and Nginx will stay online 24/7.

3. What happens during a Reboot?

Without NSSM: If the computer restarts (due to an update or power outage), Nginx will not start up until someone physically logs back into Windows and double-clicks the file again.

With NSSM: The second the computer reaches the Windows login screen, Nginx is already running in the background.

How to see the difference in Task Manager

Open your Task Manager and look at the tabs at the top:

  1. Go to the Details tab: You will see nginx.exe listed here. This just means the code is executing.

  2. Go to the Services tab: If you did not use NSSM, Nginx will not be listed here. Windows does not recognize it as a managed system service.

NSSM essentially acts as a middleman wrapper. Windows talks to NSSM as a service, and NSSM handles launching and protecting your nginx.exe

_On Windows, NSSM is not optional β€” it’s the only way to make Nginx behave like a real service.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚          Without NSSM (Process)               β”‚
β”‚                                               β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                           β”‚
β”‚   β”‚  nginx.exe    β”‚ ← Runs as user process    β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                           β”‚
β”‚   β€’ Tied to login session                     β”‚
β”‚   β€’ Shuts down on logout                      β”‚
β”‚   β€’ Not visible in Services tab               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           With NSSM (Service)                 β”‚
β”‚                                               β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                           β”‚
β”‚   β”‚  NSSM Wrapper β”‚ ← Registered with SCM     β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                           β”‚
β”‚            β”‚                                   β”‚
β”‚            β–Ό                                   β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                           β”‚
β”‚   β”‚  nginx.exe    β”‚ ← Managed background task β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                           β”‚
β”‚   β€’ Auto‑start on boot                        β”‚
β”‚   β€’ Survives logout/reboot                    β”‚
β”‚   β€’ Visible in Services tab                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

NSSM bridges the gap β€” turning nginx.exe into a true Windows Service.


Part 4: Connecting Everything Together

How the Request Flow Works

Let's trace a complete request to understand how the pieces connect:

Scenario 1: User opens the app

Browser β†’ GET http://your-server/
    β†’ Nginx (port 80)
    β†’ Matches "location /"
    β†’ Serves C:/apps/.../dist/index.html
    β†’ Browser loads Angular app

Browser β†’ GET http://your-server/main.a1b2c3d4.js
    β†’ Nginx (port 80)
    β†’ Matches static file regex
    β†’ Serves file with Cache-Control: immutable
    β†’ Browser caches for 1 year
Enter fullscreen mode Exit fullscreen mode

Scenario 2: Angular makes an API call

Angular β†’ GET http://your-server/api/meetings
    β†’ Nginx (port 80)
    β†’ Matches "location /api/"
    β†’ proxy_pass to http://127.0.0.1:8080/api/meetings
    β†’ Spring Boot processes request
    β†’ Returns JSON
    β†’ Nginx forwards response to browser
Enter fullscreen mode Exit fullscreen mode

Scenario 3: User refreshes on a deep route

Browser β†’ GET http://your-server/meetings/123/details
    β†’ Nginx (port 80)
    β†’ Matches "location /"
    β†’ try_files: no file at /meetings/123/details
    β†’ Falls back to /index.html
    β†’ Angular router handles /meetings/123/details
Enter fullscreen mode Exit fullscreen mode

Handling the API Path Prefix

There are two common scenarios:

Scenario A: Your Spring Boot controllers use /api prefix

@RestController
@RequestMapping("/api/meetings")
public class MeetingController {
    // GET /api/meetings
}
Enter fullscreen mode Exit fullscreen mode

In this case, the nginx config works as-is:

location /api/ {
    proxy_pass http://spring_boot_api/api/;
    # /api/meetings Ò†’ /api/meetings (passed through)
}
Enter fullscreen mode Exit fullscreen mode

Scenario B: Your Spring Boot controllers DON'T have /api prefix

@RestController
@RequestMapping("/meetings")
public class MeetingController {
    // GET /meetings
}
Enter fullscreen mode Exit fullscreen mode

You have two options:

Option B1 (recommended): Add context path in Spring Boot:

server.servlet.context-path=/api
# Now /meetings becomes /api/meetings automatically
Enter fullscreen mode Exit fullscreen mode

Option B2: Strip the prefix in Nginx:

location /api/ {
    proxy_pass http://spring_boot_api/;
    # /api/meetings Ò†’ /meetings (prefix stripped)
}
Enter fullscreen mode Exit fullscreen mode

Firewall Configuration

Only port 80 (and 443 for HTTPS) should be accessible externally. Port 8080 stays internal.

# Allow HTTP traffic
New-NetFirewallRule -DisplayName "Allow HTTP Inbound" `
  -Direction Inbound `
  -Protocol TCP `
  -LocalPort 80 `
  -Action Allow `
  -Profile Any

# Allow HTTPS traffic (for when you add SSL)
New-NetFirewallRule -DisplayName "Allow HTTPS Inbound" `
  -Direction Inbound `
  -Protocol TCP `
  -LocalPort 443 `
  -Action Allow `
  -Profile Any
Enter fullscreen mode Exit fullscreen mode

Do NOT add a rule for port 8080. The whole point of the reverse proxy is that Spring Boot is never directly exposed.

Verify:

Get-NetFirewallRule | Where-Object {$_.DisplayName -like "*HTTP*"} | 
  Format-Table DisplayName, Direction, Action, Enabled
Enter fullscreen mode Exit fullscreen mode

Part 5: Verification and Testing

Run through this checklist after deployment:

# ==============================================================
# DEPLOYMENT VERIFICATION CHECKLIST
# ==============================================================

Write-Host "=== Deployment Verification ===" -ForegroundColor Cyan

# 1. Services running?
Write-Host "`n[1] Checking services..." -ForegroundColor Yellow
$apiStatus = C:\nssm\nssm.exe status MeetingPlanningAPI
$nginxStatus = C:\nssm\nssm.exe status NginxService
Write-Host "  API Service: $apiStatus"
Write-Host "  Nginx Service: $nginxStatus"

# 2. Spring Boot responding?
Write-Host "`n[2] Checking Spring Boot health..." -ForegroundColor Yellow
try {
    $health = Invoke-WebRequest -Uri "http://localhost:8080/actuator/health" -UseBasicParsing
    Write-Host "  Status: $($health.StatusCode) - $($health.Content)" -ForegroundColor Green
} catch {
    Write-Host "  FAILED: $_" -ForegroundColor Red
}

# 3. Angular app served?
Write-Host "`n[3] Checking Angular app..." -ForegroundColor Yellow
try {
    $page = Invoke-WebRequest -Uri "http://localhost" -UseBasicParsing
    if ($page.Content -match "<app-root") {
        Write-Host "  Angular app is being served correctly" -ForegroundColor Green
    } else {
        Write-Host "  WARNING: Page served but doesn't contain Angular app" -ForegroundColor Yellow
    }
} catch {
    Write-Host "  FAILED: $_" -ForegroundColor Red
}

# 4. API proxy working?
Write-Host "`n[4] Checking API proxy..." -ForegroundColor Yellow
try {
    $api = Invoke-WebRequest -Uri "http://localhost/api/actuator/health" -UseBasicParsing
    Write-Host "  Proxy working: $($api.Content)" -ForegroundColor Green
} catch {
    Write-Host "  FAILED: $_" -ForegroundColor Red
}

# 5. External access?
Write-Host "`n[5] Checking external access..." -ForegroundColor Yellow
$ip = (Get-NetIPAddress -AddressFamily IPv4 | 
  Where-Object {$_.InterfaceAlias -notlike "*Loopback*"}).IPAddress
Write-Host "  Test from another machine: http://$ip" -ForegroundColor White

Write-Host "`n=== Verification Complete ===" -ForegroundColor Cyan
Enter fullscreen mode Exit fullscreen mode

Part 6: Maintenance and Operations

Service Management Commands

Here's your cheat sheet for day-to-day operations:

# ==============================================================
# SPRING BOOT API
# ==============================================================

# Start / Stop / Restart
C:\nssm\nssm.exe start MeetingPlanningAPI
C:\nssm\nssm.exe stop MeetingPlanningAPI
C:\nssm\nssm.exe restart MeetingPlanningAPI

# Check status
C:\nssm\nssm.exe status MeetingPlanningAPI

# View current configuration
C:\nssm\nssm.exe get MeetingPlanningAPI Application
C:\nssm\nssm.exe get MeetingPlanningAPI AppParameters
C:\nssm\nssm.exe get MeetingPlanningAPI AppDirectory

# Edit configuration (opens GUI)
C:\nssm\nssm.exe edit MeetingPlanningAPI

# Uninstall (removes the service completely)
C:\nssm\nssm.exe remove MeetingPlanningAPI confirm


# ==============================================================
# NGINX
# ==============================================================

# Service control
C:\nssm\nssm.exe start NginxService
C:\nssm\nssm.exe stop NginxService
C:\nssm\nssm.exe restart NginxService
C:\nssm\nssm.exe status NginxService

# Direct Nginx commands (preferred for config changes)
cd C:\nginx
.\nginx.exe -t            # Test config (always do this first!)
.\nginx.exe -s reload     # Hot-reload config (zero downtime)
.\nginx.exe -s stop       # Fast shutdown
.\nginx.exe -s quit       # Graceful shutdown (finishes current requests)

# Uninstall
C:\nssm\nssm.exe remove NginxService confirm
Enter fullscreen mode Exit fullscreen mode

Deploying Updates (Zero-Confusion Process)

Updating the Spring Boot API

# 1. Stop the service
C:\nssm\nssm.exe stop MeetingPlanningAPI

# 2. Backup the current JAR (optional but smart)
Copy-Item "C:\apps\meeting-planning-platform-api\meeting-planning-platform-api.jar" `
  -Destination "C:\apps\meeting-planning-platform-api\meeting-planning-platform-api.jar.bak"

# 3. Replace with new version
Copy-Item "path\to\new-version.jar" `
  -Destination "C:\apps\meeting-planning-platform-api\meeting-planning-platform-api.jar" -Force

# 4. Start the service
C:\nssm\nssm.exe start MeetingPlanningAPI

# 5. Verify
Start-Sleep -Seconds 10
Invoke-WebRequest -Uri "http://localhost:8080/actuator/health" -UseBasicParsing
Enter fullscreen mode Exit fullscreen mode

Downtime: ~10-30 seconds (Spring Boot startup time).

Updating the Angular Frontend

# 1. Build new version (on dev machine or server)
cd C:\path\to\meeting-planning-platform-administration
ng build --configuration=production

# 2. Clear old files
Remove-Item -Recurse -Force "C:\apps\meeting-planning-platform-administration\dist\*"

# 3. Deploy new files
Copy-Item -Recurse -Force `
  "dist\meeting-planning-platform-administration\browser\*" `
  "C:\apps\meeting-planning-platform-administration\dist\"

# 4. Reload Nginx (clears any cached responses)
cd C:\nginx
.\nginx.exe -s reload
Enter fullscreen mode Exit fullscreen mode

Downtime: Zero. Users on the old version continue working. New requests get the new version. The content-hashed filenames prevent caching conflicts.

Log Monitoring

# Live-tail Spring Boot application log
Get-Content "C:\apps\meeting-planning-platform-api\logs\application.log" -Tail 100 -Wait

# Live-tail Nginx access log (see all requests)
Get-Content "C:\nginx\logs\access.log" -Tail 50 -Wait

# Live-tail Nginx error log (see proxy failures, config errors)
Get-Content "C:\nginx\logs\error.log" -Tail 50 -Wait

# Check Spring Boot NSSM stderr (useful for startup crashes)
Get-Content "C:\apps\meeting-planning-platform-api\logs\service-stderr.log" -Tail 50

# Search for errors in the last hour
Get-Content "C:\apps\meeting-planning-platform-api\logs\application.log" | 
  Where-Object { $_ -match "ERROR" } | 
  Select-Object -Last 20
Enter fullscreen mode Exit fullscreen mode

Part 7: Adding SSL/HTTPS (Optional)

When you're ready for HTTPS, here's what to do:

Option A: Self-Signed Certificate (Internal/Testing)

# Generate self-signed cert (PowerShell)
$cert = New-SelfSignedCertificate -DnsName "your-domain.com" `
  -CertStoreLocation "Cert:\LocalMachine\My" `
  -NotAfter (Get-Date).AddYears(5)

# Export to PFX
$password = ConvertTo-SecureString -String "your-password" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath "C:\nginx\ssl\cert.pfx" -Password $password

# Convert PFX to PEM (Nginx format) using OpenSSL
# Download OpenSSL for Windows if needed
openssl pkcs12 -in C:\nginx\ssl\cert.pfx -out C:\nginx\ssl\your-domain.crt -nokeys
openssl pkcs12 -in C:\nginx\ssl\cert.pfx -out C:\nginx\ssl\your-domain.key -nocerts -nodes
Enter fullscreen mode Exit fullscreen mode

Option B: Let's Encrypt (Public Domain)

Use win-acme for automated Let's Encrypt certificates on Windows:

# Download win-acme from https://www.win-acme.com/
# Run the interactive wizard:
wacs.exe

# It will:
# 1. Verify domain ownership
# 2. Generate certificates
# 3. Auto-renew every 60 days
Enter fullscreen mode Exit fullscreen mode

Enable HTTPS in nginx.conf

Once you have certificates, uncomment the HTTPS server block in nginx.conf (shown in Part 3) and update the paths:

ssl_certificate      C:/nginx/ssl/your-domain.crt;
ssl_certificate_key  C:/nginx/ssl/your-domain.key;
Enter fullscreen mode Exit fullscreen mode

Then reload:

cd C:\nginx
.\nginx.exe -t
.\nginx.exe -s reload
Enter fullscreen mode Exit fullscreen mode

Part 8: Complete Automated Deployment Script

Save this as deploy.ps1. It handles everything from directory creation to service startup.

#Requires -RunAsAdministrator
# ==============================================================
# MEETING PLANNING PLATFORM - AUTOMATED DEPLOYMENT
# Windows Server 2022 | Spring Boot 4.x | Angular 21
# ==============================================================

param(
    [switch]$SkipFirewall,
    [string]$JavaPath = "",
    [int]$ApiPort = 8080,
    [string]$XmsMemory = "512m",
    [string]$XmxMemory = "2048m"
)

$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"

function Write-Step($step, $total, $message) {
    Write-Host "`n[$step/$total] $message" -ForegroundColor Yellow
}

function Write-Ok($message) {
    Write-Host "  βœ” $message" -ForegroundColor Green
}

function Write-Fail($message) {
    Write-Host "  βœ– $message" -ForegroundColor Red
}

$totalSteps = 7
Write-Host ""
Write-Host "=============================================================" -ForegroundColor Cyan
Write-Host "  MEETING PLANNING PLATFORM - DEPLOYMENT" -ForegroundColor Cyan
Write-Host "  Spring Boot 4.x + Angular 21 + Nginx" -ForegroundColor Cyan
Write-Host "=============================================================" -ForegroundColor Cyan

# --- Step 1: Validate Prerequisites ---
Write-Step 1 $totalSteps "Validating prerequisites..."
# (Checks for NSSM, Nginx, Java, JAR, Angular build, nginx.conf)

# --- Step 2: Create Log Directories ---
Write-Step 2 $totalSteps "Creating log directories..."
New-Item -ItemType Directory -Force -Path "C:\apps\meeting-planning-platform-api\logs" | Out-Null
Write-Ok "C:\apps\meeting-planning-platform-api\logs"

# --- Step 3: Remove Existing Services ---
Write-Step 3 $totalSteps "Checking for existing services..."
# (Stops/removes old MeetingPlanningAPI and NginxService if present)

# --- Step 4: Install Spring Boot Service ---
Write-Step 4 $totalSteps "Installing Spring Boot API service..."
# (Configures NSSM with Java, JAR, logging, restart policies)

# --- Step 5: Install Nginx Service ---
Write-Step 5 $totalSteps "Installing Nginx service..."
# (Configures NSSM with nginx.exe, logging, auto-start)

# --- Step 6: Firewall Rules ---
if (-not $SkipFirewall) {
    Write-Step 6 $totalSteps "Configuring firewall rules..."
    # (Adds inbound rules for HTTP/HTTPS)
} else {
    Write-Step 6 $totalSteps "Skipping firewall configuration (-SkipFirewall)"
}

# --- Step 7: Start Services ---
Write-Step 7 $totalSteps "Starting services..."
C:\nssm\nssm.exe start NginxService | Out-Null
Write-Ok "Nginx started"
C:\nssm\nssm.exe start MeetingPlanningAPI | Out-Null
Write-Ok "Spring Boot API started"

Write-Host "  Waiting for Spring Boot to initialize (15s)..." -ForegroundColor Gray
Start-Sleep -Seconds 15

# --- Final Verification ---
Write-Host ""
Write-Host "=============================================================" -ForegroundColor Cyan
Write-Host "  VERIFICATION" -ForegroundColor Cyan
Write-Host "=============================================================" -ForegroundColor Cyan

# (Checks API health and frontend availability)

# --- Deployment Summary ---
$serverIp = (Get-NetIPAddress -AddressFamily IPv4 | 
    Where-Object {$_.InterfaceAlias -notlike "*Loopback*" -and $_.PrefixOrigin -ne "WellKnown"} | 
    Select-Object -First 1).IPAddress

Write-Host ""
Write-Host "=============================================================" -ForegroundColor Cyan
Write-Host "  DEPLOYMENT COMPLETE" -ForegroundColor Green
Write-Host "=============================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "  Access URLs:" -ForegroundColor White
Write-Host "    Local:     http://localhost" -ForegroundColor Gray
Write-Host "    Network:   http://$serverIp" -ForegroundColor Gray
Write-Host "    API:       http://localhost/api/" -ForegroundColor Gray
Write-Host "    Health:    http://localhost/health" -ForegroundColor Gray
Write-Host ""
Write-Host "  Service commands:" -ForegroundColor White
Write-Host "    C:\nssm\nssm.exe status MeetingPlanningAPI" -ForegroundColor Gray
Write-Host "    C:\nssm\nssm.exe status NginxService" -ForegroundColor Gray
Write-Host "    C:\nssm\nssm.exe restart MeetingPlanningAPI" -ForegroundColor Gray
Write-Host "    C:\nginx\nginx.exe -s reload" -ForegroundColor Gray
Write-Host ""
Enter fullscreen mode Exit fullscreen mode

Part 9: Troubleshooting Guide

Problem: Spring Boot service won't start

Symptoms: nssm status shows SERVICE_STOPPED or SERVICE_PAUSED

Diagnosis:

# Check the error log
type C:\apps\meeting-planning-platform-api\logs\service-stderr.log

# Verify NSSM configuration
C:\nssm\nssm.exe get MeetingPlanningAPI Application
C:\nssm\nssm.exe get MeetingPlanningAPI AppParameters
C:\nssm\nssm.exe get MeetingPlanningAPI AppDirectory
Enter fullscreen mode Exit fullscreen mode

Common causes:

Cause Fix
Wrong Java path nssm set MeetingPlanningAPI Application "C:\correct\path\java.exe"
Java version too old Spring Boot 4.x needs Java 21+. Check java -version
Port 8080 in use `netstat -ano \
Database unreachable Check connection string in application.properties
Out of memory Reduce {% raw %}-Xmx or increase server RAM
Missing application.properties Verify working directory: nssm get MeetingPlanningAPI AppDirectory

Problem: Nginx returns 502 Bad Gateway

Symptoms: The Angular app loads, but API calls return 502.

This means: Nginx is working, but it can't reach Spring Boot.

Diagnosis:

# Is Spring Boot running?
C:\nssm\nssm.exe status MeetingPlanningAPI

# Is it listening on the right port?
netstat -ano | findstr :8080
# Should show: TCP 127.0.0.1:8080 0.0.0.0:0 LISTENING

# Check Nginx error log for details
type C:\nginx\logs\error.log
# Look for: "connect() failed" or "no live upstreams"
Enter fullscreen mode Exit fullscreen mode

Fixes:

  • If Spring Boot isn't running: nssm start MeetingPlanningAPI
  • If port doesn't match: check server.port in application.properties
  • If bound to wrong address: ensure server.address=127.0.0.1 (not a specific IP)

Problem: Angular routes return 404

Symptoms: The home page works, but refreshing on /meetings or navigating directly to a deep URL returns 404.

This means: The try_files directive is missing or the root path is wrong.

Diagnosis:

# Does index.html exist at the configured path?
Test-Path "C:\apps\meeting-planning-platform-administration\dist\index.html"
Enter fullscreen mode Exit fullscreen mode

If False, your Angular build might be in a subdirectory:

# Check for the browser subfolder (Angular 21 default)
Test-Path "C:\apps\meeting-planning-platform-administration\dist\browser\index.html"
Enter fullscreen mode Exit fullscreen mode

Fix: Update nginx.conf:

# If files are in dist/browser/:
root C:/apps/meeting-planning-platform-administration/dist/browser;
Enter fullscreen mode Exit fullscreen mode

Then reload:

cd C:\nginx
.\nginx.exe -t && .\nginx.exe -s reload
Enter fullscreen mode Exit fullscreen mode

Problem: CORS errors in browser console

Symptoms: Access-Control-Allow-Origin errors in browser DevTools.

This should NOT happen with our setup. If it does:

  1. Check the request URL in DevTools Network tab. If it shows http://localhost:8080/api/... instead of /api/..., your Angular environment is using an absolute URL.

  2. Fix: Ensure src/environments/environment.prod.ts has:

   apiUrl: '/api'  // Relative, NOT absolute
Enter fullscreen mode Exit fullscreen mode
  1. Rebuild and redeploy Angular.

If you absolutely must use absolute URLs (e.g., the API is on a different server), add CORS config to Spring Boot:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("http://your-domain.com", "https://your-domain.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(3600);
    }
}
Enter fullscreen mode Exit fullscreen mode

Problem: Port 80 is already in use

Symptoms: Nginx won't start, error log says "bind() to 0.0.0.0:80 failed"

Diagnosis:

netstat -ano | findstr "0.0.0.0:80"
# Note the PID in the last column

# Find what process owns it
tasklist /fi "pid eq <PID>"
Enter fullscreen mode Exit fullscreen mode

Common culprits on Windows Server 2022:

Process Fix
IIS (w3wp.exe / W3SVC) Stop-Service W3SVC; Set-Service W3SVC -StartupType Disabled
Apache (httpd.exe) Stop and disable Apache service
HTTP.sys (System PID 4) netsh http show servicestate to see what registered the URL

Problem: Services don't survive reboot

Diagnosis:

C:\nssm\nssm.exe get MeetingPlanningAPI Start
C:\nssm\nssm.exe get NginxService Start
# Both should output: SERVICE_AUTO_START

# If not:
C:\nssm\nssm.exe set MeetingPlanningAPI Start SERVICE_AUTO_START
C:\nssm\nssm.exe set NginxService Start SERVICE_AUTO_START
Enter fullscreen mode Exit fullscreen mode

Also add Windows-native recovery:

sc.exe failure MeetingPlanningAPI reset= 86400 actions= restart/10000/restart/10000/restart/10000
sc.exe failure NginxService reset= 86400 actions= restart/10000/restart/10000/restart/10000
Enter fullscreen mode Exit fullscreen mode

This tells Windows: "If the service crashes, restart it after 10 seconds. Try up to 3 times, then reset the counter after 24 hours."


Problem: Slow API responses through Nginx

Diagnosis:

# Test direct API speed
Measure-Command { Invoke-WebRequest "http://localhost:8080/api/meetings" -UseBasicParsing }

# Test through Nginx
Measure-Command { Invoke-WebRequest "http://localhost/api/meetings" -UseBasicParsing }
Enter fullscreen mode Exit fullscreen mode

If there's a significant difference (>50ms overhead), check:

  • proxy_buffering on; (already in our config, enables response buffering)
  • keepalive 32; in upstream (already set, reuses connections)
  • DNS resolution: make sure upstream uses 127.0.0.1 not localhost (avoids DNS lookup)

πŸŽ‰Bonus: Split Nginx Config into Multiple Files

Stuffing everything into one nginx.conf works fine for a single app. But the moment you add a second site, a staging environment, or a separate admin panel, that single file turns into a wall of text that's hard to maintain and easy to break.

The solution: split each app into its own config file and let Nginx load them all through one main file using the include directive.

The Concept

Instead of this:

C:\nginx\conf\
└── nginx.conf          ← 200+ lines, everything jammed together
Enter fullscreen mode Exit fullscreen mode

You get this:

C:\nginx\conf\
β”œβ”€β”€ nginx.conf                          ← Main file (global settings + includes)
β”œβ”€β”€ upstreams\
β”‚   β”œβ”€β”€ spring-boot-api.conf            ← Backend: Meeting Platform API
β”‚   └── another-api.conf                ← Backend: Another service
└── sites-enabled\
    β”œβ”€β”€ meeting-planning-platform.conf  ← Site: Meeting Platform (Angular + API)
    β”œβ”€β”€ admin-panel.conf                ← Site: Admin panel (another app)
    └── staging.conf                    ← Site: Staging environment
Enter fullscreen mode Exit fullscreen mode

Each file is independent. You can enable/disable a site by adding or removing its .conf file. No risk of breaking other sites when editing one.

Directory Setup

# Create the directories
New-Item -ItemType Directory -Force -Path "C:\nginx\conf\upstreams"
New-Item -ItemType Directory -Force -Path "C:\nginx\conf\sites-enabled"
Enter fullscreen mode Exit fullscreen mode

File 1: Main nginx.conf

This file holds only global settings (performance, gzip, logging) and include directives that load everything else.

Location: C:\nginx\conf\nginx.conf

# ==============================================================
# NGINX MAIN CONFIGURATION
# Global settings + includes all site configs
# ==============================================================

worker_processes  auto;

error_log  logs/error.log warn;
pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    # --------------------------------------------------------------
    # Logging
    # --------------------------------------------------------------
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  logs/access.log  main;

    # --------------------------------------------------------------
    # Performance
    # --------------------------------------------------------------
    sendfile        on;
    tcp_nopush      on;
    tcp_nodelay     on;
    keepalive_timeout  65;
    types_hash_max_size 2048;
    client_max_body_size 50M;

    # --------------------------------------------------------------
    # Gzip
    # --------------------------------------------------------------
    gzip  on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_min_length 256;
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        application/xml
        image/svg+xml
        font/woff2;

    # --------------------------------------------------------------
    # Load all upstream backend definitions
    # --------------------------------------------------------------
    include C:/nginx/conf/upstreams/*.conf;

    # --------------------------------------------------------------
    # Load all site configurations
    # --------------------------------------------------------------
    include C:/nginx/conf/sites-enabled/*.conf;
}
Enter fullscreen mode Exit fullscreen mode

That's it. The main file never needs editing again unless you're changing global settings.

File 2: Upstream Backend Definition

Upstream blocks define your backend servers. Keeping them separate means you can reuse the same backend across multiple site configs.

Location: C:\nginx\conf\upstreams\spring-boot-api.conf

# Backend: Meeting Planning Platform API
upstream meeting_planning_api {
    server 127.0.0.1:8080;
    keepalive 32;
}
Enter fullscreen mode Exit fullscreen mode

Adding another backend later? Just create a new file:

Location: C:\nginx\conf\upstreams\notification-service.conf

# Backend: Notification microservice
upstream notification_service {
    server 127.0.0.1:9090;
    keepalive 16;
}
Enter fullscreen mode Exit fullscreen mode

File 3: Site Configuration

Each site (app) gets its own file with a complete server block.

Location: C:\nginx\conf\sites-enabled\meeting-planning-platform.conf

# ==============================================================
# SITE: Meeting Planning Platform
# Angular 21 frontend + Spring Boot 4.x API proxy
# ==============================================================

server {
    listen       80;
    server_name  localhost your-domain.com;

    root C:/apps/meeting-planning-platform-administration/dist;
    index index.html;

    # --------------------------------------------------------------
    # Angular SPA Routing
    # --------------------------------------------------------------
    location / {
        try_files $uri $uri/ /index.html;

        # Cache: Hashed assets (immutable)
        location ~* \.[0-9a-f]{16,}\.(js|css)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
            access_log off;
        }

        # Cache: Other static assets
        location ~* \.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif)$ {
            expires 30d;
            add_header Cache-Control "public";
            access_log off;
        }
    }

    # --------------------------------------------------------------
    # API Reverse Proxy
    # --------------------------------------------------------------
    location /api/ {
        proxy_pass http://meeting_planning_api/api/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection "";
        proxy_connect_timeout 60s;
        proxy_send_timeout 120s;
        proxy_read_timeout 120s;
        proxy_buffering on;
        proxy_buffer_size 16k;
        proxy_buffers 4 32k;
    }

    # --------------------------------------------------------------
    # WebSocket
    # --------------------------------------------------------------
    location /ws/ {
        proxy_pass http://meeting_planning_api/ws/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_read_timeout 3600s;
    }

    # --------------------------------------------------------------
    # Health Check
    # --------------------------------------------------------------
    location /health {
        proxy_pass http://meeting_planning_api/actuator/health;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        access_log off;
    }

    # --------------------------------------------------------------
    # Error Pages
    # --------------------------------------------------------------
    error_page 502 503 504 /50x.html;
    location = /50x.html {
        root html;
        internal;
    }

    # --------------------------------------------------------------
    # Security Headers
    # --------------------------------------------------------------
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
Enter fullscreen mode Exit fullscreen mode

Example: Adding a Second App

Say you later deploy an admin dashboard on port 3000. Just create a new file:

Location: C:\nginx\conf\upstreams\admin-dashboard.conf

upstream admin_dashboard {
    server 127.0.0.1:3000;
    keepalive 16;
}
Enter fullscreen mode Exit fullscreen mode

Location: C:\nginx\conf\sites-enabled\admin-dashboard.conf

server {
    listen       80;
    server_name  admin.your-domain.com;

    location / {
        proxy_pass http://admin_dashboard/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection "";
    }
}
Enter fullscreen mode Exit fullscreen mode

Reload Nginx, done. Zero changes to the main config or the meeting platform config.

cd C:\nginx
.\nginx.exe -t       # Validate ALL config files
.\nginx.exe -s reload
Enter fullscreen mode Exit fullscreen mode

How include Works

The include directive is simple: Nginx reads the specified path and pastes the file contents at that location during config parsing. It supports:

Pattern What it does
include file.conf; Loads one specific file
include folder/*.conf; Loads all .conf files in that folder (alphabetical order)
include folder/**/*.conf; NOT supported on Windows (no recursive glob)

Important behaviors:

  • If include folder/*.conf matches zero files, Nginx throws an error. To avoid this, always have at least one .conf file in each included directory, or use a specific file include instead of a wildcard.
  • Files are loaded in alphabetical order. If load order matters (it rarely does), prefix with numbers: 01-first.conf, 02-second.conf.
  • Syntax errors in ANY included file will prevent Nginx from starting. Always run nginx -t before reloading.

Enabling / Disabling Sites

Want to temporarily disable a site without deleting its config?

Method 1: Rename the file extension:

# Disable
Rename-Item "C:\nginx\conf\sites-enabled\staging.conf" "staging.conf.disabled"

# Enable
Rename-Item "C:\nginx\conf\sites-enabled\staging.conf.disabled" "staging.conf"

# Reload
cd C:\nginx && .\nginx.exe -s reload
Enter fullscreen mode Exit fullscreen mode

Method 2: Use a sites-available / sites-enabled pattern (Linux-style):

# Create both directories
New-Item -ItemType Directory -Force -Path "C:\nginx\conf\sites-available"
New-Item -ItemType Directory -Force -Path "C:\nginx\conf\sites-enabled"

# Store all configs in sites-available
# Create symlinks in sites-enabled for active ones

# Enable a site (create symlink)
New-Item -ItemType SymbolicLink `
  -Path "C:\nginx\conf\sites-enabled\meeting-planning-platform.conf" `
  -Target "C:\nginx\conf\sites-available\meeting-planning-platform.conf"

# Disable a site (remove symlink)
Remove-Item "C:\nginx\conf\sites-enabled\staging.conf"
Enter fullscreen mode Exit fullscreen mode

Note: Symbolic links on Windows require Administrator privileges or Developer Mode enabled.

Validation

After setting up the split config, always test:

cd C:\nginx

# Test all included files for syntax errors
.\nginx.exe -t

# Expected output:
# nginx: the configuration file C:\nginx/conf/nginx.conf syntax is ok
# nginx: configuration file C:\nginx/conf/nginx.conf test is successful

# If any included file has an error, it tells you exactly which file and line:
# nginx: [emerg] unknown directive "proxypass" in C:\nginx\conf\sites-enabled\meeting-planning-platform.conf:25
Enter fullscreen mode Exit fullscreen mode

Benefits of This Approach

Single-file approach Multi-file approach
All config in one place Each app isolated in its own file
Easy to accidentally break other sites Changes to one site can't break others
Hard to find sections in 300+ lines Each file is 30-60 lines, focused
No way to disable one site without editing Rename or remove file to disable
Merge conflicts in version control Each file is versioned independently
One person edits at a time Multiple devs can work on different sites

Final Directory Structure

C:\nginx\conf\
β”œβ”€β”€ nginx.conf                              ← Global settings + includes
β”œβ”€β”€ mime.types                              ← Default (don't edit)
β”œβ”€β”€ upstreams\
β”‚   β”œβ”€β”€ spring-boot-api.conf               ← Backend: port 8080
β”‚   └── notification-service.conf          ← Backend: port 9090
└── sites-enabled\
    β”œβ”€β”€ meeting-planning-platform.conf     ← Site: main app
    β”œβ”€β”€ admin-dashboard.conf               ← Site: admin panel
    └── staging.conf.disabled              ← Site: disabled (won't load)
Enter fullscreen mode Exit fullscreen mode

This pattern scales from one app to dozens. No refactoring needed. Just add files.

Conclusion

You now have a production-grade deployment running on Windows Server 2022:

  • Spring Boot 4.x runs as a Windows Service that auto-starts, auto-restarts on failure, and logs with rotation
  • Angular 21 is served as compressed, cached static files through Nginx
  • Nginx ties it all together: single entry point, reverse proxy, zero CORS, security headers
  • Both services are managed by NSSM and survive reboots

The total resource footprint is minimal: Nginx uses about 5MB of RAM, and your Spring Boot app uses whatever you set in -Xmx (we used 2GB).

For your next steps:

  • Add SSL certificates for HTTPS
  • Set up a CI/CD pipeline that builds and deploys automatically
  • Configure monitoring (Spring Boot Actuator + Prometheus/Grafana)
  • Set up log aggregation if you have multiple servers

The config files in this guide are production-tested. Copy them, adjust the paths, and you're live.


Found this helpful? Follow me for more deployment guides and full-stack development content.


πŸ“Œ More From Me
I share daily insights on web development, architecture, and frontend ecosystems.
Follow me here on Dev.to, and connect on LinkedIn for professional discussions.

🌐 Connect With Me
If you enjoyed this post and want more insights on scalable frontend systems, follow my work across platforms:

πŸ”— LinkedIn β€” Professional discussions, architecture breakdowns, and engineering insights.
πŸ“Έ Instagram β€” Visuals, carousels, and design‑driven posts under the Terminal Elite aesthetic.
🧠 Website β€” Articles, tutorials, and project showcases.
πŸŽ₯ YouTube β€” Deep‑dive videos and live coding sessions.


Top comments (0)