Automating Next.js 15 deployments
In my previous guide, Next.js 15 build once, deploy many, we discussed the architectural shift toward runtime configuration. Now, it is time to put that theory into practice.
To achieve a true "Build Once, Deploy Many" workflow, your GitHub Actions must be structured to separate the expensive Build phase from the lightweight Deploy phase.
The Strategy: Artifact Promotion
In a standard pipeline, developers often run npm run build for every environment. This is inefficient. Instead, we will:
-
Build the application once in a "Standalone" mode.
-
Upload that bundle as a GitHub Artifact.
-
Download and push that same bundle to Azure DEV, SIT, and PROD.
The Pipeline Visualized
[ CI/CD RUNNER ]
|
[ Job: Build ] ---------------------> [ Artifact Storage ]
(npm build + zip) |
| |
[ Job: Deploy DEV ] <------------------------+
(Download + Azure Push) |
| |
[ Job: Deploy SIT ] <------------------------+ (Manual Approval)
(Download + Azure Push)
The GitHub Actions Workflow
This YAML configuration utilizes GitHub Environments to manage promotion logic and secrets for different Azure targets.
name: Next.js 15 Promotion Pipeline
on:
push:
branches: [main]
jobs:
# --- STAGE 1: BUILD ---
build:
name: Build Universal Artifact
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install & Build
# Note: No env variables are passed here!
run: |
npm ci
npm run build
- name: Package Standalone Output
# We prepare the standalone folder for Azure
run: |
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public
cd .next/standalone && zip -r ../../site.zip .
- name: Archive Artifact
uses: actions/upload-artifact@v4
with:
name: nextjs-universal-bundle
path: site.zip
retention-days: 5
# --- STAGE 2: DEPLOY TO DEV ---
deploy-dev:
name: Deploy to DEV
needs: build
runs-on: ubuntu-latest
environment: Development
steps:
- name: Get Artifact
uses: actions/download-artifact@v4
with:
name: nextjs-universal-bundle
- name: Push to Azure DEV
uses: azure/webapps-deploy@v3
with:
app-name: 'myapp-dev-web'
publish-profile: $
package: site.zip
- name: Trigger Revalidation
run: curl -X GET "https://myapp-dev-web.azurewebsites.net/api/revalidate?secret=$"
# --- STAGE 3: DEPLOY TO SIT ---
deploy-sit:
name: Promote to SIT
needs: deploy-dev
runs-on: ubuntu-latest
environment: SIT # Set this to require manual approval in GitHub
steps:
- name: Get Artifact
uses: actions/download-artifact@v4
with:
name: nextjs-universal-bundle
- name: Push to Azure SIT
uses: azure/webapps-deploy@v3
with:
app-name: 'myapp-sit-web'
publish-profile: $
package: site.zip
- name: Trigger Revalidation
run: curl -X GET "https://myapp-sit-web.azurewebsites.net/api/revalidate?secret=$"
Key Technical Considerations
-
Standalone Output: For this pipeline to work, your
next.config.jsmust haveoutput: 'standalone'. This tells Next.js to package only the essential files required for a production server, significantly reducing the size of yoursite.zip. -
Manual Approvals (The Gatekeeper): By using
environment: SIT, you can go to your GitHub repository settings and add "Required Reviewers." This prevents the code from moving from DEV to SIT until a Lead Developer or QA Engineer clicks "Approve." -
Azure Application Settings: Since we are no longer using
NEXT_PUBLIC_variables, you must go to the Azure Portal > Configuration for each Web App and manually add yourAPI_BASE_URL. The Next.js server will read these at runtime via the SSR approach we established.
Benefits of this Pipeline
-
Speed: Deployments to SIT and PROD take seconds because the build step is skipped.
-
Integrity: You are guaranteed that the code running in SIT is identical to the code tested in DEV.
-
Cost: You save significant GitHub Actions "Minutes" by only building once per commit.
If you haven't yet configured your application to handle runtime variables without
NEXT_PUBLIC_, check out my companion article: Next.js 15 Build Once, Deploy Many: Achieving Environment-Agnostic Builds with the App Router.