Deploy Next.js 16 with PNPM on Linux Azure App Service
- Rory Wade
- Nov 4
- 10 min read
Next.js 15 has introduced significant improvements in performance, developer experience, and deployment flexibility. When combined with PNPM's efficient dependency management and Azure App Service's robust hosting platform, you have a powerful stack for building and deploying modern web applications. However, deploying Next.js to Azure App Service isn't the most intuitive developer experience when you deploy via the default options.
In this three part guide, you'll learn how to:
Set up a new Next.js version 13, 14, 15 or 16 project using PNPM
Configure Next.js for standalone output mode (essential for Azure)
Implement PNPM package hoisting for Azure App Service compatibility
Build and validate your application for deployment
Optimise your project structure for production
This is Part 1 of a three-part series on deploying Next.js 16 to Azure App Service. In Part 2, we'll cover setting up Azure DevOps CI/CD pipelines, and in Part 3, we'll explore the Azure Infrastructure configuration and monitoring.
Why PNPM for Next.js 15/16 on Azure Web App?
Before diving into the setup, it's worth understanding why PNPM is an excellent choice for Next.js applications.
PNPM's Key Advantages:
Disk Space Efficiency: PNPM uses a content-addressable store, meaning packages are stored once globally and hard-linked into projects. This can reduce disk usage by up to 50% compared to npm.
Faster Installations: By leveraging hard links and a efficient dependency resolution algorithm, PNPM typically installs dependencies faster than npm or Yarn.
Strict Dependency Resolution: PNPM creates a non-flat node_modules structure by default, preventing phantom dependencies (packages you didn't explicitly install but can somehow import).
Better Monorepo Support: If you're building multiple Next.js applications or have a monorepo structure, PNPM's workspace features is a real nice to have.
Azure-Specific Benefits:
Smaller build artifacts when combined with Next.js standalone mode
Faster CI/CD pipeline builds due to efficient caching
Reduced memory footprint during build processes
That said, PNPM's strict dependency isolation can require additional configuration for Azure App Service, which we'll address in this guide. Also note that this guide will be useful for other package managers.
Creating a New Next.js 16 Project with PNPM
With PNPM installed, let's create a new Next.js 15 project. The process is similar to using npx, but we'll use PNPM's equivalent command.
Create your Next.js 15 application:
pnpm create next-app@latest my-nextjs-azure-app
You'll be prompted with several configuration options. Here are the recommended settings for an Azure deployment:

Why these choices?
TypeScript: Provides type safety and better developer experience. Highly recommended for production applications.
ESLint: Helps catch common errors and enforces code quality standards.
Tailwind CSS: Efficient styling solution that doesn't impact runtime bundle size.
src/ directory: Better project organisation, separating application code from configuration files.
App Router: Next.js 13+ recommended routing system with better performance and features.
Turbopack: Faster development builds (though this only affects local development, not Azure deployment).
Navigate into your project:
cd my-nextjs-azure-app
Project Structure Overview:
After creation, your project structure should look like this:
my-nextjs-azure-app/
├── public/ # Static assets (images, fonts, etc.)
├── src/
│ └── app/ # App Router directory
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx # Root layout component
│ └── page.tsx # Home page component
├── .eslintrc.json # ESLint configuration
├── .gitignore
├── next.config.ts # Next.js configuration
├── package.json # Project dependencies and scripts
├── pnpm-lock.yaml # PNPM lock file (like package-lock.json)
├── postcss.config.mjs # PostCSS configuration for Tailwind
├── tailwind.config.ts # Tailwind CSS configuration
└── tsconfig.json # TypeScript configuration
Important files for Azure deployment:
next.config.ts: Where we'll configure standalone output mode
package.json: Contains build scripts and dependencies
pnpm-lock.yaml: Ensures deterministic builds in CI/CD pipelines
Verify the installation works:
pnpm dev
Open your browser to http://localhost:3000. You should see the default Next.js welcome page. Press Ctrl+C to stop the development server.
Configuring Next.js for Standalone Output Mode
This is the most critical configuration for Azure App Service deployment. By default, Next.js
builds includes the entire node_modules directory or must be installed manually on the deployed server, resulting in large deployment artifacts or slow deployments (which can take hours on the lowest Linux Azure App Service).
Standalone output mode creates a minimal, self-contained build that includes only the necessary dependencies.
What is Standalone Output Mode?
Standalone output mode is a Next.js feature that:
Traces all dependencies required for production
Copies only the necessary files to .next/standalone
Dramatically reduces deployment size (often by 80% or more)
Includes a minimal server.js file to run the application
Excludes development dependencies and unused packages
Why is it essential for Azure?
Azure App Service has specific characteristics that make standalone mode beneficial:
Faster deployment times with smaller artifacts
Reduced storage costs in Azure DevOps artifact storage
Lower memory footprint during application startup
Better cold start performance on App Service plans with auto-scaling
Configuring standalone output:
Open next.config.ts (or next.config.js if you chose JavaScript) and modify it as follows:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: 'standalone',
};
export default nextConfig;
Understanding the standalone build output:
When you build with output: 'standalone', Next.js creates this structure:
.next/
├── standalone/ # Minimal production build
│ ├── .next/ # Next.js build artifacts
│ ├── node_modules/ # Only production dependencies
│ ├── public/ # Static files (copied automatically)
│ ├── package.json
│ └── server.js # Entry point for running the app
├── static/ # Static assets with cache headers
└── ... # Other build artifacts
Important considerations:
Static files location: The public folder and .next/static files are automatically copied into the standalone output. However, you'll need to ensure these are properly deployed to Azure (covered in Part 2).
Server.js entry point: The generated server.js file is what you'll run on Azure App Service. It's a minimal Node.js server that serves your Next.js application.
Environment variables: Standalone mode respects environment variables. You can set these in Azure App Service configuration (covered in Part 3).
Additional next.config.ts optimisations for Azure:
While we're configuring Next.js, let's add some production-ready settings:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: 'standalone',
// Disable x-powered-by header for security
poweredByHeader: false,
// Compression is handled by Azure, disable Next.js compression
compress: false,
// Enable React strict mode for better error catching in development
reactStrictMode: true,
// Optimise images
images: {
formats: ['image/avif', 'image/webp'],
// If using Azure CDN or Blob Storage for images:
// remotePatterns: [
// {
// protocol: 'https',
// hostname: 'yourstorage.blob.core.windows.net',
// },
// ],
},
// Set security headers (also covered in Part 3)
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin'
},
],
},
];
},
};
export default nextConfig;
Note on compression: Azure App Service handles Gzip/Brotli compression at the IIS/Nginx level, so it's more efficient to disable Next.js compression to avoid double-compression overhead.
Configuring PNPM Package Hoisting for Azure Compatibility
By default, PNPM uses a content-addressable storage system that creates a nested node_modules structure. This prevents phantom dependencies but can cause issues with some Node.js applications expecting a flat structure. Azure App Service and certain Node.js runtime behaviours can be affected by this strict isolation. In Addition, Windows 11 move and copy operations copy the symlinks directly not the destination files making it hard to build your final artifact.
Understanding the issue:
When Next.js builds in standalone mode, it traces dependencies and copies them to .next/standalone/node_modules. However, with PNPM's default strict mode, some dependencies might not resolve correctly at runtime on Azure if they expect to access peer dependencies or transitive dependencies.
The solution: Package hoisting
We'll configure PNPM to "hoist" packages, making them more accessible in the dependency tree. This ensures compatibility with Azure App Service while maintaining most of PNPM's benefits. It worth noting that you can also set this up to only happen on your CI/CD pipeline so that you can maximise PNPM performance on your development machine.
Creating the .npmrc configuration file:
In your project root (alongside package.json), create a file named .npmrc:
touch .npmrc
Recommended configuration for Azure:
Add the following content to .npmrc:
//.npmrc
// Automatically installs peer dependencies without prompting.
auto-install-peers=true
// Dependency linking strategy. "hoisted" flattens the dependency
node-linker=hoisted
Testing your hoisting configuration:
After creating .npmrc, reinstall dependencies to apply the configuration:
# Remove existing node_modules and lock file
rm -rf node_modules pnpm-lock.yaml
# Reinstall with new configuration
pnpm install
Examine the node_modules structure:
ls -la node_modules/
With node-linker=hoisted, you should see all packages at the root level, similar to npm's flat structure.
Troubleshooting hoisting issues:
If you encounter runtime errors after deployment to Azure (covered in Part 3), try these steps:
Switch to shamefully-hoist: Modify .npmrc to use shamefully-hoist=true instead
Reinstall dependencies: Always run pnpm install after changing .npmrc
Install transient dependencies: Some dependencies may need to be explicitly installed
Clear build cache: Delete .next folder and rebuild
Check Azure logs: Use App Service logs to identify missing dependencies (Part 3)
Common issues and solutions:
Security consideration: While hoisting improves compatibility, it does reduce PNPM's strict dependency isolation. In production environments with stringent security requirements, consider:
Regularly auditing dependencies with pnpm audit
Using pnpm-lock.yaml to ensure reproducible builds
Implementing dependency scanning in your CI/CD pipeline (covered in Part 2)
Building Your Next.js Application for Azure
With configuration complete, let's build the application and understand what gets generated.
Run the production build:
pnpm build
You should see output similar to:

Understanding the build output:
Route table: Shows all pages and their bundle sizes
○ indicates static pages (generated at build time)
λ (if you see it) indicates server-side rendered pages
First Load JS is the total JavaScript needed for that page
.next directory: Contains all build artifacts
.next/standalone/: The deployable application (what goes to Azure)
.next/static/: Static assets with content hashing
.next/server/: Server-side rendering chunks
Examining the standalone output:
Navigate to the standalone directory:
cd .next/standalone
ls -la
You should see:
.next/ # Build artifacts
node_modules/ # Only production dependencies
public/ # Static files (if any)
package.json # Minimal package.json
server.js # Entry point
The server.js file:
This is the entry point for your Next.js application on Azure. Let's look at what it does.
The generated server.js typically contains:
// Minimal Next.js server that:
// 1. Starts the HTTP server
// 2. Serves static files
// 3. Handles SSR and API routes
// 4. Manages Next.js middleware
process.env.NODE_ENV = 'production'
process.chdir(__dirname)
const NextServer = require('next/dist/server/next-server').default
const http = require('http')
const path = require('path')
// Read configuration
const nextConfig = {...}
// Start server
startServer({
dir,
isDev: false,
config: nextConfig,
hostname,
port: currentPort,
allowRetry: false,
keepAliveTimeout,
}).catch((err) => {
console.error(err);
process.exit(1);
});
Understanding package.json in standalone:
cat package.json
The standalone package.json is minimal:
{
"dependencies": {
"next": "16.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
// Only packages actually used in production
}
}
Testing the standalone build locally:
This is crucial before deploying to Azure. Let's ensure the standalone build works correctly:
# From the .next/standalone directory
node server.js
You should see:

