Yes — you can host a Django application and a WordPress site on the same Amazon EC2 instance under one domain. The trick is to put nginx in front as a single reverse proxy and split traffic by URL: nginx forwards Django requests to gunicorn over a Unix socket (proxy_pass) and hands WordPress's PHP requests to php-fpm (fastcgi_pass). You choose the split with location blocks — either by path (example.com/app/ → Django, example.com/ → WordPress) or by subdomain (app.example.com → Django, example.com → WordPress) — and secure everything with one free Let's Encrypt certificate.
This pattern works well for small sites and tight budgets. For anything business-critical you'll get better isolation, scaling and security by separating the two apps — we cover that trade-off in full below.
Key takeaways
- One nginx, two back ends: nginx is the single entry point —
proxy_passto gunicorn for Django,fastcgi_passto php-fpm for WordPress. - Pick path or subdomain routing: a subpath (
/app/) is the cheapest to run; a subdomain (app.example.com) gives cleaner cookies, SEO and isolation. - gunicorn, not uWSGI: in 2026, gunicorn behind a systemd socket is the standard way to run Django in production; uWSGI is now largely legacy.
- Keep the databases separate: Django on PostgreSQL, WordPress on MySQL/MariaDB, both bound to
127.0.0.1only. - Free TLS in minutes:
certbot --nginxissues and auto-renews a Let's Encrypt certificate and adds the HTTP→HTTPS redirect for you. - Co-hosting has limits: it's fine for low-traffic or budget setups, but a noisy neighbour, a WordPress breach, or a traffic spike hits both apps — separate them once either one matters.
Why run Django and WordPress on the same EC2 instance?
The most common reason is brand consistency on a budget: you want a marketing site or blog in WordPress (where non-developers can publish content) and a product, dashboard or API in Django — all under one domain, on one server, behind one TLS certificate.
Putting both on a single Amazon EC2 instance gives you:
- One bill and one box to patch instead of two.
- One domain and one certificate, so links never cross-origin.
- A simple mental model — nginx is the only thing exposed to the internet, and it decides who gets each request.
Be honest about what it is, though: this is a small-scale / cost-first architecture. The two apps share CPU, RAM, disk and a single blast radius. It's a great fit for a startup landing page plus an early product, or an internal tool beside a content site. It is not where you want a high-traffic store and a critical app to live together long-term.
Path-based or subdomain routing — which should you choose?
Both keep the two apps on the same instance; they differ in how nginx and the apps see the URL.
| Consideration | Path-based (example.com/app/) |
Subdomain (app.example.com) |
|---|---|---|
| DNS setup | None — same hostname | One extra A/AAAA record |
| TLS certificate | One cert covers it | Add the subdomain to the same cert (-d app.example.com) |
| Django config | Needs FORCE_SCRIPT_NAME = "/app" and prefixed cookie/static paths |
Runs at root, no script-name juggling |
| Cookies & sessions | Must scope cookie paths so Django and WordPress don't clash | Cleanly isolated per host by default |
| SEO | One domain's authority; tidy internal links | Treated as a related site; clearer separation |
| Isolation | Lower — same origin | Higher — different origin |
Rule of thumb: reach for a subdomain unless you specifically need everything under one path on one hostname. It avoids the fiddliest part of co-hosting (Django under a sub-path) and gives you cleaner cookie and origin boundaries for almost no extra effort.
What do you install on the EC2 instance?
Spin up an Ubuntu 24.04 LTS instance (a t3.small or t3.medium is a sensible starting size for a co-hosted pair — pick based on traffic, not this guide). Then install the front door, both runtimes and both databases.
Note how much has changed since the original Ubuntu-14-era recipe: php5-fpm is gone (PHP 8.3 is current), mysql-server is commonly swapped for MariaDB, and Django is served by gunicorn, not uWSGI.
# Ubuntu 24.04 LTS on EC2 — update the box first
sudo apt update && sudo apt upgrade -y
# Front door + Django runtime
sudo apt install -y nginx python3 python3-venv python3-pip
# WordPress runtime: PHP-FPM 8.3 + the extensions WordPress expects
sudo apt install -y php8.3-fpm php8.3-mysql php8.3-curl php8.3-gd php8.3-xml php8.3-mbstring
# Databases: PostgreSQL for Django, MariaDB for WordPress
sudo apt install -y postgresql mariadb-server
# Free TLS tooling (Let's Encrypt)
sudo apt install -y certbot python3-certbot-nginx
# Isolated virtualenv for the Django app, with gunicorn + the Postgres driver
python3 -m venv /home/ubuntu/django/env
source /home/ubuntu/django/env/bin/activate
pip install --upgrade pip
pip install django gunicorn "psycopg[binary]"How do you run Django with gunicorn behind a systemd socket?
The modern, restart-safe way to run Django is gunicorn managed by systemd, listening on a Unix socket that only nginx talks to. A systemd socket unit also gives you on-demand activation and a stable socket path with the right permissions.
The key detail is the socket's group: set gunicorn's group to www-data (nginx's user) so nginx can read /run/gunicorn.sock. If you've followed our older walkthrough of deploying Django with nginx, this is the same idea — just with gunicorn in place of uWSGI.
# /etc/systemd/system/gunicorn.socket
[Unit]
Description=gunicorn socket for the Django app
[Socket]
ListenStream=/run/gunicorn.sock
SocketUser=www-data
SocketMode=0660
[Install]
WantedBy=sockets.target# /etc/systemd/system/gunicorn.service
[Unit]
Description=gunicorn daemon for the Django app
Requires=gunicorn.socket
After=network.target
[Service]
User=ubuntu
Group=www-data
WorkingDirectory=/home/ubuntu/django/src
ExecStart=/home/ubuntu/django/env/bin/gunicorn \
--workers 3 \
--bind unix:/run/gunicorn.sock \
myproject.wsgi:application
[Install]
WantedBy=multi-user.target
# Enable the socket (it starts gunicorn on first request):
# sudo systemctl daemon-reload
# sudo systemctl enable --now gunicorn.socket
# sudo systemctl status gunicorn.socketIf you serve Django under a sub-path (/app/), it needs to know that prefix so URLs, redirects, static files and cookies all line up. With a subdomain you can skip every line below except ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS and SECURE_PROXY_SSL_HEADER.
# settings.py — serving Django under the /app/ sub-path
FORCE_SCRIPT_NAME = "/app"
STATIC_URL = "/app/static/"
SESSION_COOKIE_PATH = "/app/"
CSRF_COOKIE_PATH = "/app/"
ALLOWED_HOSTS = ["example.com", "www.example.com"]
CSRF_TRUSTED_ORIGINS = ["https://example.com"]
# Trust nginx's X-Forwarded-Proto so request.is_secure() is correct behind TLS
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")How do you serve WordPress with php-fpm?
WordPress is plain PHP, so nginx never "runs" it directly — it hands .php requests to a php-fpm worker pool over a Unix socket. The default pool (www) already listens on /run/php/php8.3-fpm.sock; you mostly just confirm the user/group and tune the worker count for your instance size.
# /etc/php/8.3/fpm/pool.d/www.conf (the lines that matter)
[www]
user = www-data
group = www-data
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
# Apply changes:
# sudo systemctl restart php8.3-fpmHow do you configure nginx as the front door?
This is where it comes together. Define an upstream pointing at the gunicorn socket, send /app/ to it with proxy_pass, serve Django's collected static files straight from disk, and let everything else fall through to WordPress — whose .php files go to php-fpm via fastcgi_pass.
The ^~ on location /app/ tells nginx "if this prefix matches, don't also try the \.php$ regex," so a Django URL can never be mistaken for a PHP file. WordPress's try_files ... /index.php?$args is what makes pretty permalinks work.
# /etc/nginx/sites-available/example.com — PATH-BASED routing
# example.com/ -> WordPress (php-fpm)
# example.com/app/ -> Django (gunicorn)
upstream django_gunicorn {
server unix:/run/gunicorn.sock;
}
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/wordpress;
index index.php index.html;
# --- Django app at /app/ (reverse-proxied to gunicorn) ---
location ^~ /app/ {
proxy_pass http://django_gunicorn;
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_redirect off;
}
# Django's collected static files, served directly by nginx
location /app/static/ {
alias /home/ubuntu/django/staticfiles/;
access_log off;
expires 30d;
}
# --- WordPress at the domain root ---
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
# Never serve .htaccess or other dotfiles
location ~ /\.(?!well-known).* {
deny all;
}
}Prefer the subdomain layout? It's two clean server blocks and no FORCE_SCRIPT_NAME gymnastics — Django runs at its own root on app.example.com.
# SUBDOMAIN routing — app.example.com -> Django, example.com -> WordPress
upstream django_gunicorn {
server unix:/run/gunicorn.sock;
}
# Django
server {
listen 80;
server_name app.example.com;
location /static/ {
alias /home/ubuntu/django/staticfiles/;
expires 30d;
}
location / {
proxy_pass http://django_gunicorn;
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;
}
}
# WordPress
server {
listen 80;
server_name example.com www.example.com;
root /var/www/wordpress;
index index.php;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}How do you add free HTTPS with Let's Encrypt and certbot?
Don't hand-edit TLS config — let certbot do it. Point it at nginx and list every hostname you serve; it requests a free Let's Encrypt certificate, rewrites your server blocks to listen on 443 with the right cert paths, and inserts an automatic HTTP→HTTPS redirect. Renewal runs unattended via a systemd timer.
# Validate config and reload nginx first
sudo nginx -t && sudo systemctl reload nginx
# Issue + install a free cert for every hostname nginx serves
sudo certbot --nginx -d example.com -d www.example.com -d app.example.com
# certbot adds, to each server block:
# listen 443 ssl;
# ssl_certificate / ssl_certificate_key (managed by certbot)
# and a port-80 block that does: return 301 https://$host$request_uri;
# Renewal is automatic — prove it works:
sudo certbot renew --dry-run
systemctl list-timers | grep certbotHow should you separate the databases and lock down the box?
Give each app its own database engine — it's the cleanest split and matches each framework's strengths:
- Django → PostgreSQL (via
psycopg), listening onlocalhostonly (the defaultlisten_addresses = 'localhost'). - WordPress → MySQL/MariaDB, with
bind-address = 127.0.0.1so it never touches the network.
Neither database should ever be reachable from the internet. Combine that with a tight EC2 security group and a host firewall:
- Inbound 443 (and 80, so certbot and the redirect work) open to the world.
- Inbound 22 (SSH) restricted to your office/VPN IP only — and keep that key safe, because if you lose it you're into recovering EC2 access after losing the PEM file.
- 5432 and 3306 closed to everyone; they're loopback-only.
- Give each app its own DB user with least privilege, take regular automated backups, and patch aggressively — WordPress core and plugins are the single most-attacked surface on the instance, so an outdated plugin can compromise the whole box, Django included.
Single instance vs separated architecture: the honest trade-off
Co-hosting is the right call when budget and simplicity matter more than isolation. But the moment either app gets real traffic, real revenue or real compliance requirements, the shared blast radius becomes the problem — a WordPress plugin exploit, a runaway PHP process or a traffic spike degrades both apps at once.
| Factor | Single EC2 (co-hosted) | Separated (containers / managed WP / PaaS) |
|---|---|---|
| Setup effort | Low — one box, one config | Higher — more moving parts |
| Running overhead | Lowest | Higher |
| Isolation | Shared CPU/RAM/disk, one blast radius | Each app fails independently |
| Scaling | Vertical only (bigger box) | Scale each app on its own |
| Security | One breach can reach both apps | Compromise is contained |
| Best for | Small sites, MVPs, internal tools, budget builds | Business-critical, regulated or high-traffic apps |
When you outgrow one box, the usual next steps are: containerise each app (Docker/ECS), move WordPress to a managed host, scale Django horizontally behind a load balancer, or push it onto a PaaS — see our guide to deploying Django on Elastic Beanstalk.
Frequently Asked Questions
Can I really run Django and WordPress on the same EC2 instance?
Yes. nginx sits in front as a single reverse proxy and routes requests by URL: it proxies Django requests to gunicorn over a Unix socket and passes WordPress's PHP requests to php-fpm. Each app keeps its own runtime and database on the same box, all under one domain and one TLS certificate. It's a proven, low-cost pattern for small sites.
Should I use a subpath or a subdomain for the Django app?
A subdomain (app.example.com) is usually the better choice: Django runs at its own root with no FORCE_SCRIPT_NAME, and cookies and origins are cleanly isolated from WordPress. Use a subpath (example.com/app/) only when you specifically need everything on one hostname — and remember to set Django's script name and prefixed static/cookie paths.
Why gunicorn instead of uWSGI in 2026?
Gunicorn is now the de-facto standard for serving Django WSGI apps: it's simpler to configure, integrates cleanly with systemd sockets, and is actively maintained. uWSGI still works but has fallen out of favour and its development has slowed, so new deployments should default to gunicorn behind nginx.
Do Django and WordPress need separate databases?
Yes — they're different stacks. Run Django on PostgreSQL and WordPress on MySQL/MariaDB, each with its own database, user and least-privilege grants. Bind both engines to 127.0.0.1 so they're never exposed to the internet, and never let the two apps share a schema or credentials.
How do I get HTTPS for both apps for free?
Use certbot with the nginx plugin: sudo certbot --nginx -d example.com -d www.example.com -d app.example.com. It obtains a free Let's Encrypt certificate, configures nginx to listen on 443, and adds an automatic HTTP→HTTPS redirect. A systemd timer renews the certificate automatically; verify it with sudo certbot renew --dry-run.
When should I stop co-hosting and split the apps onto separate servers?
Split them once either app becomes business-critical, handles regulated data, or grows past what one instance comfortably serves. The shared box means a WordPress breach or a traffic spike can take Django down too. At that point, move to containers, a managed WordPress host, or a PaaS, and scale each app independently.
Want this set up cleanly the first time?
Co-hosting Django and WordPress is straightforward to get running and surprisingly easy to get wrong — socket permissions, a PHP location hijacking Django URLs, an exposed database port, or a cookie clash under a sub-path. We've spent 12+ years and 50+ projects shipping and securing exactly these setups on AWS.
If you'd like a hand, explore our AWS consulting services and cloud migration services, or talk to us about ongoing Django development services — from a single co-hosted box to a fully separated, scalable architecture.