Troubleshooting Subdomain Issues With Nginx, Django, And Gunicorn
Hey everyone! Today, we're diving into a common issue that many developers face when setting up subdomains with Nginx, Django, and Gunicorn. It's that frustrating moment when your main domain works perfectly, but your subdomains just don't seem to cooperate. Let's break down what might be happening and how to fix it.
Understanding the Problem
So, you've got a fantastic website running smoothly at cerulinux.com
. Awesome! But now you're trying to add subdomains, like blog.cerulinux.com
or shop.cerulinux.com
. You click on the link, the URL changes to the subdomain, but… nothing seems to load correctly, or worse, it shows the wrong content. Sounds familiar? You're not alone!
Common Symptoms
Before we get into the solutions, let's nail down the typical signs that you're experiencing this issue:
- URL changes, but the content doesn't: You click on a subdomain link, the URL bar updates, but the page content remains the same as the main domain or shows an error.
- Incorrect content: The subdomain loads, but it displays content from the main domain or another subdomain.
- Error messages: You might see errors like "Page Not Found," "Bad Request," or other server-related issues.
Why This Happens
The problem usually stems from misconfigurations in one or more of these areas:
- Nginx Configuration: Nginx acts as a reverse proxy, directing traffic to the appropriate server based on the domain name. If your Nginx configuration isn't set up correctly, it won't know how to handle subdomain requests.
- Django Settings: Django needs to be aware of the subdomains you're using. If you haven't configured Django to recognize the subdomains, it might not serve the correct content.
- Gunicorn Setup: Gunicorn is a WSGI server that serves your Django application. It needs to be set up to handle requests for different subdomains.
- DNS Settings: Your DNS records are like the internet's address book. If they're not pointing your subdomains to the correct server, nothing will work.
Diving Deep: Nginx Configuration
Nginx Configuration is often the first place to check when subdomains aren't working correctly. Nginx acts as a traffic controller, directing incoming requests to the appropriate server or application based on the domain name. A misconfigured Nginx setup can lead to requests for subdomains being routed incorrectly, resulting in the issues we discussed earlier. Let's walk through the key aspects of Nginx configuration for subdomains.
Server Blocks: The Foundation of Subdomain Handling
In Nginx, virtual hosts, or server blocks, are the core mechanism for handling different domains and subdomains. Each server block defines how Nginx should handle requests for a specific domain or subdomain. These blocks are typically located in the nginx.conf
file or in separate configuration files within the /etc/nginx/sites-available/
directory.
Here's a basic example of an Nginx server block for a subdomain:
server {
listen 80; # Or 443 for HTTPS
server_name blog.cerulinux.com;
# ... other configurations ...
}
In this snippet, listen 80
tells Nginx to listen for HTTP traffic on port 80 (the standard port for HTTP), and server_name blog.cerulinux.com
specifies that this block should handle requests for the blog.cerulinux.com
subdomain. If you're using HTTPS, you'd listen on port 443 and configure SSL certificates.
Common Configuration Mistakes
Several common mistakes can cause subdomain issues in Nginx configurations:
- Missing Server Blocks: The most straightforward mistake is simply forgetting to create a server block for the subdomain. If Nginx doesn't have a specific block for
blog.cerulinux.com
, it won't know how to handle requests for it. - Incorrect
server_name
Directive: Typos or incorrect domain names in theserver_name
directive are surprisingly common. Double-check that the domain and subdomain names are spelled correctly and match your DNS settings. - Overlapping Server Blocks: If you have multiple server blocks with overlapping
server_name
directives, Nginx might route traffic to the wrong block. Ensure that each subdomain has a uniqueserver_name
. - Incorrect Root Directive: The
root
directive specifies the directory from which Nginx serves files. If this is misconfigured, Nginx might not be able to find the correct files for your subdomain. - Proxy Pass Misconfiguration: If you're using Nginx as a reverse proxy (which is common with Django and Gunicorn), the
proxy_pass
directive tells Nginx where to forward requests. An incorrectproxy_pass
can lead to requests being sent to the wrong application server.
Example Configuration for Subdomains
Let's look at a more complete example of how to configure Nginx for subdomains with Django and Gunicorn. Suppose you have two subdomains: blog.cerulinux.com
and shop.cerulinux.com
. Your Nginx configuration might look like this:
# Server block for the main domain
server {
listen 80;
server_name cerulinux.com www.cerulinux.com;
location / {
proxy_pass http://127.0.0.1:8000; # Gunicorn server
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# ... other proxy settings ...
}
}
# Server block for the blog subdomain
server {
listen 80;
server_name blog.cerulinux.com;
location / {
proxy_pass http://127.0.0.1:8001; # Different Gunicorn server or port
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# ... other proxy settings ...
}
}
# Server block for the shop subdomain
server {
listen 80;
server_name shop.cerulinux.com;
location / {
proxy_pass http://127.0.0.1:8002; # Another Gunicorn server or port
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# ... other proxy settings ...
}
}
In this example, each subdomain has its own server block, listening on port 80. The proxy_pass
directive forwards requests to different Gunicorn servers or ports, allowing each subdomain to run its own application. The proxy_set_header
directives ensure that the correct host and IP information are passed to the backend server.
Debugging Nginx Configuration
If you're having trouble with your Nginx configuration, here are some debugging tips:
- Check the Nginx Error Logs: The error logs (usually located at
/var/log/nginx/error.log
) can provide valuable clues about what's going wrong. Look for messages related to configuration errors, file access issues, or proxy connection problems. - Test the Configuration: Use the
nginx -t
command to test your configuration for syntax errors before restarting Nginx. This can help you catch problems early. - Simplify the Configuration: If you have a complex configuration, try simplifying it to isolate the issue. For example, you can comment out server blocks for other subdomains and focus on getting one subdomain working correctly.
- Use Verbose Logging: Temporarily enable verbose logging in Nginx to get more detailed information about request processing. This can help you track down routing issues.
Best Practices for Nginx Configuration
To avoid subdomain issues in the future, follow these best practices for Nginx configuration:
- Keep Configurations Organized: Use separate configuration files for each domain and subdomain. This makes it easier to manage and troubleshoot your setup.
- Use Consistent Naming Conventions: Adopt a consistent naming convention for your server blocks and configuration files. This will help you stay organized and avoid confusion.
- Regularly Review Configurations: Periodically review your Nginx configurations to ensure they're up-to-date and optimized for your needs.
- Use Configuration Management Tools: Consider using configuration management tools like Ansible or Chef to automate the deployment and management of your Nginx configurations.
Django Settings: Making Your App Subdomain-Aware
Next up, let's talk about Django Settings. Even if your Nginx configuration is spot-on, Django needs to know how to handle requests for different subdomains. This involves configuring your Django settings to recognize and serve content for each subdomain correctly. If Django isn't properly configured, it might serve the same content for all subdomains or throw errors. Let's dive into how to make your Django app subdomain-aware.
The ALLOWED_HOSTS
Setting
The ALLOWED_HOSTS
setting in your Django settings.py
file is crucial for security. It tells Django which hostnames are allowed to serve your application. If a request comes in for a hostname that's not in ALLOWED_HOSTS
, Django will raise a SuspiciousOperation
exception. This prevents potential security vulnerabilities like HTTP Host header attacks.
To support subdomains, you need to include them in the ALLOWED_HOSTS
setting. Here's an example:
ALLOWED_HOSTS = ['cerulinux.com', 'www.cerulinux.com', 'blog.cerulinux.com', 'shop.cerulinux.com']
In this example, Django will serve requests for the main domain (cerulinux.com
and www.cerulinux.com
) as well as the blog.cerulinux.com
and shop.cerulinux.com
subdomains.
Common Mistakes with ALLOWED_HOSTS
Several common mistakes can cause issues with the ALLOWED_HOSTS
setting:
- Forgetting to Add Subdomains: The most common mistake is simply forgetting to add the subdomains to
ALLOWED_HOSTS
. If you add a new subdomain, make sure to update this setting. - Typos: Typos in the hostnames are surprisingly common. Double-check that the domain and subdomain names are spelled correctly.
- Wildcard Usage: While you can use a wildcard (
*
) to allow all subdomains, this is generally not recommended for production environments due to security concerns. It's better to explicitly list the allowed subdomains. - Incorrect Environment Variables: If you're using environment variables to configure
ALLOWED_HOSTS
, make sure the variables are set correctly in your deployment environment.
Dynamic Subdomain Handling
In some cases, you might want to handle subdomains dynamically. For example, you might want to create a new subdomain for each user or organization. In these scenarios, you can't simply list all possible subdomains in ALLOWED_HOSTS
.
One approach is to use middleware to dynamically check the hostname and add it to ALLOWED_HOSTS
if it meets certain criteria. Here's an example of middleware that does this:
from django.conf import settings
class SubdomainMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
host = request.get_host().split(':')[0] # Remove port if present
if host not in settings.ALLOWED_HOSTS and self.is_valid_subdomain(host):
settings.ALLOWED_HOSTS.append(host)
response = self.get_response(request)
return response
def is_valid_subdomain(self, host):
# Add your logic to validate the subdomain
# For example, check if it matches a specific pattern
return host.endswith('.cerulinux.com')
This middleware checks if the hostname is in ALLOWED_HOSTS
. If not, it calls the is_valid_subdomain
method to validate the subdomain. If the subdomain is valid, it's added to ALLOWED_HOSTS
. You'll need to add your own logic to the is_valid_subdomain
method to determine which subdomains are allowed.
Using Subdomains in URLs and Views
Once Django is aware of your subdomains, you can start using them in your URLs and views. There are several ways to do this:
- Subdomain-Specific URL Patterns: You can define different URL patterns for each subdomain by using different URL configurations or by using middleware to modify the URLconf based on the subdomain.
- Context Processors: You can use context processors to make the current subdomain available in your templates. This allows you to dynamically generate links and content based on the subdomain.
- Custom Template Tags: You can create custom template tags to generate URLs that include the subdomain.
Example: Subdomain-Specific Views
Let's look at an example of how to define different views for different subdomains. Suppose you want to have a blog view for blog.cerulinux.com
and a shop view for shop.cerulinux.com
. You can achieve this by creating separate URL configurations for each subdomain.
First, create a urls.py
file for each subdomain:
# blog/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.blog_view, name='blog'),
]
# shop/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.shop_view, name='shop'),
]
Then, in your main urls.py
file, include these URL configurations based on the subdomain:
# main/urls.py
from django.urls import path, include
from django.conf import settings
urlpatterns = [
# ... other URL patterns ...
]
if settings.DEBUG:
from django.urls import re_path
from django.views.static import serve
urlpatterns += [
re_path(r'^media/(?P<path>.*){{content}}#39;, serve, {
'document_root': settings.MEDIA_ROOT,
}),
]
def subdomain_urls():
from django.urls import reverse
def reverse_for_subdomain(subdomain, viewname, kwargs=None):
url = reverse(viewname, kwargs=kwargs)
if settings.DEBUG:
return f'//{subdomain}:8000{url}'
else:
return f'//{subdomain}{url}'
return reverse_for_subdomain
from django.urls import URLPattern, URLResolver
def _is_urlresolver(url): return isinstance(url, URLResolver)
def _is_urlpattern(url): return isinstance(url, URLPattern)
class SubdomainURLRouter:
def __init__(self, parent_router, subdomain)
self.parent_router = parent_router
self.subdomain = subdomain
self.compiled_urlpatterns = list()
@classmethod
def root(cls, subdomain):
return cls(None, subdomain)
def register(self, view, regex, name=None):
self.parent_router.register(self, view, regex, name)
return self
def compile(self):
self.compiled_urlpatterns = list()
for compiled_url in self.parent_router.compiled_urlpatterns:
self._apply_subdomain_to_compiled_url(compiled_url)
return self
def _apply_subdomain_to_compiled_url(self, compiled_url):
if _is_urlpattern(compiled_url):
self.compiled_urlpatterns.append(self._apply_subdomain_to_pattern(compiled_url))
elif _is_urlresolver(compiled_url):
self.compiled_urlpatterns.append(self._apply_subdomain_to_resolver(compiled_url))
def _apply_subdomain_to_pattern(self, compiled_url):
regex = r'^{}{}{{content}}#39;.format(self.subdomain_prefix, compiled_url.pattern.regex.pattern)
return path(compiled_url.pattern.regex.pattern, compiled_url.callback, name=compiled_url.name)
def _apply_subdomain_to_resolver(self, compiled_url):
# todo implement resolver processing
# compiled_url.url_patterns
pass
@property
def subdomain_prefix(self):
return r'' if self.subdomain is None else r'{}\.'.format(self.subdomain)
class MainURLRouter:
def __init__(self):
self.registered_urls = list()
self.compiled_urlpatterns = list()
def register(self, router, view, regex, name=None):
self.registered_urls.append((router, path(regex, view, name=name)))
return self
def compile(self):
self.compiled_urlpatterns = list()
for router, url in self.registered_urls:
self.compiled_urlpatterns.append(url)
return self
main_router = MainURLRouter()
def register_url(router, view, regex, name=None):
main_router.register(router, view, regex, name)
def compile_urls():
return main_router.compile()
def include_subdomain_urls(subdomain, urlpatterns):
subdomain_router = SubdomainURLRouter.root(subdomain)
urlpatterns(register_url=subdomain_router.register)
subdomain_router.compile()
return subdomain_router.compiled_urlpatterns
urlpatterns += include_subdomain_urls('blog', lambda register_url: [
register_url(None, lambda req: HttpResponse('blog'), 'blog-page')
])
urlpatterns += include_subdomain_urls('shop', lambda register_url: [
register_url(None, lambda req: HttpResponse('shop'), 'shop-page')
])
urlpatterns += main_router.compile().urlpatterns
In this setup, the main urls.py
file includes the subdomain-specific URL configurations based on the hostname. When a request comes in for blog.cerulinux.com
, Django will use the URL patterns defined in blog/urls.py
, and similarly for shop.cerulinux.com
.
Best Practices for Django Subdomain Settings
To ensure your Django app handles subdomains correctly, follow these best practices:
- Always Include Subdomains in
ALLOWED_HOSTS
: Make sure to add all your subdomains to theALLOWED_HOSTS
setting to prevent security issues. - Use Dynamic Handling When Necessary: If you need to handle subdomains dynamically, use middleware or other techniques to validate and add them to
ALLOWED_HOSTS
. - Organize URL Configurations: Keep your URL configurations organized by creating separate files for each subdomain or using URL routers.
- Use Context Processors for Template Access: Use context processors to make the current subdomain available in your templates, making it easier to generate subdomain-specific content.
Gunicorn Setup: Serving Subdomains with Multiple Workers
Let's move on to Gunicorn Setup. Gunicorn is a popular WSGI server used to serve Django applications. When dealing with subdomains, it's important to configure Gunicorn to handle requests for multiple subdomains efficiently. This often involves running multiple Gunicorn workers or instances, each serving a different subdomain. Let's explore how to set up Gunicorn for subdomains.
Why Multiple Workers?
Before we dive into the configuration, let's understand why running multiple Gunicorn workers is important for subdomains. Gunicorn uses a pre-fork worker model, which means it spawns multiple worker processes to handle incoming requests. Each worker can handle one request at a time. If you have a single worker, it can become a bottleneck if you have a lot of traffic to your subdomains.
By running multiple workers, you can handle more concurrent requests, improving the performance and scalability of your application. For subdomains, you might want to dedicate specific workers or instances to each subdomain to ensure that requests are handled efficiently.
Running Multiple Gunicorn Instances
One way to serve subdomains with Gunicorn is to run multiple instances, each bound to a different port or socket. This allows you to configure Nginx to proxy requests to the appropriate instance based on the subdomain. Here's an example of how to do this:
- Create Separate Systemd Units: If you're using Systemd to manage Gunicorn, create separate unit files for each instance. For example, you might have
gunicorn-cerulinux.com.service
,gunicorn-blog.cerulinux.com.service
, andgunicorn-shop.cerulinux.com.service
. - Configure Each Instance: In each unit file, specify the appropriate settings for the subdomain. This includes the Gunicorn command, the port or socket to bind to, and any other relevant options.
Here's an example of a Systemd unit file for the blog.cerulinux.com
subdomain:
# /etc/systemd/system/gunicorn-blog.cerulinux.com.service
[Unit]
Description=Gunicorn server for blog.cerulinux.com
After=network.target
[Service]
User=youruser
Group=www-data
WorkingDirectory=/path/to/your/django/project
ExecStart=/path/to/your/virtualenv/bin/gunicorn --workers 3 --bind 127.0.0.1:8001 yourproject.wsgi:application
[Install]
WantedBy=multi-user.target
In this example, Gunicorn is configured to run with 3 workers and bind to port 8001 on the loopback interface (127.0.0.1
). You would create similar unit files for the other subdomains, binding them to different ports (e.g., 8002 for shop.cerulinux.com
).
- Configure Nginx: In your Nginx configuration, you would then proxy requests for each subdomain to the appropriate Gunicorn instance based on the port. We covered this in the Nginx configuration section.
Using a Single Gunicorn Instance with Multiple Workers
Another approach is to run a single Gunicorn instance with multiple workers and use Django middleware to route requests to the appropriate application based on the subdomain. This can be simpler to manage than running multiple instances, but it might not provide the same level of isolation and scalability.
- Configure Gunicorn: In this case, you would configure Gunicorn to run with multiple workers, but you wouldn't need to bind it to specific ports for each subdomain.
- Use Django Middleware: You would then use Django middleware to inspect the hostname and route the request to the appropriate application. This involves creating middleware that checks the subdomain and sets the appropriate URLconf or view based on the subdomain.
Common Gunicorn Configuration Issues
Several common issues can arise when configuring Gunicorn for subdomains:
- Port Conflicts: If you're running multiple Gunicorn instances, make sure they're bound to different ports to avoid conflicts.
- Insufficient Workers: If you don't have enough workers, your application might not be able to handle the traffic to your subdomains. Monitor your server's performance and adjust the number of workers as needed.
- File Permissions: Ensure that the Gunicorn user has the necessary permissions to access your Django project and any other required files.
- Gunicorn Not Running: Double-check that Gunicorn is running and that the Systemd units are enabled and started.
Best Practices for Gunicorn Subdomain Setup
To ensure your Gunicorn setup is optimized for subdomains, follow these best practices:
- Use Multiple Workers: Run multiple Gunicorn workers to handle concurrent requests and improve performance.
- Consider Multiple Instances: If you need strong isolation or have high traffic to your subdomains, consider running multiple Gunicorn instances.
- Monitor Performance: Monitor your server's performance and adjust the number of workers and instances as needed.
- Use Systemd: Use Systemd to manage your Gunicorn processes, making it easier to start, stop, and restart them.
DNS Settings: Pointing Subdomains to Your Server
Last but definitely not least, let's discuss DNS Settings. No matter how perfectly you've configured Nginx, Django, and Gunicorn, your subdomains won't work if your DNS records aren't set up correctly. DNS records are like the internet's address book, mapping domain names and subdomains to IP addresses. If your DNS records are pointing to the wrong place, requests for your subdomains won't reach your server. Let's walk through how to configure DNS settings for subdomains.
Understanding DNS Records
Before we dive into the specifics, let's quickly review the key DNS record types that are relevant to subdomains:
- A Records: A records map a domain name or subdomain to an IPv4 address. This is the most common type of DNS record for subdomains.
- AAAA Records: AAAA records are similar to A records, but they map a domain name or subdomain to an IPv6 address.
- CNAME Records: CNAME records create an alias for a domain name or subdomain. They point one domain name to another, rather than directly to an IP address.
Configuring A Records for Subdomains
For most subdomain setups, you'll use A records to point your subdomains to your server's IP address. Here's how to do it:
- Log in to Your DNS Provider: Log in to the control panel for your DNS provider (e.g., GoDaddy, Namecheap, Cloudflare).
- Navigate to DNS Settings: Find the DNS settings or DNS management section.
- Add A Records: Add A records for each of your subdomains, pointing them to your server's IP address.
Here's an example of what the A records might look like:
Name Type Value
-------------------- ------ ----------------
blog A 192.0.2.1
shop A 192.0.2.1
In this example, blog
and shop
are the subdomain names, and 192.0.2.1
is your server's IP address. You would replace 192.0.2.1
with your actual IP address.
Using CNAME Records
In some cases, you might use CNAME records for subdomains. For example, if you're using a content delivery network (CDN) or a third-party service that provides a domain name, you might create a CNAME record that points your subdomain to the service's domain name.
Here's an example of a CNAME record:
Name Type Value
-------------------- ------ ----------------
cdn CNAME example.cloudfront.net
In this example, the cdn
subdomain points to example.cloudfront.net
, which is a domain name provided by a CDN service.
Common DNS Configuration Mistakes
Several common mistakes can cause issues with DNS settings for subdomains:
- Incorrect IP Address: The most common mistake is pointing the subdomain to the wrong IP address. Double-check that the IP address in your A record matches your server's IP address.
- Missing Records: Forgetting to add A records for your subdomains is another common mistake. Make sure you create A records for all your subdomains.
- Conflicting Records: If you have conflicting DNS records, such as an A record and a CNAME record for the same subdomain, it can cause issues. Make sure your records are consistent and don't conflict with each other.
- DNS Propagation: DNS changes can take some time to propagate across the internet. It might take up to 48 hours for your changes to take effect. Be patient and wait for the propagation to complete before troubleshooting further.
Troubleshooting DNS Issues
If you're having trouble with your DNS settings, here are some troubleshooting tips:
- Use DNS Lookup Tools: Use online DNS lookup tools (e.g.,
dig
,nslookup
, online DNS checkers) to verify that your DNS records are configured correctly. These tools can show you the A records, CNAME records, and other DNS information for your subdomains. - Check DNS Propagation: Use DNS propagation checkers to see if your DNS changes have propagated to different DNS servers around the world. This can help you determine if the issue is due to propagation delay.
- Clear DNS Cache: Your local DNS cache might be storing old DNS records. Try clearing your DNS cache to force your system to fetch the latest records.
Best Practices for DNS Subdomain Setup
To ensure your DNS settings are optimized for subdomains, follow these best practices:
- Use A Records for Subdomains: In most cases, use A records to point your subdomains to your server's IP address.
- Double-Check IP Addresses: Always double-check that the IP addresses in your A records are correct.
- Be Patient with Propagation: DNS changes can take time to propagate. Be patient and wait for the propagation to complete before troubleshooting.
- Use DNS Lookup Tools: Use DNS lookup tools to verify your DNS records and troubleshoot issues.
Wrapping Up
Setting up subdomains can be tricky, but by carefully configuring Nginx, Django, Gunicorn, and your DNS settings, you can get everything working smoothly. Remember to double-check your configurations, use debugging tools when needed, and follow best practices to avoid common pitfalls. You've got this! If you have any questions or run into further issues, feel free to ask in the comments below. Happy subdomaining!