Desplegar una pila Flask PostgreSQL Nginx es el camino estándar para llevar una aplicación web Python de tu equipo local a un servidor de producción en vivo. Esta guía recorre cada capa: una base de datos PostgreSQL, Gunicorn como servidor de aplicaciones WSGI, systemd para mantener el proceso en ejecución y Nginx como proxy inverso orientado a internet. Al finalizar tendrás una configuración robusta y lista para producción sobre la que podrás seguir construyendo.

Requisitos Previos

  • Servidor Ubuntu 22.04 o 24.04 (acceso root o sudo)
  • Un nombre de dominio apuntando a la IP del servidor (opcional para SSL después)
  • Familiaridad básica con la línea de comandos de Linux y Python
  • Una aplicación Flask lista para desplegar (o usa el ejemplo mínimo de esta guía)

Instalación de Dependencias del Sistema

Comienza actualizando el índice de paquetes e instalando todo lo necesario en un solo paso:

sudo apt update && sudo apt upgrade -y
sudo apt install -y python3 python3-pip python3-venv \
    postgresql postgresql-contrib \
    nginx curl

Verifica las instalaciones:

python3 --version    # Python 3.10+
psql --version       # psql 14+
nginx -v             # nginx/1.22+

Configuración de PostgreSQL

Crear la Base de Datos y el Usuario

Cambia a la cuenta del sistema postgres y abre el shell interactivo:

sudo -u postgres psql

Dentro de psql, crea un usuario y una base de datos dedicados. Nunca uses el superusuario postgres por defecto para las conexiones de la aplicación:

CREATE USER flaskapp WITH PASSWORD 'StrongPassw0rd!';
CREATE DATABASE flaskdb OWNER flaskapp;
GRANT ALL PRIVILEGES ON DATABASE flaskdb TO flaskapp;
\q

Probar la Conexión

Confirma la conectividad antes de integrarla en Flask:

psql -U flaskapp -h 127.0.0.1 -d flaskdb -c "\conninfo"

Deberías ver la confirmación de que la conexión fue exitosa. Si falla, comprueba que pg_hba.conf permita autenticación md5 o scram-sha-256 para conexiones locales:

sudo nano /etc/postgresql/14/main/pg_hba.conf
# Asegúrate de que exista esta línea:
# host  all  all  127.0.0.1/32  scram-sha-256
sudo systemctl restart postgresql

Despliegue de la Aplicación Flask

Estructura del Proyecto

Coloca tu aplicación en /srv/flaskapp. Usar /srv mantiene las aplicaciones web separadas de los archivos del sistema:

/srv/flaskapp/
├── venv/
├── app/
│   ├── __init__.py
│   └── models.py
├── wsgi.py
├── .env
└── requirements.txt

Crear el Entorno Virtual

sudo mkdir -p /srv/flaskapp
sudo chown $USER:$USER /srv/flaskapp
cd /srv/flaskapp
python3 -m venv venv
source venv/bin/activate

Instalar Paquetes Python

pip install flask flask-sqlalchemy psycopg2-binary gunicorn python-dotenv
pip freeze > requirements.txt

Aplicación Flask Mínima

Crea app/__init__.py:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os

db = SQLAlchemy()

def create_app():
    app = Flask(__name__)
    app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["DATABASE_URL"]
    app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
    app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]

    db.init_app(app)

    @app.route("/")
    def index():
        return "Flask + PostgreSQL + Nginx is running."

    return app

Crea wsgi.py (el punto de entrada que llama Gunicorn):

from app import create_app

app = create_app()

if __name__ == "__main__":
    app.run()

Variables de Entorno

Almacena los secretos en /srv/flaskapp/.env. Nunca confirmes este archivo en el control de versiones:

DATABASE_URL=postgresql://flaskapp:StrongPassw0rd!@127.0.0.1:5432/flaskdb
SECRET_KEY=replace-with-a-long-random-string
FLASK_ENV=production

Restringe los permisos inmediatamente:

chmod 600 /srv/flaskapp/.env

Probar Gunicorn Manualmente

