en
Django Tor Hidden Service: Complete Setup Tutorial 2026
Django's robust security features, authentication system, and admin interface make it an excellent choice for hidden service applications. This tutorial covers deploying a production Django app as a .onion service, from security settings to Gunicorn/Nginx configuration.
Need this done for your project?
We implement, you ship. Async, documented, done in days.
Django Security Settings for Hidden Services
Django's settings.py has security settings that need adjustment for .onion deployment. ALLOWED_HOSTS: set to ['youraddress.onion', '127.0.0.1', 'localhost']. Without this, Django rejects all requests with a 400 error. CSRF_TRUSTED_ORIGINS: for Django 4.0+, add ['http://youraddress.onion', 'https://youraddress.onion'] if using CSRF protection across different ports. SESSION_COOKIE_SECURE: set True only if your .onion is served over HTTPS (some hidden services use HTTP internally). For HTTP-only .onion: set SESSION_COOKIE_SECURE = False (the Tor transport provides encryption). CSRF_COOKIE_SECURE: same as SESSION_COOKIE_SECURE. SECURE_SSL_REDIRECT: set False for HTTP .onion services (the Tor network provides transport security). X_FRAME_OPTIONS = 'DENY': keep this enabled regardless of HTTP/HTTPS. Content-Security-Policy: configure via django-csp middleware or manually in Nginx headers. For .onion services, default-src 'self' with appropriate exceptions for your application.
Gunicorn Configuration for .onion Services
Gunicorn (Green Unicorn) serves Django applications via WSGI. For hidden services: bind to Unix socket (better performance than TCP for local connections): gunicorn --bind unix:/run/gunicorn/gunicorn.sock myapp.wsgi:application. Workers: use 2*(CPU cores)+1 workers. For a 2-core VPS: 5 workers. Worker class: sync workers for CPU-bound apps, gevent or asyncio workers for I/O-bound apps (database queries, external API calls). Timeout: 120 seconds is appropriate for Tor's variable latency. Systemd service for Gunicorn: create /etc/systemd/system/gunicorn.service with Type=notify, ExecStart=/path/to/gunicorn command, ExecReload=kill -s HUP $MAINPID, RuntimeDirectory=gunicorn, RuntimeDirectoryMode=755. Enable with systemctl enable gunicorn, start with systemctl start gunicorn.
Nginx Configuration for Django .onion Service
Nginx proxies HTTP requests from Tor to Gunicorn. Configuration for Django hidden service: server block listening on 127.0.0.1:PORT (not 0.0.0.0), proxy_pass to Unix socket (proxy_pass http://unix:/run/gunicorn/gunicorn.sock;), static file serving (Django's staticfiles at a configured STATIC_ROOT path), and media file serving if applicable. Critical headers: proxy_set_header Host $host; (passes the .onion hostname to Django, required for ALLOWED_HOSTS validation), proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; (though useless for .onion, include for consistency), and proxy_set_header X-Forwarded-Proto $scheme; (informs Django of the protocol). Django STATIC_ROOT: run python manage.py collectstatic before deployment, serve static files directly from Nginx (faster than Django's development server).
Django Admin and Staff Access Over .onion
Django's admin interface is accessible at /admin/ by default. For hidden services: keep admin at /admin/ (obscurity is not security), protect with two-factor authentication (django-otp or django-two-factor-auth), restrict admin access to specific sessions (middleware that checks for a cookie set during a privileged authentication step), and consider moving admin to a separate .onion address (different private key, different introduction points - admin operations cannot be correlated with user-facing traffic). Staff authentication: all admin logins should use strong passwords and 2FA. Session expiry for admin: SESSION_COOKIE_AGE = 3600 (1 hour) for admin sessions is appropriate. Admin actions are logged by Django's built-in admin log - retain these for security auditing.
Database Configuration and Migration Management
PostgreSQL is the recommended database for production Django hidden services. Connection: use Unix socket for local PostgreSQL (faster than TCP loopback). In settings.py: DATABASES = {'default': {'ENGINE': 'django.db.backends.postgresql', 'NAME': 'mydb', 'USER': 'myuser', 'HOST': '/var/run/postgresql/', 'PORT': ''}}. Database security: PostgreSQL listens on localhost only (not 0.0.0.0). Database user has only the permissions needed (no superuser). Enable pg_hba.conf local authentication. Migrations in production: test all migrations in staging before production, never run migrations on production without a backup, use django-safemigrate for destructive migration checks. Backup schedule: daily pg_dump to encrypted off-server storage. Connection pooling: django-db-connection-pool or pgBouncer for high-traffic sites that exhaust PostgreSQL's connection limit.
Related Services
Why Anubiz Host
100% async — no calls, no meetings
Delivered in days, not weeks
Full documentation included
Production-grade from day one
Security-first approach
Post-delivery support included
Ready to get started?
Skip the research. Tell us what you need, and we'll scope it, implement it, and hand it back — fully documented and production-ready.