Open your browser to http://localhost:3000. The application may look a little funky when compared to the development version. But that is ok! We need to do a little more work to get the deployment bundles which we will do in part 2.
Common build errors and solutions:
Build size optimisation:
Check the size of your standalone build:
# From project root
du -sh .next/standalone
A typical Next.js 15 starter should be around 80-120 MB. If significantly larger:
Audit dependencies: Use pnpm why <package> to understand why packages are included
Remove unused packages: Clean up package.json
Check for large assets: Ensure images/videos aren't bundled unnecessarily
Analyze bundle: Use @next/bundle-analyzer
Setting up bundle analyzer (optional):
pnpm add -D @next/bundle-analyzer
Update next.config.ts:
import type { NextConfig } from "next";
import bundleAnalyzer from '@next/bundle-analyzer';
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
const nextConfig: NextConfig = {
output: 'standalone',
// ... other config
};
export default withBundleAnalyzer(nextConfig);
Analyse your bundle:
ANALYZE=true pnpm build
This opens a visual breakdown of your bundle in the browser, helping identify optimisation opportunities.
Project Structure Best Practices for Azure Deployment
Let's organise the project for optimal maintainability and Azure deployment.
Recommended folder structure:
my-nextjs-azure-app/
├── .azure/ # Azure-specific configurations (Part 2)
│ └── pipelines/
│ └── azure-pipelines.yml
├── .github/ # OR, GitHub configurations
│ └── workflows/
├── public/ # Static assets
│ ├── images/
│ ├── fonts/
│ └── favicon.ico
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── api/ # API routes
│ │ ├── (auth)/ # Route groups
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── components/ # Reusable React components
│ │ ├── ui/ # UI primitives
│ │ └── layout/ # Layout components
│ ├── lib/ # Utility functions and shared code
│ │ ├── utils.ts
│ │ └── constants.ts
│ ├── types/ # TypeScript type definitions
│ └── styles/ # Global styles
├── .env.local # Local environment variables
├── .env.example # Example environment variables template
├── .env.production # Production env vars (not committed)
├── .eslintrc.json
├── .gitignore
├── .npmrc # PNPM configuration
├── next.config.ts
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── README.md
├── tailwind.config.ts
└── tsconfig.json
Environment variable management:
Create .env.local for development (never commit this):
# .env.local - DO NOT COMMIT
NODE_ENV=development
NEXT_PUBLIC_API_URL=http://localhost:4000
DATABASE_URL=postgresql://localhost:5432/mydb
AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;...
Important: Add .env.local to .gitignore:
# .gitignore additions
.env.local
.env.production
.env*.local
Understanding Next.js environment variables:
Next.js has specific rules for environment variables:
NEXT_PUBLIC_ variables:*
Exposed to the browser
Embedded at build time
Use for API endpoints, public configuration
Example: NEXT_PUBLIC_API_URL
Server-only variables:
Only available in server components and API routes
Never exposed to the browser
Use for secrets, database URLs, API keys
Example: DATABASE_URL
Azure-specific environment variables (Part 3):
Azure App Service provides built-in variables you can use:
// src/lib/azure-config.ts
export const azureConfig = {
// Azure provides these automatically
websiteSiteName: process.env.WEBSITE_SITE_NAME,
websiteHostname: process.env.WEBSITE_HOSTNAME,
websiteInstanceId: process.env.WEBSITE_INSTANCE_ID,
// Your custom variables (set in Azure Portal)
apiUrl: process.env.NEXT_PUBLIC_API_URL,
databaseUrl: process.env.DATABASE_URL,
};
Your Azure Hosted NextJs 16 Deployment is almost complete
Setting up Next.js 16 with PNPM for Azure App Service deployment requires specific configuration considerations that differ from the default Vercel-optimised setup. By implementing standalone output mode and configuring PNPM's package hoisting, you've created a foundation for efficient, reliable Azure deployments.
The key takeaways from this guide:
Standalone mode is essential for Azure App Service deployments, dramatically reducing size and complexity
PNPM's hoisting configuration ensures compatibility with Node.js runtime expectations on Azure
Proper project structure and environment variable management set you up for success in production
Validation before deployment catches issues early in the development cycle
With these configurations in place, your Next.js application is ready for automated deployment via Azure DevOps pipelines. The standalone build we've created will deploy efficiently, start quickly, and run reliably on Azure App Service.
In the next article, we'll automate the entire standalone build process using Azure DevOps, implementing CI/CD best practices for continuous delivery to Azure.
Did you find this guide helpful? Share it with your team, and stay tuned for Part 2 where we dive into Azure DevOps CI/CD pipeline configuration!
In Part 2, we'll cover:
Creating an Azure App Service
Setting up Azure DevOps organisation and project
Configuring service connections to Azure
Writing the complete Azure Pipelines YAML configuration
Building and deploying automatically on every commit
Managing environment-specific configurations
Handling secrets with Azure Key Vault
Pipeline optimisation and caching strategies
This is Part 1 of a 3-part series on deploying Next.js 15 to Azure App Service. Continue with Part 2: Deploying with Azure DevOps Pipelines or jump to Part 3: Production Configuration and Monitoring.
Additional Resources
Official Documentation:
Useful Tools:
Community:





Comments