Antes de configurar systemd, verifica que Gunicorn pueda arrancar tu app:

source /srv/flaskapp/venv/bin/activate
cd /srv/flaskapp
gunicorn --workers 3 --bind 0.0.0.0:8000 wsgi:app

Visita http://your-server-ip:8000 — deberías ver la respuesta de Flask. Si es así, detén Gunicorn con Ctrl+C y pasa a systemd.

Servicio Systemd para Gunicorn

Ejecutar Gunicorn bajo systemd te proporciona reinicios automáticos ante fallos, arranque al inicio y registro centralizado mediante journalctl.

Elegir la Cantidad de Workers

RAM del servidorWorkers recomendados
512 MB2
1 GB3
2 GB5
4 GB+9

Una fórmula habitual: (2 × núcleos de CPU) + 1. Tres workers es un valor seguro por defecto para la mayoría de instancias VPS pequeñas.

Crear el Archivo de Unidad del Servicio

sudo nano /etc/systemd/system/flaskapp.service

Pega lo siguiente — ajusta las rutas y el usuario según sea necesario:

[Unit]
Description=Gunicorn instance for Flask app
After=network.target postgresql.service

[Service]
User=www-data
Group=www-data
WorkingDirectory=/srv/flaskapp
EnvironmentFile=/srv/flaskapp/.env
ExecStart=/srv/flaskapp/venv/bin/gunicorn \
    --workers 3 \
    --bind unix:/run/flaskapp/gunicorn.sock \
    --access-logfile /var/log/flaskapp/access.log \
    --error-logfile /var/log/flaskapp/error.log \
    wsgi:app
RuntimeDirectory=flaskapp
RuntimeDirectoryMode=0755

[Install]
WantedBy=multi-user.target

Explicación de los campos clave:

  • EnvironmentFile — carga los secretos de tu .env en el entorno del proceso
  • RuntimeDirectory — systemd crea automáticamente /run/flaskapp/ y lo elimina al detener el servicio
  • After=postgresql.service — garantiza que la base de datos esté activa antes de arrancar la app
  • Socket unix: — más rápido que TCP para la comunicación local entre Nginx y Gunicorn

Crea el directorio de logs y ajusta la propiedad:

sudo mkdir -p /var/log/flaskapp
sudo chown www-data:www-data /var/log/flaskapp
sudo chown -R www-data:www-data /srv/flaskapp

Habilita e inicia el servicio:

sudo systemctl daemon-reload
sudo systemctl enable flaskapp
sudo systemctl start flaskapp
sudo systemctl status flaskapp

Deberías ver Active: active (running). Comprueba que el socket fue creado:

ls -la /run/flaskapp/gunicorn.sock

Configuración del Proxy Inverso en Nginx

Nginx se sitúa delante de Gunicorn, gestiona el tráfico HTTP/HTTPS entrante y reenvía las solicitudes de la aplicación al socket Unix.

Crear la Configuración del Sitio

