<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Navdeep Rana</title>
    <description>The latest articles on DEV Community by Navdeep Rana (@app_dev_b139d1a58b00b6516).</description>
    <link>https://dev.to/app_dev_b139d1a58b00b6516</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2870047%2Fad160f8c-7fe0-4ea8-947d-86faa822e8eb.png</url>
      <title>DEV Community: Navdeep Rana</title>
      <link>https://dev.to/app_dev_b139d1a58b00b6516</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/app_dev_b139d1a58b00b6516"/>
    <language>en</language>
    <item>
      <title>Scale Django on Seenode: Production Architecture That Actually Works</title>
      <dc:creator>Navdeep Rana</dc:creator>
      <pubDate>Sun, 16 Nov 2025 13:08:36 +0000</pubDate>
      <link>https://dev.to/app_dev_b139d1a58b00b6516/scale-django-on-seenode-production-architecture-that-actually-works-2bkk</link>
      <guid>https://dev.to/app_dev_b139d1a58b00b6516/scale-django-on-seenode-production-architecture-that-actually-works-2bkk</guid>
      <description>&lt;p&gt;&lt;em&gt;Advanced scaling patterns for Django apps - from 100 to 100K requests/day&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Last year I pushed 18 app versions with staging endpoints hardcoded into production. Residents across the UK couldn't unlock their hotel rooms via NFC. My PM couldn't log in. That was a frontend disaster, but it taught me something: deployment F-ups scale with complexity.&lt;/p&gt;

&lt;p&gt;Django backend scaling is the same energy - you can't just throw more Gunicorn workers at it and pray. I learned this the hard way when a payments API I built started choking at 2,000 req/min. Tea break interrupted at 11am by a Slack ping: "Checkout timing out, users complaining." I ssh'd in, watched workers dying one by one, and realized our monolith was the problem.&lt;/p&gt;

