Migrating WordPress to Azure App Service Linux: Fixing Ports, Database Conflicts, and Nginx 404s

Here is a comprehensive summary of our technical troubleshooting steps, followed by a complete, production-ready blog post formatted with clear Markdown headers, code blocks, and callouts so you can easily copy and paste it directly into your WordPress block editor.

Technical Summary Checklist

  • App Service Infrastructure Initialization: Provisioned an Azure Web App on an App Service Linux Plan, staging WordPress core assets (v6.6.1) directly into the /home/site/wwwroot/ path.
  • Networking & Container Port Alignment: Resolved a container startup failure by mapping the WEBSITES_PORT application setting to port 8080, successfully routing traffic from Azure’s outer proxy layer down to the internal Nginx listener.
  • Reverse Proxy Layer Core Overrides: Updated wp-config.php to intercept the HTTP_X_FORWARDED_PROTO header, explicitly informing WordPress that incoming traffic was encrypted (https), thereby fixing invalid redirection loops.
  • Database Schema & Content Isolation: Mitigated a critical production-breaking database upgrade prompt (where the new v6.6.1 application container attempted to overwrite an older v5.2.21 database engine) by generating a separate schema clone using MySQL Workbench.
  • Custom Nginx Ingress Controller Routing Configuration: Addressed global routing 404 errors by extracting the native /etc/nginx/sites-available/default file, appending the mandatory WordPress rewrites (try_files $uri $uri/ /index.php?$args;), and linking it directly to the active sites-enabled state.
  • Mass Media Migration Management: Successfully pushed over 462 MB of uncompressed binary media library assets (/wp-content/uploads/) past Azure basic auth publishing limitations using safe end-to-end WinSCP file streaming.
  • Persistent Provisioning Loop Automation: Wrapped the custom Nginx file overrides into a persistent shell asset (/home/site/startup.sh), sanitized the invisible file formatting from CRLF to LF to pass standard Unix compilation boundaries, and mapped it into the global Azure Web App Startup Command array.

Migrating WordPress to Azure App Service Linux: Fixing Ports, Database Conflicts, and Nginx 404s

Migrating a live WordPress website to a modern, cloud-native hosting platform like Azure App Service on Linux yields immense performance and scaling benefits. However, combining a containerized cloud infrastructure with WordPress’s legacy architecture often presents unique technical hurdles.

During a recent deployment of an updated WordPress ecosystem running parallel to a legacy production site, we ran the gauntlet of container port mismatches, database version conflicts, broken permalinks, and ephemeral file resets.

Here is the step-by-step engineering playbook detailing how we migrated the assets, isolated the database schemas, and configured a persistent, reboot-proof Nginx web server to achieve a successful deployment.


1. Initial Deployment & Solving the Port Mismatch

The architecture began by provisioning a fresh Azure Web App on a Linux App Service plan running a modern PHP 8.2 runtime. We uploaded the latest WordPress core engine files directly into the standard persistent app directory:
/home/site/wwwroot/

After updating the core database connection array inside the wp-config.php file, the initial deployment ground to a halt. The container failed its startup probe, yielding a critical platform error:

Container did not respond to startup probe on port 80 within the expected time limit. 
Port mismatch detected: the container is listening on port 8080...

The Fix:

Azure Web Apps expect your application container to listen on port 80 or 8080 by default. If your custom container or specific stack configuration defaults to an alternate internal port, you must explicitly instruct Azure’s routing layer where to pass incoming web traffic.

We navigated to Settings > Environment variables in the Azure Portal and added the following key-value pair to align the proxy layers:

  • Name: WEBSITES_PORT
  • Value: 8080

2. Preventing Database Corruption & Safely Isolating Schema Versions

Once the container initialization successfully completed, logging into the new administrative dashboard presented a critical dilemma. WordPress immediately intercepted the initialization loop with a block screen: “Update WordPress Database”.

The Conflict:

The newly deployed application files were running a modern WordPress engine, while our active, live production website (://aspnet4you.com) was bound to the exact same external database running a legacy version.

Clicking the update button would have immediately altered the core database schema tables globally, instantly crashing the live production website.

                  ┌──────────────────────────────┐
                  │ External MySQL DB (v5.2.21)  │
                  └──────────────┬───────────────┘
                                 │
                ┌────────────────┴────────────────┐
                ▼                                 ▼
┌──────────────────────────────┐   ┌──────────────────────────────┐
│  Live App Service Container  │   │  New App Service Container   │
│     WordPress v5.2.21        │   │      WordPress v6.6.1        │
│      (Status: Stable)        │   │  (CRITICAL SCHEMA CONFLICT)  │
└──────────────────────────────┘   └──────────────────────────────┘

The Fix:

To isolate the development and migration layers, we opened MySQL Workbench and executed a clean schema replication cycle to split the environments completely:

  1. Used the Data Export wizard inside the Administration panel to download a full backup file containing all tables, views, stored procedures, and triggers.
  2. Instantly instantiated a clean, isolated database sandbox target:CREATE DATABASE blogs_stage_db;
  3. Utilized the Data Import wizard to restore the .sql dataset into the newly created database target.
  4. Updated the application environment array inside /home/site/wwwroot/wp-config.php to target the new database and custom user permissions:define('DB_NAME', 'blogs_stage_db');

Now completely sandbox-isolated from our production traffic, we safely executed the database update utility. The application completed the schema modernization effortlessly without touching our active live site assets.


3. Resolving Global Nginx 404 Pretty Permalink Errors

With the database split completed, the administration panels loaded smoothly. However, trying to access the root page or any historic post sub-links threw absolute HTTP 404 Not Found errors.

The Problem:

WordPress relies heavily on Apache .htaccess rewriting rules to build clean, human-readable permalinks. Because our Azure Linux Web App operates on an Nginx web engine, it completely ignores .htaccess files. Without an explicit fallback loop configuration, Nginx throws a 404 error on any route that does not match a real file on disk.

The Fix:

We accessed the internal web server environment to inject native routing fallbacks. Nginx routes traffic based on configuration files mapped inside its structural runtime folders.

  1. Connected to the environment and copied the active system configuration file to our persistent storage array:cp /etc/nginx/sites-available/default /home/site/default
  2. Opened /home/site/default and appended the essential try_files query parsing fallback directly inside the central location / block:location / { index index.php index.html index.htm hostingstart.html; try_files $uri $uri/ /index.php?$args; }

4. Bypassing Large Media Upload Constraints

A major hurdle remained: migrating the expansive media library, which exceeded 462 MB of legacy upload folders. Azure App Service protects network endpoints by blocking basic password-based FTP authentication layers by default on new instances, and web-based Kudu managers time out on large data streams.

The Fix:

We bypassed file managers entirely by leveraging secure SSH-based file streaming:

  1. Enabled standard system package deployment channels to fetch WinSCP, a secure file transfer client.
  2. Connected directly to the web app storage over a secure network tunnel.
  3. Uploaded the entire historic binary dataset directly into the persistent folder path:
    /home/site/wwwroot/wp-content/uploads/

5. Automation: Creating a Reboot-Proof Startup Architecture

Azure App Service Linux containers are inherently ephemeral. If the underlying virtual host scales out, restarts during routine maintenance, or reboots from the portal, any changes made directly to system directories (like /etc/nginx/) are completely wiped out. Only files placed within the /home drive persist.

To automate our custom Nginx routing rules so they seamlessly survive reboots, we built an initialization hook script.

Step 1: Create the Automation Engine

Inside the persistent storage location (/home/site/), we instantiated an initialization script file named startup.sh:

#!/bin/bash

echo "Applying custom WordPress Nginx routing rules..."

# Overwrite factory-default configurations with our persistent copy
cp /home/site/default /etc/nginx/sites-available/default
cp /home/site/default /etc/nginx/sites-enabled/default

# Validate the integrity of the Nginx configuration syntax and reload
nginx -t && service nginx reload

echo "Nginx successfully updated!"

Step 2: Clear Hidden Formatting Obstacles

Because files written or edited on Windows environments contain invisible Carriage Return Line Feed (CRLF) characters, running this on Linux will crash with a hidden script interpretation error.

We opened the file inside a advanced editor like Notepad++ and converted the line endings from CRLF to LF (or utilized the terminal utility dos2unix startup.sh). Then, we granted universal execution permissions to the script file:

chmod +x /home/site/startup.sh

Step 3: Map the Azure Bootstrap Hooks

Finally, we informed Azure to execute this file automatically during every container boot cycle. Navigating to Configuration > General Settings in the Azure Portal, we mapped our script directly into the Startup Command setting input:

/home/site/startup.sh
                     ┌─────────────────────────────┐
                     │ Azure Container Boot Sequence│
                     └──────────────┬──────────────┘
                                    │
                                    ▼
                     ┌─────────────────────────────┐
                     │ Oryx Runtimes Standard Init │
                     └──────────────┬──────────────┘
                                    │
                                    ▼
                     ┌─────────────────────────────┐
                     │ Executes: /home/site/startup.sh
                     └──────────────┬──────────────┘
                                    │
            ┌───────────────────────┴───────────────────────┐
            ▼                                               ▼
┌──────────────────────────────┐                ┌──────────────────────────────┐
│  Overwrites System Default   │                │   Triggers: nginx -t &&      │
│  Nginx Configuration Files   │                │     service nginx reload     │
└──────────────────────────────┘                └──────────────────────────────┘

Conclusion

With the startup sequence automation finalized, the app container boots flawlessly, automatically pulls down our tailored Nginx reverse-proxy routes, reconnects cleanly to the isolated database, and servers our rich media assets without error. The entire ecosystem is robust, persistent, and entirely reboot-proof.

If you are setting up WordPress on Azure Web Apps, investing time early to handle port configurations, isolate staging database schemas, and create shell execution hooks for Nginx guarantees a high-performance, maintenance-free production experience!


Disclaimers

First this blog post is generated by Gemini based on extensive conversations over 3 days/nights of troubleshooting. I tried to use Copilot initially but Copilot was giving me wrong instructions in this case as it was confused by context from my previous .NET8 app that is successfully deployed in same Linus App Service Plan! I tried my best to remember what I did end-to-end to solve multiple issue but Gemini did great job remembering the details – so nice! This is the context that I provided Gemini to produce this blog post –
Can you summarize what we have done from start to finish and write a blog post in WordPress format?
This is what I remember I did-
1) Created a new Web App in Linux app plan
2) Downloaded WordPress v6.6.1 and uploaded to wwwroot of newly created web app.
3) Updated the wp-config file with database settings.
4) Tried to access the site but it was failing due to port mismatch. That is corrected with your help. I had to add port 8080 in environment setting.
5) I got past the port issue. I was able to sign-in to admin page but it presented me with option to upgrade database. I did not upgrade as it would break my live app/site which is running on different version of WordPress.
6) I created a new database using SQL Workbench. Exported and then imported.
7) Updated wp-config with new database settings.
8) Tried to access site but both parent site and child pages came with 404.
9) Copied the default file from /etc/nginx/sites-available/default to home/site. Modified the location section to add – try_files $uri $uri/ /index.php?$args; This fixed the 404 redirection issue.
10) One final update. Created a startup.sh page and referenced it in startup command. This is to avoid losing the default location settings on container recreation.

Leave a Reply