sudo nano /etc/nginx/sites-available/flaskapp
server {
    listen 80;
    server_name example.com www.example.com;

    # Archivos estáticos servidos directamente por Nginx — sin sobrecarga de Gunicorn
    location /static/ {
        alias /srv/flaskapp/app/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    location / {
        proxy_pass http://unix:/run/flaskapp/gunicorn.sock;
        proxy_http_version 1.1;

        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;

        # Evitar que los clientes lentos bloqueen los workers de Gunicorn
        proxy_read_timeout 90;
        proxy_connect_timeout 90;
    }
}

Habilita el sitio y prueba la configuración:

sudo ln -s /etc/nginx/sites-available/flaskapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Añadir SSL con Certbot

En producción, siempre termina TLS en Nginx. Certbot automatiza la emisión y renovación de certificados:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Certbot reescribe tu configuración de Nginx con las directivas SSL y configura la renovación automática mediante un temporizador systemd.

Escenario Real

Tienes una aplicación SaaS en producción construida con Flask que procesa pedidos de clientes almacenados en PostgreSQL. El tráfico alcanza su pico al mediodía con ~200 usuarios concurrentes. Sin el almacenamiento en búfer de Nginx, los clientes móviles lentos mantendrían los workers de Gunicorn ocupados durante más de 30 segundos cada uno, provocando timeouts. Con la configuración de esta guía:

  1. Nginx acepta la conexión inmediatamente y almacena en búfer al cliente lento
  2. Reenvía el cuerpo completo de la solicitud a Gunicorn en milisegundos
  3. Gunicorn procesa la solicitud y escribe la respuesta de vuelta a Nginx
  4. Nginx transmite la respuesta al cliente lento mientras Gunicorn ya está libre para la siguiente solicitud

Tus tres workers de Gunicorn pueden atender ahora a muchos más de tres usuarios concurrentes. Añade más workers o cambia a workers asíncronos gevent cuando necesites mayor escala.

Errores Comunes y Casos Especiales

Permisos del socket Unix: El usuario www-data de Nginx debe poder leer el socket de Gunicorn. Ejecutar ambos bajo www-data (como se muestra arriba) es la solución más sencilla. Si se ejecutan como usuarios distintos, añade www-data al grupo de la app y establece el socket en 660.

DATABASE_URL con caracteres especiales: Las contraseñas que contienen @, / o % deben estar codificadas en porcentaje en la URL. Como alternativa, usa variables de entorno individuales (PGUSER, PGPASSWORD, etc.) y construye la URL en Python.

python-dotenv ausente en producción: EnvironmentFile en systemd lee el .env directamente — no necesitas python-dotenv para la ruta systemd. Sí lo necesitas si ejecutas flask run en local.

Patrón application factory vs. app a nivel de módulo: wsgi:app de Gunicorn espera un callable. Con el patrón factory, expón la app creada a nivel de módulo en wsgi.py como se muestra arriba. No llames a create_app() dentro de una función sin exponer el resultado.

max_connections de PostgreSQL: Cada worker de Gunicorn abre su propia conexión a la base de datos. Con 9 workers en 4 servidores podrías alcanzar el límite predeterminado de 100 conexiones. Usa PgBouncer como agrupador de conexiones delante de PostgreSQL en despliegues con muchos workers.

Solución de Problemas

502 Bad Gateway desde Nginx: Comprueba que el servicio Gunicorn esté en ejecución y que el socket exista:

sudo systemctl status flaskapp
ls /run/flaskapp/gunicorn.sock

Revisa también los logs de errores de Nginx: sudo tail -50 /var/log/nginx/error.log

[CRITICAL] WORKER TIMEOUT en los logs de Gunicorn: Una solicitud está tardando más del timeout predeterminado de 30 segundos. Auméntalo con --timeout 120 u optimiza la consulta lenta que lo provoca.

OperationalError: could not connect to server: PostgreSQL no está en ejecución o el DATABASE_URL es incorrecto. Prueba la cadena de conexión directamente con psql $DATABASE_URL.

Permission denied en el socket: Discrepancia de usuario entre Nginx y Gunicorn. Verifica que ambos se ejecuten como www-data:

ps aux | grep -E "gunicorn|nginx" | awk '{print $1, $11}'

Archivos estáticos devuelven 404: La ruta alias en Nginx debe coincidir con tu directorio estático real. Nota: alias requiere barra diagonal al final; root no.

Resumen

  • Instala PostgreSQL, crea un usuario y base de datos dedicados, y restringe los permisos del archivo .env para mantener las credenciales seguras
  • Ejecuta Flask bajo Gunicorn con un socket Unix — nunca expongas Gunicorn directamente a internet
  • Usa una unidad systemd [Service] con EnvironmentFile y RuntimeDirectory para una gestión limpia del proceso y reinicio automático
  • Configura Nginx para hacer proxy de / al socket de Gunicorn y servir /static/ directamente, eliminando carga innecesaria en el servidor de aplicaciones
  • Añade SSL mediante Certbot de inmediato — HTTP plano nunca es aceptable en producción
  • Vigila el agotamiento del pool de conexiones con PostgreSQL al escalar los workers de Gunicorn; usa PgBouncer si es necesario

Artículos Relacionados