&lt;p&gt;This article covers what actually worked when I migrated that payments system to Seenode and scaled it to 50K+ requests/day without melting down. No theory, just the architecture patterns that kept me sleeping through the night.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You'll learn:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why splitting web/worker services matters (and how to do it on Seenode)&lt;/li&gt;
&lt;li&gt;Database connection pooling that actually prevents "too many connections" errors
&lt;/li&gt;
&lt;li&gt;Redis caching patterns worth implementing&lt;/li&gt;
&lt;li&gt;Real cost considerations when scaling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Django app deployed on Seenode (&lt;a href="https://dev.to/app_dev_b139d1a58b00b6516/advanced-django-deployment-on-seenode-production-ready-strategies-for-2025-5bcm"&gt;see my production deployment guide&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Basic understanding of Django, PostgreSQL, Celery&lt;/li&gt;
&lt;li&gt;Seenode account (sign up at cloud.seenode.com with a 7-day free trial)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Read time:&lt;/strong&gt; 10 minutes&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Level:&lt;/strong&gt; Advanced (assumes you've deployed Django before)&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Monoliths Break at Scale
&lt;/h2&gt;

&lt;p&gt;Most Django apps start simple: one Gunicorn process handling HTTP requests and background tasks via &lt;code&gt;@shared_task&lt;/code&gt; decorators. This works great until it doesn't.&lt;/p&gt;

&lt;p&gt;The breaking point? When a slow task (PDF generation, sending emails) ties up a worker and your API requests start queuing. Or when 20 Gunicorn workers x 5 DB connections hits PostgreSQL's 100-connection limit. I've debugged both, multiple times.&lt;/p&gt;

&lt;p&gt;The fix isn't bigger servers. It's splitting web and worker services so they scale independently. Seenode makes this easy—same Git repo, different start commands.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Multi-Service Architecture
&lt;/h2&gt;

&lt;p&gt;Here's how I structure Django apps on Seenode—three services, one Git repo:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Web Service&lt;/strong&gt; (Gunicorn) - handles HTTP only, scales horizontally&lt;br&gt;&lt;br&gt;
&lt;strong&gt;2. Worker Service&lt;/strong&gt; (Celery) - background jobs, scales for throughput&lt;br&gt;&lt;br&gt;
&lt;strong&gt;3. Scheduler Service&lt;/strong&gt; (Celery Beat) - cron jobs, runs once  &lt;/p&gt;

&lt;p&gt;Plus PostgreSQL (managed by Seenode) and Redis (external provider required).&lt;/p&gt;

&lt;p&gt;Each service uses the same codebase but different start commands. Seenode's dashboard lets you configure all three pointing to the same GitHub repo—just change the startup script.&lt;/p&gt;
&lt;h3&gt;
  
  
  Service 1: Web Service (Gunicorn)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgfp6ofrll246h46vn5ij.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgfp6ofrll246h46vn5ij.png" alt="Seenode Web Service Configuration" width="800" height="702"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Seenode web service configuration showing Gunicorn start command with workers and threads. The $PORT variable is automatically provided by Seenode.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your Seenode web service start command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gunicorn config.wsgi:application &lt;span class="nt"&gt;--bind&lt;/span&gt; 0.0.0.0:&lt;span class="nv"&gt;$PORT&lt;/span&gt; &lt;span class="nt"&gt;--workers&lt;/span&gt; 4 &lt;span class="nt"&gt;--threads&lt;/span&gt; 2 &lt;span class="nt"&gt;--timeout&lt;/span&gt; 30 &lt;span class="nt"&gt;--max-requests&lt;/span&gt; 1000 &lt;span class="nt"&gt;--max-requests-jitter&lt;/span&gt; 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key settings explained:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--workers 4&lt;/code&gt;&lt;/strong&gt; - Use &lt;code&gt;(2 x CPU) + 1&lt;/code&gt;. More workers != better performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--threads 2&lt;/code&gt;&lt;/strong&gt; - Handles concurrent DB queries without exploding connection count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--max-requests 1000&lt;/code&gt;&lt;/strong&gt; - Restarts workers to prevent memory leaks. Learned this after watching memory climb to 95% over 3 days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--timeout 30&lt;/code&gt;&lt;/strong&gt; - Kills hung requests. Saved me when a payment provider's API started timing out.&lt;/p&gt;

&lt;p&gt;On a properly configured instance, this handles 2,000 req/min with p95 response time around 180ms.&lt;/p&gt;

&lt;p&gt;I tried running 8 workers on a smaller instance once. Thought more workers = better performance. Nope. Hit OOM errors within 2 hours. Turns out the formula &lt;code&gt;(2 x CPU) + 1&lt;/code&gt; exists for a reason.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fys28ba85kp4q90mg0m4h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fys28ba85kp4q90mg0m4h.png" alt="Environment Variables" width="800" height="305"&gt;&lt;/a&gt;&lt;br&gt;
Set these environment variables in Seenode's dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GUNICORN_WORKERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4
&lt;span class="nv"&gt;GUNICORN_THREADS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Service 2: Celery Workers (Background Jobs)
&lt;/h3&gt;

&lt;p&gt;Seenode worker service start command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;celery &lt;span class="nt"&gt;-A&lt;/span&gt; config worker &lt;span class="nt"&gt;--loglevel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;info &lt;span class="nt"&gt;--concurrency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="nt"&gt;--max-tasks-per-child&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000 &lt;span class="nt"&gt;--time-limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Celery config (&lt;code&gt;config/celery.py&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Celery&lt;/span&gt;

&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DJANGO_SETTINGS_MODULE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;config.settings&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Celery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;myproject&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config_from_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;django.conf:settings&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CELERY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;autodiscover_tasks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;broker_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;REDIS_URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;redis://localhost:6379/0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;result_backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;REDIS_URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;task_serializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;task_time_limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# 5 min hard limit (prevent stuck tasks)
&lt;/span&gt;    &lt;span class="n"&gt;task_acks_late&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Only ack after completion
&lt;/span&gt;    &lt;span class="n"&gt;worker_prefetch_multiplier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# One task at a time for long operations
&lt;/span&gt;    &lt;span class="n"&gt;result_expires&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Results expire after 1 hour
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# myapp/tasks.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;shared_task&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.core.mail&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;send_mail&lt;/span&gt;

&lt;span class="nd"&gt;@shared_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_retry_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_welcome_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;myapp.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;send_mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Welcome!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Hi &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;from_email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;noreply@myapp.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;recipient_list&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call from views:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Queue background task (returns immediately)
&lt;/span&gt;&lt;span class="n"&gt;send_welcome_email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I started with &lt;code&gt;--concurrency=4&lt;/code&gt;. When queue backed up during Black Friday, scaled to 5 worker instances via Seenode's dashboard. Took 30 seconds, queue cleared in 2 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Service 3: Celery Beat (Scheduler)
&lt;/h3&gt;

&lt;p&gt;Run periodic tasks on a schedule. Start command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;celery &lt;span class="nt"&gt;-A&lt;/span&gt; config beat &lt;span class="nt"&gt;--loglevel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;info &lt;span class="nt"&gt;--scheduler&lt;/span&gt; django_celery_beat.schedulers:DatabaseScheduler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install &lt;code&gt;django-celery-beat&lt;/code&gt;, add to &lt;code&gt;INSTALLED_APPS&lt;/code&gt;, run migrations. Define schedules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/celery.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery.schedules&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;crontab&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;beat_schedule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cleanup-sessions-daily&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;myapp.tasks.cleanup_old_sessions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;schedule&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;crontab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# 2 AM daily
&lt;/span&gt;    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;check-payment-status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;myapp.tasks.check_payment_status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;schedule&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;300.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Every 5 min
&lt;/span&gt;    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or manage via Django admin—no code deployments needed to change schedules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Critical:&lt;/strong&gt; Run ONLY ONE Beat instance. Multiple schedulers = duplicate tasks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Horizontal Scaling &amp;amp; Database Connection Pooling
&lt;/h2&gt;

&lt;p&gt;Seenode automatically handles load balancing and scaling. But your app needs to be &lt;strong&gt;stateless&lt;/strong&gt;—no file uploads on local disk, no in-memory sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis Sessions (Required for Multi-Instance)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
# pip install django-redis
&lt;/span&gt;
&lt;span class="n"&gt;CACHES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;BACKEND&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;django_redis.cache.RedisCache&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;LOCATION&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;REDIS_URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;redis://127.0.0.1:6379/1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OPTIONS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CLIENT_CLASS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;django_redis.client.DefaultClient&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CONNECTION_POOL_KWARGS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;max_connections&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;KEY_PREFIX&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;TIMEOUT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;SESSION_ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;django.contrib.sessions.backends.cache&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;SESSION_CACHE_ALIAS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;REDIS_URL&lt;/code&gt; in Seenode environment variables. You'll need to use an external Redis provider (Seenode does not offer managed Redis).&lt;/p&gt;

&lt;h3&gt;
  
  
  Media Files on S3
&lt;/h3&gt;

&lt;p&gt;File uploads disappear when you scale or redeploy. Use S3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install django-storages boto3
&lt;/span&gt;&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;storages&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;DEFAULT_FILE_STORAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;storages.backends.s3boto3.S3Boto3Storage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="n"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;AWS_STORAGE_BUCKET_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AWS_STORAGE_BUCKET_NAME&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Database Connection Pooling (Critical)
&lt;/h3&gt;

&lt;p&gt;With multiple instances, connections explode:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; 3 web instances x 4 workers x 5 connections = 60. Add 2 Celery services and you hit PostgreSQL's 100-connection limit. This is the math that breaks things.&lt;/p&gt;

&lt;p&gt;I once spent a Saturday debugging "too many connections" errors. Math was simple: 6 services x 5 connections = 30, but &lt;code&gt;max_connections&lt;/code&gt; was set to 20. Increased to 50, problem solved.&lt;/p&gt;

&lt;p&gt;Enable connection pooling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
&lt;/span&gt;&lt;span class="n"&gt;DATABASES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ENGINE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;django.db.backends.postgresql&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CONN_MAX_AGE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Keep connections alive 10 minutes
&lt;/span&gt;        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OPTIONS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;connect_timeout&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-c statement_timeout=30000&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# For Celery workers, close connections immediately
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CELERY_WORKER&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;DATABASES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CONN_MAX_AGE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check Seenode's &lt;a href="https://docs.seenode.com/databases/postgresql" rel="noopener noreferrer"&gt;PostgreSQL docs&lt;/a&gt; for PgBouncer if needed for higher connection counts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Caching: The Cheap Performance Win
&lt;/h2&gt;

&lt;p&gt;Database queries kill performance. Caching is the easiest fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  View-Level Caching
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.views.decorators.cache&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cache_page&lt;/span&gt;

&lt;span class="nd"&gt;@cache_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 5 minutes
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;product_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;products/list.html&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;products&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Query-Level Caching
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.core.cache&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_popular_products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cache_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;popular_products_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;annotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;order_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;order_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-order_count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 10 min
&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cache Invalidation
&lt;/h3&gt;

&lt;p&gt;First time I implemented caching, I forgot to invalidate on updates. Users saw prices from 3 days ago. Customer support got hammered. The fix? One &lt;code&gt;cache.delete()&lt;/code&gt; call. Cost of mistake: $2,000 in lost sales. Cost of fix: 2 lines of code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# myapp/models.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.db.models.signals&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;post_save&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_delete&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.dispatch&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;receiver&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.core.cache&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;

&lt;span class="nd"&gt;@receiver&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;post_save&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_delete&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invalidate_product_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;product_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;popular_products_10&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Performance Monitoring
&lt;/h2&gt;

&lt;p&gt;Seenode has built-in metrics, but you need to know what broke.&lt;/p&gt;

&lt;h3&gt;
  
  
  Django Debug Toolbar (Dev Only)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;debug_toolbar&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;MIDDLEWARE&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;debug_toolbar.middleware.DebugToolbarMiddleware&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shows SQL queries, cache hits/misses. Use it to find N+1 query problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Seenode Metrics (What to Watch)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU &amp;gt;80%&lt;/strong&gt; sustained = scale up or optimize&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory &amp;gt;85%&lt;/strong&gt; = memory leak or need more RAM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response time p95 &amp;gt;500ms&lt;/strong&gt; = investigate slow endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I keep the Seenode metrics tab pinned. Saw CPU spike to 95% every day at 2pm. Took a week to realize it was daily report generation. Moved to dedicated worker, CPU dropped to 40%.&lt;/p&gt;




&lt;h2&gt;
  
  
  Database Optimization
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fix N+1 Queries
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# BAD: N+1 query problem
&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Separate query each time!
&lt;/span&gt;
&lt;span class="c1"&gt;# GOOD: Use select_related for foreign keys
&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select_related&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# GOOD: Use prefetch_related for many-to-many
&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;prefetch_related&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add Database Indexes
&lt;/h3&gt;

&lt;p&gt;Add indexes on frequently queried fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;indexes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-created_at&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Cost Management
&lt;/h2&gt;

&lt;p&gt;Scaling costs increased faster than expected. What kept costs reasonable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start small, scale on metrics&lt;/strong&gt; - Don't over-provision. I started with 1 web instance on the Basic tier ($3/mo), scaled up when CPU hit 85%. Seenode offers four pricing tiers: Basic ($3/mo, 0.5 GB RAM, 0.2 CPU), Standard ($6/mo, 1 GB RAM, 0.5 CPU), Pro ($12/mo, 2 GB RAM, 1.0 CPU), and Ultra ($24/mo, 4 GB RAM, 2.0 CPU). You can upgrade or downgrade anytime based on your needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimize before scaling&lt;/strong&gt; - Profile with Debug Toolbar, fix N+1 queries, add indexes, enable caching. Then scale if needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scale workers independently&lt;/strong&gt; - Workers are cheaper than web instances. Add worker capacity without scaling entire web service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching ROI&lt;/strong&gt; - Caching saved ~$100/mo in database costs by reducing query load 60%.&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting (The Usual Suspects)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "Too Many Database Connections"
&lt;/h3&gt;

&lt;p&gt;Simple math problem: 6 services x 5 connections = 30, but &lt;code&gt;max_connections&lt;/code&gt; was 20. Fix: Increase &lt;code&gt;max_connections&lt;/code&gt; or use PgBouncer. For Celery workers, close connections immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DATABASES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CONN_MAX_AGE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CELERY_WORKER&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Redis Out of Memory
&lt;/h3&gt;

&lt;p&gt;Set TTLs on everything. Celery results don't need to live forever: &lt;code&gt;CELERY_RESULT_EXPIRES = 3600&lt;/code&gt; (1 hour, not 24h).&lt;/p&gt;

&lt;h3&gt;
  
  
  Celery Queue Backing Up
&lt;/h3&gt;

&lt;p&gt;Workers can't keep up. Add more worker instances or route slow tasks to separate queues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;CELERY_TASK_ROUTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;myapp.tasks.slow_report&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;queue&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;slow&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;myapp.tasks.fast_email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;queue&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fast&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Things I Wish I Knew About Scaling on Seenode
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Connection pooling isn't optional—learned this at 3am when PostgreSQL started rejecting connections during a sale. You'll hit the limit fast.&lt;/li&gt;
&lt;li&gt;Celery workers need separate services, not the web service. Tried running both in one process. Don't.&lt;/li&gt;
&lt;li&gt;Redis is cheap insurance ($10/mo saves $100/mo in DB costs). Skip it and you'll pay later.&lt;/li&gt;
&lt;li&gt;Profile before scaling—I wasted $200/mo on instances when the fix was a missing index. Debug Toolbar is your friend.&lt;/li&gt;
&lt;li&gt;Seenode logs rotate fast. Ship to external storage or lose debugging info when you need it most.&lt;/li&gt;
&lt;li&gt;Scaling is automatic but can be addictive. Scale on metrics, not fear. I've over-provisioned more times than I'll admit.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Scaling Django on Seenode isn't about manual scaling controls. It's architecture: split services (web/worker/beat), cache aggressively, fix N+1 queries, enable connection pooling.&lt;/p&gt;

&lt;p&gt;This multi-service pattern works. I've used it to scale from 2K to 50K+ requests/day without melting down. It's boring, proven, and lets you sleep.&lt;/p&gt;

&lt;p&gt;If you're ssh'd into production restarting Gunicorn manually, this is your escape route.&lt;/p&gt;

&lt;p&gt;Questions? Drop them in comments. I actually respond.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/app_dev_b139d1a58b00b6516/advanced-django-deployment-on-seenode-production-ready-strategies-for-2025-5bcm"&gt;Advanced Django Deployment on Seenode (Part 1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.seenode.com/scaling" rel="noopener noreferrer"&gt;Seenode Scaling Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.seenode.com/databases/optimization" rel="noopener noreferrer"&gt;Seenode Database Optimization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Next:&lt;/strong&gt; Django REST API deployment with JWT and rate limiting. Publishing next week.&lt;/p&gt;

</description>
      <category>django</category>
      <category>devops</category>
      <category>python</category>
      <category>deploydjango</category>
    </item>
    <item>
      <title>Advanced Django Deployment on Seenode: Production-Ready Strategies for 2025</title>
      <dc:creator>Navdeep Rana</dc:creator>
      <pubDate>Sat, 15 Nov 2025 08:26:42 +0000</pubDate>
      <link>https://dev.to/app_dev_b139d1a58b00b6516/advanced-django-deployment-on-seenode-production-ready-strategies-for-2025-5bcm</link>
      <guid>https://dev.to/app_dev_b139d1a58b00b6516/advanced-django-deployment-on-seenode-production-ready-strategies-for-2025-5bcm</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonumncam2eknvlp4j5ep.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonumncam2eknvlp4j5ep.png" alt="Setting up first project" width="800" height="1031"&gt;&lt;/a&gt;&lt;br&gt;
I’m Navdeep. I’ve been deploying Django apps since 2014, back when we ssh’d into bare-metal boxes and ran &lt;code&gt;git pull&lt;/code&gt; by hand (yes, really). Last month I migrated a client’s analytics tool to Seenode and hit the greatest hits of prod pain: connection pools melting down, static files mysteriously 404ing, and a DEBUG=True scare at 2 a.m. If you’ve ever whispered “just one last manual migration” before pushing to prod, consider this an intervention.&lt;/p&gt;

&lt;p&gt;What follows is the messy, opinionated playbook I’ve been carving out for years—the same advice I end up whiteboarding for mentees after we spend three hours chasing a missing comma in &lt;code&gt;WhiteNoiseMiddleware&lt;/code&gt;. Screenshots come straight from my live Seenode project so you can copy the setup without guessing, and I’ve left in the false starts (because that’s what real deployments look like).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero-patience TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Harden &lt;code&gt;settings.py&lt;/code&gt;, enable conn pooling, and treat env vars like explosives.&lt;/li&gt;
&lt;li&gt;Split web/worker/scheduler services; autoscale workers instead of overloading Gunicorn.&lt;/li&gt;
&lt;li&gt;Automate deploys via Seenode’s API so every push ships with guardrails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I kicked this migration off on Seenode’s 7-day free trial, started the web service on the Basic tier (~$3/mo), and only stepped up through Standard/Pro to Ultra (~$24/mo) when metrics proved we needed the headroom. Managed PostgreSQL mirrored that journey: the entry-level tier handled smoke tests, mid-tier covered beta, and Tier 4 (~$20/mo) became home once analytics jobs started hammering the cluster. Every database tier ships with 14 days of metrics plus both private and public connection hostnames, so I kept workers on the private network while BI tools hit the public endpoint.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Production Deployment is Different
&lt;/h2&gt;

&lt;p&gt;The Seenode docs get you live in five minutes, which is perfect for demos. Production? That’s where the sharp edges live:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security hardening&lt;/strong&gt;: Protecting against common vulnerabilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance optimization&lt;/strong&gt;: Handling real traffic efficiently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability&lt;/strong&gt;: Ensuring your app stays online during deployments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt;: Knowing what's happening when things go wrong&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Preparing for growth from day one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seenode’s Git-based workflow takes care of the boring bits, but you still have to dial in Django itself. I’m allergic to slow builds or sluggish endpoints—if something feels laggy, I rebuild it. Everything below comes from the live project you’ll see in the screenshots, warts and all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5exttwq247xy9hwvnyk0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5exttwq247xy9hwvnyk0.png" alt="django developer" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Production-Ready Settings Configuration
&lt;/h2&gt;

&lt;p&gt;I usually harden &lt;code&gt;settings.py&lt;/code&gt; before touching anything else. If that file is sloppy, everything downstream is shaky—security, performance, even observability.&lt;/p&gt;
&lt;h3&gt;
  
  
  Security First: Environment Variables
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dj_database_url&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;BASE_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;

&lt;span class="c1"&gt;# SECURITY: Never commit SECRET_KEY to version control
# Seenode lets you manage these values in the dashboard’s Environment tab
&lt;/span&gt;&lt;span class="n"&gt;SECRET_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SECRET_KEY environment variable is required in production&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# DEBUG: Always False in production
# Using .lower() handles various formats: "false", "False", "FALSE"
&lt;/span&gt;&lt;span class="n"&gt;DEBUG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;False&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;# ALLOWED_HOSTS: Prevents HTTP Host header attacks
# Format in Seenode env vars: "yourdomain.com,www.yourdomain.com,api.yourdomain.com"
&lt;/span&gt;&lt;span class="n"&gt;ALLOWED_HOSTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ALLOWED_HOSTS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# If ALLOWED_HOSTS is empty, add a fallback for the generated *.seenode.app host
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ALLOWED_HOSTS&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# You'll want to set this explicitly in production
&lt;/span&gt;    &lt;span class="n"&gt;ALLOWED_HOSTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Temporary - set your actual domain!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I’ve personally watched a production app fall over because someone left &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt; empty. Set it explicitly and fail loudly if it’s missing.&lt;/p&gt;
&lt;h3&gt;
  
  
  Database Configuration with Connection Pooling
&lt;/h3&gt;

&lt;p&gt;The default &lt;code&gt;dj-database-url&lt;/code&gt; configuration works, but production apps need connection pooling to handle concurrent requests efficiently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Database configuration optimized for production
&lt;/span&gt;&lt;span class="n"&gt;DATABASES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dj_database_url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;conn_max_age&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Keep connections alive for 10 minutes
&lt;/span&gt;        &lt;span class="n"&gt;conn_health_checks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Verify connections before use
&lt;/span&gt;        &lt;span class="n"&gt;ssl_require&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Force SSL for security
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Connection pool settings (if using PostgreSQL)
# These prevent "too many connections" errors under load
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;DATABASES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ENGINE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;DATABASES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OPTIONS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;connect_timeout&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-c statement_timeout=30000&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# 30 second query timeout
&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the analytics project I mentioned, median response time dropped from ~340 ms to 180 ms after enabling &lt;code&gt;conn_max_age=600&lt;/code&gt;. Not a lab-grade benchmark, but enough proof for the product team to stop blaming PostgreSQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Static Files: WhiteNoise Configuration
&lt;/h3&gt;

&lt;p&gt;WhiteNoise is excellent for serving static files, but the default configuration isn't optimized for production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Static files configuration
&lt;/span&gt;&lt;span class="n"&gt;STATIC_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/static/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;STATIC_ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BASE_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;staticfiles&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;# WhiteNoise configuration for production
# Add this to MIDDLEWARE (before SecurityMiddleware if present)
&lt;/span&gt;&lt;span class="n"&gt;MIDDLEWARE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;django.middleware.security.SecurityMiddleware&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;whitenoise.middleware.WhiteNoiseMiddleware&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Add this
&lt;/span&gt;    &lt;span class="c1"&gt;# ... other middleware
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# WhiteNoise storage with compression
&lt;/span&gt;&lt;span class="n"&gt;STATICFILES_STORAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;whitenoise.storage.CompressedManifestStaticFilesStorage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;# Cache static files for 1 year (browsers will cache)
&lt;/span&gt;&lt;span class="n"&gt;WHITENOISE_MAX_AGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;31536000&lt;/span&gt;  &lt;span class="c1"&gt;# 1 year in seconds
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pro tip&lt;/strong&gt;: The &lt;code&gt;CompressedManifestStaticFilesStorage&lt;/code&gt; automatically gzips your static files, reducing bandwidth by 60-80%. This is especially important for JavaScript bundles and CSS files.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;UPDATE (credit to Priya from our platform team): My earlier draft recommended &lt;code&gt;ManifestStaticFilesStorage&lt;/code&gt; alone. She pointed out it breaks cache busting when you deploy frequently. Swapping to &lt;code&gt;CompressedManifestStaticFilesStorage&lt;/code&gt; solved the stale asset issue instantly, so now it’s the default in every project I touch.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Security Headers and CORS
&lt;/h3&gt;

&lt;p&gt;For APIs or apps with separate frontends, you'll need CORS configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Install: pip install django-cors-headers
&lt;/span&gt;
&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# ... other apps
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;corsheaders&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;MIDDLEWARE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;django.middleware.security.SecurityMiddleware&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;corsheaders.middleware.CorsMiddleware&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Should be near the top
&lt;/span&gt;    &lt;span class="c1"&gt;# ... other middleware
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# CORS configuration for production
&lt;/span&gt;&lt;span class="n"&gt;CORS_ALLOWED_ORIGINS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CORS_ALLOWED_ORIGINS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Security headers
&lt;/span&gt;&lt;span class="n"&gt;SECURE_BROWSER_XSS_FILTER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;SECURE_CONTENT_TYPE_NOSNIFF&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;X_FRAME_OPTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DENY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;# Prevent clickjacking
&lt;/span&gt;
&lt;span class="c1"&gt;# If you're using HTTPS (which Seenode provides automatically)
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;SECURE_SSL_REDIRECT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;SESSION_COOKIE_SECURE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;CSRF_COOKIE_SECURE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F90de9hxuwuhjejlh8azb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F90de9hxuwuhjejlh8azb.png" alt="Seenode dashboard" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Service-to-Service Secrets and Rotation
&lt;/h3&gt;

&lt;p&gt;Environment variables are a good start, but production organizations need stronger guardrails:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scoped secrets&lt;/strong&gt;: Configure environment variables per service when possible. Keep worker-only credentials out of the public web container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotation playbook&lt;/strong&gt;: Add &lt;code&gt;SECRET_KEY_V2&lt;/code&gt;, deploy, verify, then remove &lt;code&gt;SECRET_KEY_V1&lt;/code&gt;. Document the process so any engineer can rotate in minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Least privilege databases&lt;/strong&gt;: Create a separate PostgreSQL user for Celery or analytics jobs with read-only grants, and point &lt;code&gt;DATABASE_URL&lt;/code&gt; to that user from each service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI pipelines&lt;/strong&gt;: If you deploy via GitHub Actions, reference repository secrets and pass them as workflow inputs. Never echo secrets in logs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This layered approach prevents the all-too-common scenario where someone inadvertently leaks production credentials during debugging or screen sharing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Personal scar: Last year a teammate dumped &lt;code&gt;os.environ&lt;/code&gt; while debugging and our DATABASE_URL ended up in plain text in the logs. We spent four hours rotating everything. Now we have this playbook, and I quiz people on it during their first week.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Gunicorn Configuration for Production
&lt;/h2&gt;

&lt;p&gt;The basic Gunicorn command works, but production deployments need proper worker configuration. Create a &lt;code&gt;gunicorn_config.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# gunicorn_config.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="c1"&gt;# Server socket
# IMPORTANT: Configure this port in the Seenode dashboard (there is no default port)
# The load balancer proxies from ports 80/443 to your configured container port
&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0:80&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# Set this to match the port you configure in Seenode dashboard
&lt;/span&gt;&lt;span class="n"&gt;backlog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;

&lt;span class="c1"&gt;# Worker processes
# Formula: (2 x CPU cores) + 1
&lt;/span&gt;&lt;span class="n"&gt;workers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GUNICORN_WORKERS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cpu_count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;worker_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sync&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;# Use 'gevent' or 'uvicorn.workers.UvicornWorker' for async
&lt;/span&gt;&lt;span class="n"&gt;worker_connections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;  &lt;span class="c1"&gt;# 30 seconds - adjust based on your longest request
&lt;/span&gt;&lt;span class="n"&gt;keepalive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="c1"&gt;# Logging
&lt;/span&gt;&lt;span class="n"&gt;accesslog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;# Log to stdout (Seenode captures this)
&lt;/span&gt;&lt;span class="n"&gt;errorlog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;   &lt;span class="c1"&gt;# Log to stderr
&lt;/span&gt;&lt;span class="n"&gt;loglevel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;info&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;access_log_format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%(h)s %(l)s %(u)s %(t)s &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%(r)s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; %(s)s %(b)s &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%(f)s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%(a)s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; %(D)s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;# Process naming
&lt;/span&gt;&lt;span class="n"&gt;proc_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;django_app&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;# Server mechanics
&lt;/span&gt;&lt;span class="n"&gt;daemon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="n"&gt;pidfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;umask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;tmp_upload_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="c1"&gt;# Graceful timeout for worker restarts
&lt;/span&gt;&lt;span class="n"&gt;graceful_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point your Seenode start command at that config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gunicorn your_project.wsgi:application &lt;span class="nt"&gt;--config&lt;/span&gt; gunicorn_config.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Too few workers and your app chokes on concurrent requests; too many and you’ll OOM the box. I once “optimized” by setting workers to &lt;code&gt;cpu_count() * 4&lt;/code&gt; and spent the next day chasing memory leaks. More isn’t better—start with &lt;code&gt;(2 * cores) + 1&lt;/code&gt;, watch metrics, then adjust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced Build Script
&lt;/h2&gt;

&lt;p&gt;The basic &lt;code&gt;build.sh&lt;/code&gt; works, but here's a production-ready version with error handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; errexit  &lt;span class="c"&gt;# Exit on any error&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; nounset  &lt;span class="c"&gt;# Exit on undefined variables&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; pipefail &lt;span class="c"&gt;# Exit on pipe failures&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Starting build process..."&lt;/span&gt;

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Installing Python dependencies..."&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Run database migrations&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Running database migrations..."&lt;/span&gt;
python manage.py migrate &lt;span class="nt"&gt;--no-input&lt;/span&gt;

&lt;span class="c"&gt;# Collect static files&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Collecting static files..."&lt;/span&gt;
python manage.py collectstatic &lt;span class="nt"&gt;--no-input&lt;/span&gt; &lt;span class="nt"&gt;--clear&lt;/span&gt;

&lt;span class="c"&gt;# Verify critical environment variables&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SECRET_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: SECRET_KEY not set"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Build completed successfully!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key improvements&lt;/strong&gt;: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--clear&lt;/code&gt; flag removes old static files before collecting new ones&lt;/li&gt;
&lt;li&gt;Error checking prevents silent failures&lt;/li&gt;
&lt;li&gt;Verbose output helps debug deployment issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m notoriously impatient with repetitive toil. I once spent five hours writing automation just to reclaim a 30-minute weekly deployment chore, and I’d do it again. If a build step slows me down, I script it, commit it, and move on—that mindset is baked into this &lt;code&gt;build.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa10m6lpcvj7vzh97c3hi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa10m6lpcvj7vzh97c3hi.png" alt="Setting up Django" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Successful Seenode build pipeline: dependency install, migrations, collectstatic, and verification.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Environment Variables Setup in Seenode
&lt;/h2&gt;

&lt;p&gt;In the Seenode dashboard, configure these environment variables:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Required:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SECRET_KEY=your-super-secret-key-here-generate-with-openssl-rand-hex-32
DEBUG=False
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
DATABASE_URL=postgresql://user:pass@host:5432/dbname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Recommended for production:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GUNICORN_WORKERS=4
CORS_ALLOWED_ORIGINS=https://yourfrontend.com,https://www.yourfrontend.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security tip&lt;/strong&gt;: Generate a strong &lt;code&gt;SECRET_KEY&lt;/code&gt; using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import secrets; print(secrets.token_urlsafe(50))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The niceties matter: custom domains were just a CNAME away, automatic HTTPS/HTTP/2 (plus WebSocket support) meant I never touched Certbot, and the runtime log stream + env-var editor in the dashboard saved me from emergency redeploys. On the one weekend when I did need help, 24/7 support actually responded inside an hour.&lt;/p&gt;

&lt;h3&gt;
  
  
  Triggering Deployments from CI
&lt;/h3&gt;

&lt;p&gt;Seenode’s docs include end-to-end guides for &lt;a href="https://seenode.com/docs/guides/deployments/deployment-using-github-actions/" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; and &lt;a href="https://seenode.com/docs/guides/deployments/deployment-using-gitlab-ci-cd/" rel="noopener noreferrer"&gt;GitLab CI/CD&lt;/a&gt;. Follow those workflows to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Store your &lt;code&gt;SEENODE_API_TOKEN&lt;/code&gt; as an encrypted secret&lt;/li&gt;
&lt;li&gt;Call the official deployment action once tests pass&lt;/li&gt;
&lt;li&gt;Monitor the deployment from the dashboard’s Deployments tab&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That approach keeps your pipeline supported and avoids guessing undocumented API endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Service Architecture on Seenode
&lt;/h2&gt;

&lt;p&gt;Most production Django apps rely on more than a single web process. Seenode issues both public URLs and internal hostnames (see the port configuration guide’s &lt;code&gt;http://web-abcdefgh-service&lt;/code&gt; example), so services can talk over the private network without extra wiring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web, Worker, and Scheduler Split
&lt;/h3&gt;

&lt;p&gt;Spin up three services from the same repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web&lt;/strong&gt;: Runs Gunicorn and exposes port 80.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker&lt;/strong&gt;: Runs Celery/RQ with &lt;code&gt;celery -A your_project worker -l info&lt;/code&gt; and no public port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduler&lt;/strong&gt;: Runs &lt;code&gt;celery -A your_project beat&lt;/code&gt; or &lt;code&gt;python manage.py crontab run&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of them share the same managed PostgreSQL database plus whatever Redis provider you configure, but only the web service is accessible publicly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Background Jobs with Celery on Seenode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start command for worker service&lt;/span&gt;
celery &lt;span class="nt"&gt;-A&lt;/span&gt; your_project worker &lt;span class="nt"&gt;--loglevel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;info &lt;span class="nt"&gt;--autoscale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8,2

&lt;span class="c"&gt;# Start command for scheduler service&lt;/span&gt;
celery &lt;span class="nt"&gt;-A&lt;/span&gt; your_project beat &lt;span class="nt"&gt;--loglevel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tune autoscale parameters to match Seenode’s CPU allocation, and remember to set &lt;code&gt;CELERY_BROKER_URL&lt;/code&gt;/&lt;code&gt;CELERY_RESULT_BACKEND&lt;/code&gt; in the worker’s environment variables.&lt;/p&gt;

&lt;p&gt;~I tried running the workers inside the web service to “save resources.”~ Don’t do that. Separate services keep crashes isolated and make it easier to scale the noisy neighbors independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database Optimization for Analytics
&lt;/h3&gt;

&lt;p&gt;If analytics queries pile up, consider optimizing your database strategy. Depending on your database provider's capabilities, you might:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use database connection pooling to handle concurrent read queries efficiently&lt;/li&gt;
&lt;li&gt;Optimize queries with proper indexing and &lt;code&gt;select_related&lt;/code&gt;/&lt;code&gt;prefetch_related&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Consider separating read-heavy workloads if your database provider supports read replicas or similar features&lt;/li&gt;
&lt;li&gt;Use caching layers (Redis, Memcached) to reduce database load for frequently accessed data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always check your database provider's documentation for supported scaling and replication options.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared Media and CDN Layer
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;django-storages&lt;/code&gt; with an S3-compatible provider (Wasabi, Backblaze, or Cloudflare R2). Point &lt;code&gt;MEDIA_URL&lt;/code&gt; at a CDN (Cloudflare or Fastly) so static assets never touch your dynos, and configure cache invalidation via webhook when you deploy.&lt;/p&gt;

&lt;p&gt;Whenever I onboard a junior developer, this is the architecture diagram we whiteboard first. Once they see how the pieces talk inside Seenode’s private network, everything else—logs, metrics, secret rotation—suddenly clicks. Sharing those “aha!” moments is honestly my favorite part of the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting Common Production Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Issue 1: 502 Bad Gateway
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom&lt;/strong&gt;: Your app shows "502 Bad Gateway" after deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: Port binding mismatch. Your application must listen on the port you configured in the Seenode dashboard. The load balancer proxies from ports 80/443 to your configured container port.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check your Gunicorn bind address matches the port you set in Seenode dashboard (e.g., &lt;code&gt;bind = "0.0.0.0:80"&lt;/code&gt; if you configured port 80)&lt;/li&gt;
&lt;li&gt;Verify the Port field in Seenode dashboard is set to match your application's bind port (not left empty - there is no default)&lt;/li&gt;
&lt;li&gt;Check logs: &lt;code&gt;gunicorn&lt;/code&gt; should show "Listening at: &lt;a href="http://0.0.0.0:%5BYOUR_PORT%5D" rel="noopener noreferrer"&gt;http://0.0.0.0:[YOUR_PORT]&lt;/a&gt;"&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Issue 2: Database Connection Timeouts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom&lt;/strong&gt;: Intermittent "OperationalError: could not connect to server" errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: Too many database connections or connection pool exhaustion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In settings.py, add connection limits
&lt;/span&gt;&lt;span class="n"&gt;DATABASES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CONN_MAX_AGE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;  &lt;span class="c1"&gt;# Reuse connections
&lt;/span&gt;&lt;span class="n"&gt;DATABASES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OPTIONS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;connect_timeout&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, ensure your worker count doesn't exceed available database connections. If you have 10 workers and each opens 2 connections, you need at least 20 available connections in your PostgreSQL instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Issue 3: Static Files Not Loading
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom&lt;/strong&gt;: CSS/JS files return 404 errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: Static files weren't collected during build, or WhiteNoise isn't configured correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify &lt;code&gt;collectstatic&lt;/code&gt; runs in &lt;code&gt;build.sh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;STATIC_ROOT&lt;/code&gt; path is correct&lt;/li&gt;
&lt;li&gt;Ensure &lt;code&gt;WhiteNoiseMiddleware&lt;/code&gt; is in &lt;code&gt;MIDDLEWARE&lt;/code&gt; (before other middleware that might handle static files)&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;STATICFILES_STORAGE&lt;/code&gt; is set correctly&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Issue 4: Memory Issues
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom&lt;/strong&gt;: App crashes or becomes unresponsive under load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: Too many Gunicorn workers consuming too much memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Calculate optimal worker count based on available memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In gunicorn_config.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psutil&lt;/span&gt;

&lt;span class="c1"&gt;# Calculate workers based on available memory
# Assume ~100MB per worker (adjust based on your app)
&lt;/span&gt;&lt;span class="n"&gt;available_memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;virtual_memory&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# MB
&lt;/span&gt;&lt;span class="n"&gt;memory_per_worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;  &lt;span class="c1"&gt;# MB
&lt;/span&gt;&lt;span class="n"&gt;max_workers_by_memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;available_memory&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;memory_per_worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cpu_workers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cpu_count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="n"&gt;workers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpu_workers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_workers_by_memory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8hg4fl30589tks915ea.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8hg4fl30589tks915ea.png" alt="Django and MySQL" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Issue 5: Secrets Logged Accidentally
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom&lt;/strong&gt;: &lt;code&gt;SECRET_KEY&lt;/code&gt;, access tokens, or database URLs show up in logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: Debug statements (&lt;code&gt;print(os.environ)&lt;/code&gt;) or overly verbose log levels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a log filter:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SensitiveFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
           &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;secret_name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
               &lt;span class="n"&gt;secret_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
               &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;secret_value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                   &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[REDACTED]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Attach the filter to every handler in &lt;code&gt;LOGGING&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rotate the affected credentials immediately if exposure occurred.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Security Checklist for Production
&lt;/h2&gt;

&lt;p&gt;Before going live, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DEBUG = False&lt;/code&gt; (check environment variable)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SECRET_KEY&lt;/code&gt; is set and strong (50+ characters)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ALLOWED_HOSTS&lt;/code&gt; includes your domain (no wildcards in production)&lt;/li&gt;
&lt;li&gt;Database uses SSL (&lt;code&gt;ssl_require=True&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CSRF and session cookies are secure (&lt;code&gt;CSRF_COOKIE_SECURE&lt;/code&gt;, &lt;code&gt;SESSION_COOKIE_SECURE&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Security headers are configured (&lt;code&gt;X_FRAME_OPTIONS&lt;/code&gt;, &lt;code&gt;SECURE_CONTENT_TYPE_NOSNIFF&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CORS is configured (if needed) with specific origins, not &lt;code&gt;*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;No sensitive data in logs (filter out passwords, tokens, etc.)&lt;/li&gt;
&lt;li&gt;Database credentials are in environment variables, not code&lt;/li&gt;
&lt;li&gt;Static files are served securely (WhiteNoise handles this)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance Optimization Tips
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enable database query logging in development&lt;/strong&gt; to find N+1 queries:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="n"&gt;LOGGING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;handlers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
               &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;console&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                   &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;class&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;logging.StreamHandler&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="p"&gt;},&lt;/span&gt;
           &lt;span class="p"&gt;},&lt;/span&gt;
           &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;loggers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
               &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;django.db.backends&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                   &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;level&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="p"&gt;},&lt;/span&gt;
           &lt;span class="p"&gt;},&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;select_related&lt;/code&gt; and &lt;code&gt;prefetch_related&lt;/code&gt;&lt;/strong&gt; to reduce database queries&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enable database connection pooling&lt;/strong&gt; (already covered above)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitor response times&lt;/strong&gt; using your platform's monitoring tools and logs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set appropriate cache headers&lt;/strong&gt; for API responses that don't change frequently&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Anything above ~200 ms median latency makes me twitchy, so I keep the monitoring dashboards and Grafana dashboards pinned. The moment p95 creeps north, we profile queries or drop a cache layer—no “we’ll fix it later” excuses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I Wish Someone Told Me About Seenode + Django
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;There is no default port. You must configure the port in the Seenode dashboard, and your app must bind to that same port. The load balancer routes from 80/443 to your configured container port.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;collectstatic --clear&lt;/code&gt; feels optional until stale assets haunt you—run it every time.&lt;/li&gt;
&lt;li&gt;Connection pooling (&lt;code&gt;conn_max_age&lt;/code&gt;) buys you real latency wins without touching code.&lt;/li&gt;
&lt;li&gt;Start with the lowest tier that works for testing, but budget for scaling the moment real traffic enters.&lt;/li&gt;
&lt;li&gt;Logs may have retention limits depending on your plan; consider piping them to external storage if you need long-term debugging access.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Zero-Downtime Deployments and Rollbacks
&lt;/h2&gt;

&lt;p&gt;Production outages usually stem from rushed deploys. Build a boring release pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-flight checks&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   python manage.py check &lt;span class="nt"&gt;--deploy&lt;/span&gt;
   python manage.py &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;smoke
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run these in CI and fail the build on any error.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Staged rollouts&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy to a staging environment first, validate metrics/logs.&lt;/li&gt;
&lt;li&gt;Use feature flags to gradually expose changes to production.&lt;/li&gt;
&lt;li&gt;Monitor closely after each deployment increment.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Rollback strategy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep a &lt;code&gt;ROLLBACK.md&lt;/code&gt; doc with exact commands for reverting to a previous Git SHA.&lt;/li&gt;
&lt;li&gt;Document how to revert migrations (&lt;code&gt;python manage.py migrate app 0010_previous&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Test your rollback process in staging before you need it in production.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Feature flags&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use LaunchDarkly, Flagsmith, or an open-source alternative to gradually expose risky changes without redeploying.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Practice these steps during calm periods so they’re muscle memory during incidents.&lt;/p&gt;

&lt;p&gt;Whenever I learn a smoother rollback trick, I hop on a call with my mentees and walk them through it step by step. Half the fun of discovering a better deployment dodge is geeking out about it with people who’ll use it next week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;Now that your Django app is production-ready on Seenode, the next challenge is scaling it. In my next article, I'll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Architecture patterns for multi-service Django apps&lt;/li&gt;
&lt;li&gt;Database optimization strategies for PostgreSQL on Seenode&lt;/li&gt;
&lt;li&gt;Caching strategies to reduce database load&lt;/li&gt;
&lt;li&gt;Background worker configuration for async tasks&lt;/li&gt;
&lt;li&gt;Performance monitoring and alerting setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Part 2 drops within the next 24 hours—Git-based scaling deep dive, with workers, read replicas, the works. I’ll link it here as soon as it’s live so you don’t have to hunt for it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Deploying Django to production on Seenode is straightforward, but production-ready deployments require attention to security, performance, and reliability. The configurations I've shared here are based on real-world deployments and will help you avoid common pitfalls.&lt;/p&gt;

&lt;p&gt;If anything here sparks an idea, ping me. I’m the person who can talk for hours about rollout strategies over chai, especially if it means a junior dev can skip the headaches I’ve already collected.&lt;/p&gt;

&lt;p&gt;The key takeaway: Don't just make it work—make it work securely, efficiently, and reliably. Your users (and your future self) will thank you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ready to deploy?&lt;/strong&gt; &lt;a href="https://cloud.seenode.com" rel="noopener noreferrer"&gt;Sign up for Seenode&lt;/a&gt; and put these strategies into practice. The Git-based workflow makes iterating on these configurations easy—just push to your repository and watch it deploy automatically.&lt;/p&gt;

&lt;p&gt;Have questions or run into issues? Drop a comment below, and I'll help you troubleshoot.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Want to learn more about Django deployment? Check out the &lt;a href="https://docs.seenode.com/django" rel="noopener noreferrer"&gt;Seenode Django documentation&lt;/a&gt; or explore &lt;a href="https://seenode.com/pricing" rel="noopener noreferrer"&gt;Seenode's pricing&lt;/a&gt; for your production needs.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>django</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
