Crear una aplicación web profesional con Python no requiere comenzar contratando servicios cloud complejos ni construir una arquitectura con decenas de componentes. Con Linux, Django y PostgreSQL es posible desarrollar una aplicación completa, probarla localmente y después publicarla en Internet utilizando prácticamente el mismo stack empleado en proyectos empresariales.
En esta guía construiremos una pequeña aplicación de gestión de tareas con base de datos PostgreSQL y veremos el recorrido completo: preparar Linux, crear el proyecto Python, guardar información, desarrollar el CRUD y finalmente publicarlo mediante Gunicorn, Nginx y HTTPS.
El objetivo: terminar con una aplicación Python funcionando en un servidor Linux real, utilizando PostgreSQL para los datos, Django para la aplicación, Gunicorn como servidor de aplicación y Nginx como puerta de entrada desde Internet.
¿Qué vamos a construir?
Utilizaremos una aplicación sencilla para administrar tareas. Un usuario podrá crear una tarea, escribir una descripción, marcarla como terminada y eliminarla.
El ejemplo es pequeño, pero la arquitectura puede utilizarse después para desarrollar sistemas mucho mayores: inventarios, intranets, sistemas académicos, aplicaciones comerciales, sistemas de atención o plataformas empresariales.
| Capa | Tecnología | Función |
|---|---|---|
| Sistema operativo | Linux | Servidor y servicios |
| Backend | Python + Django | Lógica de la aplicación |
| Base de datos | PostgreSQL | Persistencia de datos |
| Servidor Python | Gunicorn | Ejecutar Django en producción |
| Servidor web | Nginx | Recibir tráfico web y servir contenido estático |
¿Por qué Django?
Python dispone de excelentes frameworks como Flask, FastAPI y Django. Para esta guía utilizaremos Django porque incluye muchas funciones que una aplicación empresarial termina necesitando tarde o temprano.
Django proporciona de serie ORM para bases de datos, formularios, protección CSRF, administración, autenticación, migraciones, templates y múltiples controles de seguridad.
Decisión para producción: utilizaremos Django 5.2 LTS. Aunque existen ramas posteriores, una versión LTS resulta especialmente atractiva cuando queremos una aplicación que permanezca mantenida durante varios años.
1. Preparar Linux
Los ejemplos utilizan una distribución basada en Debian o Ubuntu. En Fedora, AlmaLinux, Rocky Linux u otras distribuciones los nombres de algunos paquetes cambian, pero la arquitectura es exactamente la misma.
Primero instalamos los componentes principales:
sudo apt update
sudo apt install python3 python3-venv python3-pip postgresql postgresql-contrib nginx git
Podemos comprobar Python con:
python3 --version
Y PostgreSQL mediante:
psql --version
Consejo: no instales las dependencias Python de la aplicación directamente sobre el Python del sistema. Utiliza siempre un entorno virtual.
2. Crear la base de datos PostgreSQL
Ingresamos a PostgreSQL con la cuenta administrativa:
sudo -u postgres psql
Creamos un usuario exclusivo para la aplicación y una base de datos que le pertenezca:
CREATE ROLE gestor_user WITH LOGIN PASSWORD 'CAMBIA_ESTA_PASSWORD';
CREATE DATABASE gestor_db OWNER gestor_user;
\q
La aplicación tendrá así sus propias credenciales y no necesitará utilizar el superusuario de PostgreSQL.
No utilices la cuenta postgres desde la aplicación. Crea usuarios específicos con los privilegios mínimos necesarios y utiliza contraseñas robustas.
3. Crear el proyecto Python
Ahora creamos una carpeta para nuestra aplicación y un entorno virtual:
mkdir gestor-tareas
cd gestor-tareas
python3 -m venv .venv
source .venv/bin/activate
Actualizamos pip:
python -m pip install --upgrade pip
Instalamos Django, el controlador PostgreSQL, soporte para variables de entorno y Gunicorn:
pip install "Django>=5.2,<5.3" "psycopg[binary]" python-dotenv gunicorn
Después creamos el proyecto:
django-admin startproject config .
python manage.py startapp tareas
Y guardamos las dependencias:
pip freeze > requirements.txt
4. Nunca coloques contraseñas directamente en settings.py
La configuración de la base de datos y la clave secreta deben mantenerse fuera del repositorio.
Creamos un archivo:
.env
con esta información:
DJANGO_SECRET_KEY=cambia-esta-clave-por-una-muy-larga
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost
DB_NAME=gestor_db
DB_USER=gestor_user
DB_PASSWORD=CAMBIA_ESTA_PASSWORD
DB_HOST=127.0.0.1
DB_PORT=5432
Y añadimos al archivo .gitignore:
.env
.venv/
__pycache__/
staticfiles/
Así evitamos publicar accidentalmente credenciales en GitHub o cualquier otro repositorio.
5. Conectar Django con PostgreSQL
En config/settings.py añadimos la carga de variables de entorno:
import os
from dotenv import load_dotenv
load_dotenv(BASE_DIR / ".env")
Configuramos la seguridad básica:
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG", "False").lower() == "true"
ALLOWED_HOSTS = [
host.strip()
for host in os.environ.get(
"DJANGO_ALLOWED_HOSTS",
"127.0.0.1,localhost"
).split(",")
]
Y reemplazamos la configuración de base de datos por PostgreSQL:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["DB_NAME"],
"USER": os.environ["DB_USER"],
"PASSWORD": os.environ["DB_PASSWORD"],
"HOST": os.environ.get("DB_HOST", "127.0.0.1"),
"PORT": os.environ.get("DB_PORT", "5432"),
}
}
Dentro de INSTALLED_APPS incorporamos:
"tareas",
Y dejamos preparada la ubicación de archivos estáticos:
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
6. Crear nuestro primer modelo
Django permite definir la estructura de la base de datos utilizando clases Python.
En tareas/models.py escribimos:
from django.db import models
class Tarea(models.Model):
titulo = models.CharField(max_length=160)
descripcion = models.TextField(blank=True)
completada = models.BooleanField(default=False)
creada_en = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["completada", "-creada_en"]
def __str__(self):
return self.titulo
No tenemos que escribir manualmente una sentencia SQL para crear la tabla.
Django genera las migraciones:
python manage.py makemigrations
python manage.py migrate
Una de las grandes ventajas del ORM: el código Python describe el modelo y Django administra los cambios de esquema mediante migraciones versionables.
7. Crear el formulario
En tareas/forms.py añadimos:
from django import forms
from .models import Tarea
class TareaForm(forms.ModelForm):
class Meta:
model = Tarea
fields = ["titulo", "descripcion", "completada"]
Django podrá validar automáticamente el formulario utilizando la definición del modelo.
8. Crear la lógica de la aplicación
En tareas/views.py tendremos la pantalla principal y las acciones para completar y eliminar tareas:
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_POST
from .forms import TareaForm
from .models import Tarea
def inicio(request):
if request.method == "POST":
form = TareaForm(request.POST)
if form.is_valid():
form.save()
return redirect("inicio")
else:
form = TareaForm()
tareas = Tarea.objects.all()
return render(
request,
"tareas/inicio.html",
{
"form": form,
"tareas": tareas,
},
)
@require_POST
def cambiar_estado(request, tarea_id):
tarea = get_object_or_404(Tarea, pk=tarea_id)
tarea.completada = not tarea.completada
tarea.save(update_fields=["completada"])
return redirect("inicio")
@require_POST
def eliminar(request, tarea_id):
tarea = get_object_or_404(Tarea, pk=tarea_id)
tarea.delete()
return redirect("inicio")
9. Definir las URLs
Creamos tareas/urls.py:
from django.urls import path
from . import views
urlpatterns = [
path("", views.inicio, name="inicio"),
path(
"tareas/<int:tarea_id>/estado/",
views.cambiar_estado,
name="cambiar_estado",
),
path(
"tareas/<int:tarea_id>/eliminar/",
views.eliminar,
name="eliminar",
),
]
Después, en config/urls.py:
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("tareas.urls")),
]
10. Crear una interfaz sencilla
Creamos la carpeta tareas/templates/tareas y dentro el archivo inicio.html.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1">
<title>Gestor de tareas</title>
<style>
body {
font-family: system-ui, sans-serif;
max-width: 850px;
margin: 40px auto;
padding: 0 20px;
background: #f5f6f8;
}
.panel {
background: white;
padding: 24px;
margin-bottom: 20px;
border-radius: 10px;
}
input,
textarea {
box-sizing: border-box;
width: 100%;
padding: 10px;
}
button {
padding: 9px 14px;
cursor: pointer;
}
.tarea {
border-bottom: 1px solid #ddd;
padding: 15px 0;
}
.completada {
text-decoration: line-through;
opacity: 0.6;
}
.acciones form {
display: inline-block;
}
</style>
</head>
<body>
<h1>Mis tareas</h1>
<div class="panel">
<h2>Nueva tarea</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Guardar</button>
</form>
</div>
<div class="panel">
<h2>Tareas registradas</h2>
{% for tarea in tareas %}
<div class="tarea">
<h3 class="{% if tarea.completada %}completada{% endif %}">
{{ tarea.titulo }}
</h3>
<p>{{ tarea.descripcion }}</p>
<div class="acciones">
<form method="post"
action="{% url 'cambiar_estado' tarea.id %}">
{% csrf_token %}
<button type="submit">
Cambiar estado
</button>
</form>
<form method="post"
action="{% url 'eliminar' tarea.id %}">
{% csrf_token %}
<button type="submit">
Eliminar
</button>
</form>
</div>
</div>
{% empty %}
<p>Todavía no existen tareas.</p>
{% endfor %}
</div>
</body>
</html>
11. Probar la aplicación
Ejecutamos:
python manage.py runserver
Y abrimos:
http://127.0.0.1:8000
En este momento ya tenemos una aplicación funcional conectada a PostgreSQL.
Podemos crear tareas, consultar los registros, modificar su estado y eliminarlos.
Importante: manage.py runserver sirve únicamente para desarrollo. Django indica expresamente que no debe utilizarse como servidor de producción.
12. El salto de desarrollo a producción
Aquí comienza una de las partes que suelen quedar fuera de muchos tutoriales.
Una aplicación web no está terminada simplemente porque funciona en localhost.
En producción necesitamos considerar dominio, HTTPS, secretos, procesos persistentes, archivos estáticos, logs, backups y recuperación ante errores.
En producción cada componente tendrá una función clara:
Nginx recibirá las conexiones HTTP y HTTPS.
Gunicorn mantendrá ejecutándose nuestra aplicación Django.
Django procesará la lógica.
PostgreSQL conservará los datos.
13. Preparar el servidor Linux
Podemos utilizar un VPS Linux con una dirección IP pública y un dominio, por ejemplo:
app.midominio.com
En producción podemos colocar el proyecto en:
/srv/gestor
Después creamos nuevamente el entorno virtual e instalamos las dependencias:
cd /srv/gestor
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
14. Los secretos de producción van fuera del proyecto
En lugar de dejar un archivo .env junto al código, podemos utilizar un archivo de configuración protegido por Linux.
Por ejemplo:
sudo mkdir -p /etc/gestor
sudo nano /etc/gestor/gestor.env
Su contenido puede ser:
DJANGO_SECRET_KEY=CLAVE_SECRETA_MUY_LARGA
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=app.midominio.com
DB_NAME=gestor_db
DB_USER=gestor_user
DB_PASSWORD=PASSWORD_MUY_SEGURA
DB_HOST=127.0.0.1
DB_PORT=5432
Protegemos el archivo:
sudo chmod 640 /etc/gestor/gestor.env
Podemos generar una clave Django aleatoria con:
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
15. Preparar Django para producción
Con las variables cargadas debemos ejecutar las migraciones:
python manage.py migrate
Después recopilamos los archivos estáticos:
python manage.py collectstatic --noinput
Django incluye también una comprobación especialmente útil antes de publicar:
python manage.py check --deploy
En producción DEBUG debe estar desactivado. Una página de error de desarrollo puede revelar rutas, configuraciones y otra información que no debería mostrarse públicamente.
16. Probar Gunicorn
Antes de crear el servicio permanente podemos comprobar que Gunicorn funciona:
gunicorn --bind 127.0.0.1:8001 config.wsgi:application
Gunicorn queda escuchando solamente en localhost.
Esto es importante: no necesitamos exponer directamente el servidor Python a Internet porque Nginx será quien reciba las conexiones públicas.
17. Crear un servicio systemd
Creamos:
sudo nano /etc/systemd/system/gestor.service
Con una configuración similar:
[Unit]
Description=Aplicacion Django Gestor
After=network.target postgresql.service
[Service]
User=www-data
Group=www-data
WorkingDirectory=/srv/gestor
EnvironmentFile=/etc/gestor/gestor.env
ExecStart=/srv/gestor/.venv/bin/gunicorn \
--workers 3 \
--bind 127.0.0.1:8001 \
config.wsgi:application
Restart=on-failure
[Install]
WantedBy=multi-user.target
Ajustamos la propiedad del proyecto según nuestra política de despliegue y activamos el servicio:
sudo systemctl daemon-reload
sudo systemctl enable --now gestor
Podemos comprobarlo con:
sudo systemctl status gestor
Y consultar logs mediante:
journalctl -u gestor -f
18. Colocar Nginx delante de Django
Creamos:
sudo nano /etc/nginx/sites-available/gestor
Con una configuración básica:
server {
listen 80;
server_name app.midominio.com;
location /static/ {
alias /srv/gestor/staticfiles/;
}
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
La activamos:
sudo ln -s /etc/nginx/sites-available/gestor \
/etc/nginx/sites-enabled/gestor
sudo nginx -t
sudo systemctl reload nginx
En este momento nuestro dominio debería mostrar la aplicación utilizando HTTP.
19. HTTPS ya no debería ser opcional
Una aplicación real debe trabajar mediante HTTPS, especialmente si posteriormente añadiremos autenticación, formularios o información empresarial.
Certbot puede integrarse con Nginx y obtener certificados TLS utilizando Let's Encrypt.
Una vez instalado mediante el método recomendado para nuestro sistema podemos ejecutar:
sudo certbot --nginx -d app.midominio.com
Después de comprobar que HTTPS funciona correctamente podemos endurecer todavía más Django.
En settings.py:
SECURE_PROXY_SSL_HEADER = (
"HTTP_X_FORWARDED_PROTO",
"https",
)
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
También podemos definir:
CSRF_TRUSTED_ORIGINS = [
"https://app.midominio.com",
]
A partir de aquí ya tenemos una aplicación web realmente desplegada: dominio, HTTPS, servicio persistente, servidor web, aplicación Python y PostgreSQL.
20. El firewall también importa
En un servidor público no necesitamos publicar todos los servicios.
Normalmente deberían exponerse únicamente:
- SSH, restringido tanto como sea posible.
- HTTP.
- HTTPS.
PostgreSQL no necesita estar disponible públicamente si Django se conecta desde la misma máquina.
Gunicorn tampoco debería exponerse directamente si Nginx funciona como reverse proxy.
PostgreSQL no debería escuchar Internet sin necesidad
En nuestro escenario Django utiliza:
DB_HOST=127.0.0.1
Esto mantiene la conexión dentro del servidor.
En arquitecturas mayores, donde PostgreSQL está en otra máquina, deberían aplicarse firewall, TLS, autenticación robusta y redes privadas.
21. No publiques sin copias de seguridad
Una aplicación sin backup puede funcionar perfectamente durante meses y desaparecer en minutos después de un error humano, corrupción o fallo de infraestructura.
PostgreSQL dispone de pg_dump para realizar copias lógicas.
Por ejemplo:
pg_dump -Fc gestor_db > gestor_db.backup
Pero existe una regla todavía más importante:
Una copia de seguridad que nunca ha sido restaurada y probada es solamente una esperanza de recuperación.
Los backups deberían guardarse fuera del mismo servidor y probarse periódicamente.
22. Los logs son parte de la aplicación
Cuando algo falle en producción necesitaremos descubrir qué ocurrió.
Para Gunicorn podemos comenzar consultando:
journalctl -u gestor
Nginx mantiene además sus propios registros de acceso y errores.
En sistemas mayores podemos centralizar logs y añadir herramientas como Prometheus, Grafana, Loki, OpenTelemetry o soluciones equivalentes.
23. ¿Dónde entra Docker?
Docker puede resultar excelente para empaquetar aplicaciones, pero no es obligatorio para aprender ni para publicar Django correctamente.
De hecho, comprender primero:
- Linux.
- Python.
- PostgreSQL.
- Gunicorn.
- Nginx.
hace mucho más sencillo entender después qué está haciendo realmente un contenedor.
Una evolución natural del proyecto podría ser empaquetar Django y PostgreSQL mediante Docker Compose y posteriormente utilizar CI/CD.
24. ¿Y una API REST?
La aplicación actual devuelve HTML generado por Django, pero la misma base puede crecer hacia una arquitectura con frontend separado.
Una posible siguiente etapa consiste en incorporar:
Django REST Framework
y construir una API consumida desde React, Vue, aplicaciones móviles o sistemas externos.
PostgreSQL y la mayor parte del modelo de datos permanecerían iguales.
25. Qué añadiríamos para una aplicación empresarial
| Necesidad | Siguiente mejora |
|---|---|
| Usuarios | Autenticación Django |
| Roles | Permisos y grupos |
| API | Django REST Framework |
| Tareas en segundo plano | Celery o alternativas |
| Cache | Redis |
| Despliegue automático | CI/CD |
| Observabilidad | Logs, métricas y alertas |
El error más frecuente: empezar demasiado complicado
Es fácil pensar que toda aplicación moderna necesita inmediatamente Kubernetes, microservicios, múltiples bases de datos, colas de mensajes y una enorme infraestructura cloud.
Muchas aplicaciones no necesitan nada de eso al comenzar.
Una aplicación Django bien construida, PostgreSQL, Gunicorn y Nginx pueden atender proyectos reales durante mucho tiempo. La arquitectura debe crecer cuando el problema lo exija, no únicamente porque una tecnología esté de moda.
Checklist antes de publicar
- DEBUG desactivado.
- Clave secreta fuera del repositorio.
- Credenciales PostgreSQL protegidas.
- Dominio configurado en ALLOWED_HOSTS.
- HTTPS habilitado.
- PostgreSQL no expuesto innecesariamente.
- Gunicorn ejecutándose como servicio.
- Nginx validado con nginx -t.
- Migraciones aplicadas.
- Archivos estáticos recopilados.
- manage.py check --deploy ejecutado.
- Backups configurados y probados.
- Logs disponibles.
- Sistema y dependencias actualizados.
Puede leer también | Las mejores distribuciones Linux para programadores Python
¿Por qué aprender este stack sigue teniendo sentido?
La llegada de asistentes de inteligencia artificial permite generar aplicaciones cada vez más rápidamente, pero no elimina la necesidad de comprender cómo funcionan.
Una IA puede escribir un modelo Django o una configuración Nginx. El profesional todavía debe saber si esa configuración expone PostgreSQL, deja DEBUG activado, utiliza una contraseña dentro del repositorio o permite que la aplicación ejecute procesos con privilegios excesivos.
Aprender el recorrido completo permite entender algo mucho más importante que simplemente escribir Python: cómo una aplicación pasa desde nuestro editor hasta convertirse en un servicio disponible y mantenible en Internet.
Puede leer también | 10 herramientas poderosas de Software Libre para transformar su empresa
Recomendamos
- Las mejores distribuciones Linux para programadores Python
- Herramientas esenciales para proteger tu Sistema Operativo Linux de Hackers
- 10 herramientas poderosas de Software Libre para transformar su empresa
En resumen
Python, PostgreSQL y Linux continúan formando una combinación excelente para desarrollar aplicaciones web reales.
Django proporciona la estructura del proyecto, formularios, ORM, migraciones y controles de seguridad. PostgreSQL se ocupa de los datos. Gunicorn mantiene la aplicación Python funcionando y Nginx gestiona las conexiones que llegan desde Internet.
Lo más importante es que la misma arquitectura puede comenzar como un pequeño CRUD y evolucionar posteriormente hacia autenticación, APIs, sistemas empresariales, automatización y servicios mucho mayores.
Conclusión editorial
Aprender desarrollo web únicamente hasta que aparece una página en localhost:8000 significa aprender solamente una parte del trabajo.
Una aplicación real necesita datos persistentes, variables de entorno, permisos, HTTPS, procesos supervisados, copias de seguridad y una infraestructura capaz de reiniciarse después de un fallo.
Precisamente allí Linux demuestra por qué continúa siendo una plataforma tan importante para desarrolladores.
Permite comprender y controlar cada pieza: desde el proceso Python hasta la base de datos, los puertos, Nginx, los certificados y los logs.
Y ésa es la verdadera diferencia entre “hacer una aplicación” y saber llevar una aplicación a producción.
La inteligencia artificial puede acelerar enormemente la escritura del código. Los frameworks también pueden ocultar una gran cantidad de complejidad. Pero alguien continúa necesitando comprender dónde están los datos, qué proceso atiende las peticiones, qué servicio está expuesto a Internet y cómo recuperar el sistema cuando algo falla.
Por eso aprender este stack sigue siendo una excelente inversión: no enseña solamente Python.
Enseña cómo funciona realmente una aplicación web moderna de principio a fin.
Fuentes: Python Software Foundation — Python 3.14.8, Django — Django 5.2 LTS, Django — Deployment, Django — Deployment Checklist, PostgreSQL — documentación oficial, Nginx — Reverse Proxy y Certbot — HTTPS con Let's Encrypt.

