Django incluye un servidor de desarrollo que no está pensado para producción. Para publicar un proyecto se usa un servidor WSGI como Gunicorn, que ejecuta el código Python, y Nginx delante para atender las conexiones, servir los archivos estáticos y gestionar TLS. En este tutorial desplegarás un proyecto Django en Ubuntu 24.04 con PostgreSQL, Gunicorn activado por socket de systemd, Nginx y un certificado de Let's Encrypt.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 1 GB de RAM.
  • Un usuario no root con privilegios sudo. En los ejemplos se llama your_user.
  • Un dominio con un registro DNS A apuntando a la IP del servidor. En los ejemplos se usa your_domain.

El proyecto de ejemplo se llama myproject y vive en /var/www/myproject. Si despliegas tu propio código, sustituye el nombre del paquete de configuración (el directorio que contiene settings.py y wsgi.py) donde aparezca myproject.

Paso 1: Instalar los paquetes del sistema

Ubuntu 24.04 trae Python 3.12, compatible con las versiones actuales de Django. Instala el módulo de entornos virtuales, PostgreSQL y Nginx:

sudo apt update
sudo apt install python3-venv python3-dev postgresql nginx git

Comprueba que PostgreSQL está en marcha:

systemctl status postgresql --no-pager
● postgresql.service - PostgreSQL RDBMS
     Active: active (exited) since ...

El estado active (exited) es normal: ese servicio solo agrupa los clústeres, que se ejecutan en postgresql@16-main.

Abre en el cortafuegos SSH, HTTP y HTTPS:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Paso 2: Crear la base de datos PostgreSQL

Crea un usuario de PostgreSQL y una base de datos que le pertenezca. Desde PostgreSQL 15 solo el propietario de la base de datos puede crear tablas en el esquema public, así que usar OWNER evita errores de permisos al migrar. Sustituye your_strong_password por una contraseña larga y aleatoria:

sudo -u postgres psql -c "CREATE USER myproject WITH PASSWORD 'your_strong_password';"
sudo -u postgres psql -c "CREATE DATABASE myproject OWNER myproject;"

Comprueba que puedes conectarte con el nuevo usuario:

psql -h localhost -U myproject -d myproject -c "SELECT current_user;"
 current_user
--------------
 myproject
(1 row)

Paso 3: Preparar el proyecto y el entorno virtual

Crea el directorio de la aplicación con tu usuario como propietario. Se usa /var/www y no tu directorio personal porque en Ubuntu 24.04 los directorios de /home tienen permisos 750 y Nginx no podría leer los archivos estáticos:

sudo mkdir -p /var/www/myproject
sudo chown your_user:your_user /var/www/myproject
cd /var/www/myproject

Si tienes el proyecto en un repositorio, clónalo en ese directorio con git clone https://github.com/your_account/myproject.git ..

Crea un entorno virtual para aislar las dependencias del Python del sistema y actívalo:

python3 -m venv venv
source venv/bin/activate

Instala Django, Gunicorn y el controlador de PostgreSQL. psycopg[binary] incluye la librería de cliente, así que no necesitas compilar nada:

pip install --upgrade pip
pip install django gunicorn "psycopg[binary]"

Si clonaste tu proyecto, instala además sus dependencias con pip install -r requirements.txt. Si solo quieres probar el despliegue, crea un proyecto nuevo:

django-admin startproject myproject .

Comprueba la versión instalada:

python -m django --version
5.2.x

Paso 4: Guardar los secretos en un archivo de entorno

La clave secreta y la contraseña de la base de datos no deben estar en el código. Genera una clave aleatoria:

python3 -c "import secrets; print(secrets.token_urlsafe(50))"

Crea el archivo de entorno que leerán Gunicorn y tus comandos de gestión:

nano /var/www/myproject/.env
DJANGO_SECRET_KEY=la_clave_generada
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=your_domain,www.your_domain
DB_NAME=myproject
DB_USER=myproject
DB_PASSWORD=your_strong_password

Restringe los permisos para que solo tu usuario pueda leerlo:

chmod 600 /var/www/myproject/.env

Paso 5: Configurar Django para producción

Abre el archivo de ajustes:

nano /var/www/myproject/myproject/settings.py

Añade import os al principio del archivo y sustituye las líneas de SECRET_KEY, DEBUG, ALLOWED_HOSTS y el bloque DATABASES por estas:

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True"
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["DB_NAME"],
        "USER": os.environ["DB_USER"],
        "PASSWORD": os.environ["DB_PASSWORD"],
        "HOST": "localhost",
        "PORT": "5432",
    }
}

Al final del archivo, define dónde se reunirán los archivos estáticos e indica a Django que confíe en la cabecera con la que Nginx le dirá si la petición original llegó por HTTPS:

STATIC_ROOT = BASE_DIR / "staticfiles"

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

Carga las variables de entorno en tu sesión para ejecutar los comandos de gestión:

cd /var/www/myproject
set -a; source .env; set +a

Aplica las migraciones, crea un superusuario para el panel de administración y copia los archivos estáticos a STATIC_ROOT:

python manage.py migrate
python manage.py createsuperuser
python manage.py collectstatic --noinput
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  ...
127 static files copied to '/var/www/myproject/staticfiles'.

