To serve a WordPress blog at example.com/blog/ and a Django app at example.com/ from a single domain, use one Nginx server block: route location / to Django through a gunicorn or uWSGI socket, and route location ^~ /blog/ to WordPress through php8.3-fpm with try_files $uri $uri/ /blog/index.php?$args;. A subdirectory keeps both apps under one domain so they share SEO authority and first-party cookies — unlike a subdomain, which splits them.
Key takeaways
- One server block, two backends. Django is served at
location /via a gunicorn/uWSGI upstream socket; WordPress is served atlocation ^~ /blog/via PHP-FPM. - Subdirectory beats subdomain for SEO when the blog should reinforce the main domain's authority and share first-party cookies.
- Use
php8.3-fpm, neverphp5/php7— those PHP-FPM versions are end-of-life and get no security patches. - Point WordPress at the subpath. Set
Site Address (URL)(orWP_SITEURL/WP_HOME) tohttps://example.com/blogso permalinks and assets resolve. - Keep Django's
STATIC_URL/MEDIA_URLoff/blogso the two apps never fight over the same path. - Use
^~on the/blog/prefix so it wins over Django's catch-alllocation /and over regex location blocks.
Should the blog be a subdirectory or a subdomain?
Both work technically — the decision is mostly about SEO and operational isolation.
| Factor | Subdirectory (example.com/blog/) |
Subdomain (blog.example.com) |
|---|---|---|
| SEO authority | Consolidated into one domain; blog links lift the whole site | Treated as a related but separate site; authority is split |
| First-party cookies | Shared with the main app by default | Not shared unless you set a parent-domain cookie |
| Setup complexity | One server block, careful location precedence |
Two server blocks/vhosts, simpler routing |
| Isolation (PHP vs Python) | Lower — same hostname, logs and TLS cert | Higher — separate logs, certs and failure domains |
| TLS certificate | One cert covers everything | Subdomain must be on the cert (wildcard or SAN) |
If the goal is to funnel blog traffic and link equity into the product site, the subdirectory wins. If you want hard operational separation (different teams or deploy cadences), a subdomain is cleaner.
What this setup assumes
| Component | Value |
|---|---|
| Domain | example.com |
| Django app server | gunicorn (or uWSGI) on a Unix socket |
| Django socket | /run/gunicorn/example.sock |
| Django static / media | /static/ and /media/ prefixes |
| Nginx document root | /var/www/html |
| WordPress files | /var/www/html/blog |
| PHP-FPM socket | /run/php/php8.3-fpm.sock |
The key idea: Nginx accepts every request, then hands / to Django over an upstream socket and /blog/ to WordPress over PHP-FPM. Installing WordPress inside a folder named blog under the document root means /blog/index.php maps directly to /var/www/html/blog/index.php with no path rewriting.
How do you write the Nginx server block?
Define an upstream for Django, then a single server block. The ^~ prefix on /blog/ is what makes this reliable: it tells Nginx to stop matching regex locations once the /blog/ prefix matches, so WordPress requests never fall through to Django.
upstream django_app {
# gunicorn (or uWSGI) listening on a Unix socket
server unix:/run/gunicorn/example.sock;
}
server {
listen 80;
server_name example.com;
# Document root is the PARENT of the WordPress folder.
# WordPress files live in /var/www/html/blog, so /blog/ maps cleanly.
root /var/www/html;
index index.php;
# --- Django served at / ---
location / {
include proxy_params; # use uwsgi_params if you uwsgi_pass instead
proxy_pass http://django_app;
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;
}
# --- Django static & media (kept OUT of /blog) ---
location /static/ { alias /home/example/static/; }
location /media/ { alias /home/example/media/; }
# --- /blog (no slash) -> /blog/ so the prefix location can match ---
location = /blog { return 301 /blog/; }
# --- WordPress served at /blog/ ---
# ^~ makes this prefix WIN over "location /" and over any regex blocks,
# so blog requests never fall through to Django.
location ^~ /blog/ {
# permalink-friendly front controller
try_files $uri $uri/ /blog/index.php?$args;
location ~ \.php$ {
include fastcgi_params;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php8.3-fpm.sock; # NOT php5/php7 (EOL)
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
}How do you point WordPress at the subpath?
Nginx routing is only half the job. WordPress must also know it lives under /blog, or it will generate links and redirects back to the root and cause redirect loops. Set the Site Address in Settings → General, or hard-code it in wp-config.php:
// wp-config.php
define('WP_HOME', 'https://example.com/blog');
define('WP_SITEURL', 'https://example.com/blog');
// Nginx terminates TLS, so let WordPress know the request is HTTPS:
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}Keeping Django and WordPress from colliding
The two apps share a hostname, so any path Django owns must not overlap /blog:
- Set Django's
STATIC_URL = '/static/'andMEDIA_URL = '/media/'(any prefix except/blog/). The Nginxaliasblocks serve them directly, bypassing Django. - Don't mount a Django URL route at
/blog— that path now belongs to WordPress. - Make sure
ALLOWED_HOSTScoversexample.comso proxied requests aren't rejected, and setSECURE_PROXY_SSL_HEADERto trust theX-Forwarded-Protoheader above.
For a deeper look at tuning the Django half of this stack, see Tuning Django on Nginx and uWSGI. If you'd rather isolate the two runtimes, you can also run Django and WordPress together with Docker Compose.
Common pitfalls and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
/blog/ loads the Django app |
/blog/ lost to location / or a regex block |
Use location ^~ /blog/ so the prefix wins |
| PHP file downloads instead of running | No location ~ \.php$ or wrong FPM socket |
Add the nested PHP block, point at php8.3-fpm.sock |
Redirect loop on /blog/ |
WordPress siteurl still points at / |
Set WP_SITEURL/WP_HOME to …/blog |
| 404 on pretty permalinks | Missing front-controller fallback | try_files $uri $uri/ /blog/index.php?$args; |
/blog (no slash) returns 404 |
Prefix only matches /blog/ |
Add location = /blog { return 301 /blog/; } |
| Django CSS/JS missing | STATIC_URL overlaps /blog |
Keep static/media on /static/, /media/ |
Frequently Asked Questions
Why use a subdirectory instead of a subdomain for the blog?
A subdirectory (example.com/blog/) keeps the blog on the same domain, so inbound links and content consolidate the main site's SEO authority and share first-party cookies. A subdomain (blog.example.com) is treated as a separate site, splitting authority and cookies. Choose a subdirectory for SEO consolidation; choose a subdomain for harder operational isolation.
Which PHP version should I use with WordPress and Nginx?
Use a currently supported PHP-FPM release such as php8.3-fpm. The php5-fpm and php7.x-fpm packages referenced in many legacy guides are end-of-life, get no security patches, and aren't supported by current WordPress. Point fastcgi_pass at /run/php/php8.3-fpm.sock.
Why does my /blog/ URL keep loading the Django app?
Location precedence. Nginx evaluates a regex location before a plain prefix, and location / is the catch-all. Use location ^~ /blog/ — the ^~ modifier tells Nginx to use this prefix and skip regex matching, so WordPress requests never fall through to Django.
Do I need to change anything inside WordPress itself?
Yes. Set the WordPress Site Address (URL) to https://example.com/blog (Settings → General), or define WP_HOME and WP_SITEURL in wp-config.php. Without it, WordPress emits links and redirects pointing at the root, which can create redirect loops.
How do I stop Django static files from clashing with the blog?
Serve Django's static and media from dedicated prefixes (/static/, /media/) that don't overlap /blog, and let Nginx serve them with alias. Never mount a Django route at /blog. As long as the prefixes are distinct, the two apps coexist cleanly.
Can I run this with gunicorn instead of uWSGI, and what about HTTPS?
Yes to both. Define an upstream pointing at the gunicorn Unix socket and use proxy_pass in location / (with proxy_params); for uWSGI, swap to uwsgi_pass with include uwsgi_params;. The WordPress /blog/ block is identical either way. For TLS, terminate it in the same server block, forward X-Forwarded-Proto $scheme, set the WordPress URLs to https://, and configure Django's SECURE_PROXY_SSL_HEADER.
Where to go from here
This single-server-block pattern gives you a WordPress content engine under /blog/ while Django runs the product at / — one domain, one TLS certificate, consolidated SEO. If you'd like help hardening the deployment, tuning PHP-FPM and gunicorn, or migrating an existing blog into the subpath, our server maintenance and DevOps team can set it up end to end.