Por último, ejecuta la comprobación de despliegue de Django, que avisa de ajustes inseguros:

python manage.py check --deploy

Es normal que aparezcan avisos sobre SECURE_HSTS_SECONDS y SECURE_SSL_REDIRECT: la redirección a HTTPS la hará Nginx, y HSTS conviene activarlo solo cuando HTTPS funcione de forma estable.

Sal del entorno virtual con deactivate.

Paso 6: Ejecutar Gunicorn con systemd

Usarás dos unidades: un socket que escucha en /run/gunicorn.sock y un servicio que arranca Gunicorn cuando llega la primera conexión. Crea el socket:

sudo nano /etc/systemd/system/gunicorn.socket
[Unit]
Description=gunicorn socket

[Socket]
ListenStream=/run/gunicorn.sock

[Install]
WantedBy=sockets.target

Crea el servicio. EnvironmentFile carga el archivo .env y --workers 3 es un buen punto de partida para un servidor de 1 o 2 vCPU (la regla habitual es 2 x núcleos + 1):

sudo nano /etc/systemd/system/gunicorn.service
[Unit]
Description=gunicorn daemon for myproject
Requires=gunicorn.socket
After=network.target

[Service]
User=your_user
Group=www-data
WorkingDirectory=/var/www/myproject
EnvironmentFile=/var/www/myproject/.env
ExecStart=/var/www/myproject/venv/bin/gunicorn \
          --access-logfile - \
          --workers 3 \
          --bind unix:/run/gunicorn.sock \
          myproject.wsgi:application
Restart=on-failure

[Install]
WantedBy=multi-user.target

Activa e inicia el socket:

sudo systemctl daemon-reload
sudo systemctl enable --now gunicorn.socket

Envía una petición al socket para que systemd arranque el servicio. La cabecera Host debe estar en ALLOWED_HOSTS:

curl -s -o /dev/null -w "%{http_code}\n" --unix-socket /run/gunicorn.sock -H "Host: your_domain" http://localhost/admin/login/
200

Comprueba que el servicio se ha iniciado:

systemctl status gunicorn --no-pager
● gunicorn.service - gunicorn daemon for myproject
     Active: active (running) since ...

Si algo falla, el error de Python aparece en sudo journalctl -u gunicorn.

Paso 7: Configurar Nginx como proxy inverso

Crea un bloque de servidor para el dominio:

sudo nano /etc/nginx/sites-available/myproject

Nginx servirá directamente /static/ desde disco y enviará el resto de peticiones a Gunicorn. El archivo proxy_params de Ubuntu añade las cabeceras Host, X-Real-IP, X-Forwarded-For y X-Forwarded-Proto:

server {
    listen 80;
    listen [::]:80;
    server_name your_domain www.your_domain;

    client_max_body_size 20M;

    location = /favicon.ico { access_log off; log_not_found off; }

    location /static/ {
        alias /var/www/myproject/staticfiles/;
        expires 30d;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }
}

Activa el sitio, desactiva el sitio por defecto y comprueba la sintaxis:

sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Paso 8: Activar HTTPS con Let's Encrypt

Instala Certbot con su plugin para Nginx y solicita el certificado. Certbot añade la configuración TLS y la redirección de HTTP a HTTPS:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your_domain -d www.your_domain

Comprueba que la renovación automática funciona:

sudo certbot renew --dry-run

Abre https://your_domain/admin/ en el navegador. Deberías ver el formulario de acceso del panel de administración con sus estilos cargados, lo que confirma que Nginx sirve los estáticos. Entra con el superusuario que creaste.

Actualizar la aplicación

Para desplegar cambios, descarga el código, instala dependencias, migra, reúne los estáticos y reinicia Gunicorn:

cd /var/www/myproject
git pull
source venv/bin/activate
set -a; source .env; set +a
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput
deactivate
sudo systemctl restart gunicorn

Solución de problemas

  • 502 Bad Gateway: Gunicorn no arranca. Revisa sudo journalctl -u gunicorn -n 50; los fallos típicos son una variable de .env que falta (KeyError) o un módulo WSGI mal escrito en ExecStart.
  • 400 Bad Request: el dominio no está en DJANGO_ALLOWED_HOSTS. Corrige .env y ejecuta sudo systemctl restart gunicorn.
  • El panel se ve sin estilos: no has ejecutado collectstatic o la ruta de alias no coincide con STATIC_ROOT. Comprueba que existe /var/www/myproject/staticfiles/admin/.
  • Cambios en las unidades de systemd sin efecto: tras editar los archivos ejecuta sudo systemctl daemon-reload y sudo systemctl restart gunicorn.socket gunicorn.service.

Conclusión

Tu proyecto Django funciona en producción con PostgreSQL, Gunicorn gestionado por systemd y Nginx sirviendo los estáticos y el tráfico HTTPS. Como siguientes pasos puedes:

  • Activar SECURE_HSTS_SECONDS cuando HTTPS lleve un tiempo funcionando sin problemas.
  • Configurar almacenamiento para los archivos subidos (MEDIA_ROOT) y servirlos con otro bloque location en Nginx.
  • Añadir Celery con Redis si la aplicación necesita tareas en segundo plano.