Construcción completa de QRApp en Flask + Nginx + Gunicorn

(explicado en orden real y con el porqué de cada paso)

Este documento resume todo el proceso real que seguí para construir mi aplicación QRApp, incluyendo:

Todo está explicado en el mismo orden en que lo hicimos y con el por qué detrás de cada decisión.


1. Estructura de carpetas y permisos

1.1. Estructura del proyecto

El proyecto vive en /var/www/qrapp, siguiendo la convención de alojar proyectos web en /var/www/.

/var/www/qrapp/ │ ├── app.py ├── venv-qr/ │ ├── static/ │ └── qr/ │ └── (PNG generados) │ └── templates/ └── index.html

Por qué esta estructura:


1.2. Permisos de la carpeta static/qr

Al principio, Flask no podía guardar los PNG y lanzaba:

PermissionError: [Errno 13] Permission denied

La causa era que la carpeta pertenecía a root.

Solución:

sudo chown -R serherm:serherm /var/www/qrapp/static/qr sudo chmod -R 755 /var/www/qrapp/static/qr

Por qué funciona:


2. Preparación del entorno: Flask + Gunicorn + Nginx

Aquí empieza la parte donde todo se conecta: navegador → Nginx → Gunicorn → Flask.

Antes de ver qué he hecho, es importante saber el motivo, y el flujo real. Esto es lo que ocurre bajo el capó:

Resumen del flujo completo entre Nginx, Gunicorn y Flask (solo texto, formato terminal)

  1. El navegador del usuario hace una petición HTTP. El usuario escribe una URL en el navegador. Esa petición llega al servidor.

  2. Nginx es el primero que recibe la petición. Nginx es el servidor frontal. Está escuchando en los puertos públicos (80 y 443). Su trabajo es decidir qué hacer con cada petición:

  3. si es un archivo estático, lo sirve él mismo,
  4. si es una ruta dinámica, la reenvía al backend.

  5. Nginx detecta que la ruta pertenece a la aplicación QRApp. Según la configuración, cualquier URL que empiece por /proyectos/qrapp/ debe ser enviada al backend Python.

  6. Nginx reenvía la petición a Gunicorn. Gunicorn es el servidor WSGI que ejecuta Flask. Nginx le pasa la petición mediante proxy_pass hacia 127.0.0.1:8020. Esto significa: “Gunicorn, procesa tú esta petición dinámica.”

  7. Gunicorn recibe la petición y ejecuta Flask. Gunicorn carga app.py y la instancia Flask. Cuando llega una petición:

  8. busca la ruta correspondiente,
  9. ejecuta la función Python asociada,
  10. genera la respuesta (HTML).

  11. Flask hace la lógica de la aplicación. Aquí ocurre todo lo importante:

  12. si es la ruta principal, devuelve el formulario,
  13. si es la ruta /generar, procesa el POST,
  14. valida la URL,
  15. genera el QR,
  16. guarda el PNG,
  17. construye la URL pública,
  18. renderiza la plantilla HTML.

  19. Gunicorn recibe el HTML generado por Flask. Gunicorn no modifica nada, solo actúa como intermediario.

  20. Gunicorn devuelve la respuesta a Nginx. Nginx recibe el HTML final.

  21. Nginx entrega la respuesta al navegador. El navegador muestra la página al usuario.

Resumen ultra corto: Nginx recibe la petición → se la pasa a Gunicorn → Gunicorn ejecuta Flask → Flask genera la respuesta → Gunicorn la devuelve → Nginx la entrega al navegador.


2.1. Entorno virtual y dependencias

cd /var/www/qrapp python3 -m venv venv-qr source venv-qr/bin/activate pip install flask qrcode pillow gunicorn

Por qué así:


2.2. Gunicorn como servicio systemd

Archivo:

/etc/systemd/system/qrapp.service

Contenido:

[Unit] Description=Servicio QRApp con Gunicorn After=network.target

[Service] User=serherm Group=www-data WorkingDirectory=/var/www/qrapp Environment="PATH=/var/www/qrapp/venv-qr/bin" ExecStart=/var/www/qrapp/venv-qr/bin/gunicorn -w 3 -b 127.0.0.1:8020 app:app

[Install] WantedBy=multi-user.target

Por qué así:

Comandos:

sudo systemctl enable qrapp sudo systemctl restart qrapp sudo systemctl status qrapp


2.3. Nginx como proxy inverso

Bloque dentro de /etc/nginx/sites-available/default:

location /proyectos/qrapp/ { proxy_pass http://127.0.0.1:8020; 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; }

Por qué así:

Recarga:

sudo nginx -t sudo systemctl reload nginx


3. Orden real de las peticiones (quién llama a quién)

Este es el flujo completo:

  1. El usuario abre /proyectos/qrapp/.
  2. Nginx recibe la petición.
  3. Nginx la pasa a Gunicorn (127.0.0.1:8020).
  4. Gunicorn ejecuta Flask → devuelve el formulario.
  5. El usuario envía el formulario (POST) a /proyectos/qrapp/generar.
  6. Nginx pasa el POST a Gunicorn.
  7. Flask:
  8. valida la URL
  9. genera el PNG
  10. guarda el archivo
  11. construye la URL pública
  12. devuelve HTML con enlaces
  13. El navegador muestra:
  14. mensaje
  15. enlace para ver el QR
  16. enlace para descargar el QR

4. Código Python de la aplicación

Archivo: app.py


4.1. Importaciones y configuración

``` import os import time from urllib.parse import urlparse

from flask import Flask, render_template, request import qrcode

app = Flask( name)

QR_FOLDER = "/var/www/qrapp/static/qr" ```

Por qué así:


4.2. Validación de URL

def es_url_valida(texto): try: resultado = urlparse(texto) return resultado.scheme in ["http", "https"] and bool(resultado.netloc) except: return False

Por qué así:


4.3. Ruta principal

@app.route("/proyectos/qrapp/") def index(): return render_template("index.html")


4.4. Ruta /generar

``` @app.route("/generar", methods=["POST"]) def generar(): texto = request.form.get("texto") print("Texto recibido: ", texto)

if not es_url_valida(texto): mensaje = "La URL introducida no es válida. Por favor, revise el formato." return render_template("index.html", mensaje=mensaje)

nombre_archivo = f"qr_{int(time.time())}.png" ruta_completa = os.path.join(QR_FOLDER, nombre_archivo)

img = qrcode.make(texto) img.save(ruta_completa)

url_qr = f"/proyectos/qrapp/static/qr/{nombre_archivo}"

mensaje = "Se ha generado el QR correctamente."

return render_template("index.html", mensaje=mensaje, url_qr=url_qr) ```

Por qué así:


5. HTML y CSS

Archivo: templates/index.html


5.1. HTML

{% if mensaje %}

{{ mensaje }}

{% endif %}

{% if url_qr %}

El enlace al QR es:
{{ url_qr }}

Ver QR
Descargar QR

{% endif %}

Volver

Por qué así:


5.2. CSS

.volver-btn { display: inline-block; margin-top: 20px; padding: 10px 15px; background-color: #4a90e2; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; }

.volver-btn:hover { background-color: #357ab8; }

resultado a {

display: inline-block; margin-top: 5px; padding: 8px 12px; background-color: #4a90e2; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; }

resultado a:hover {

background-color: #357ab8; }


6. Problemas reales y cómo los solucioné


6.1. PermissionError al guardar PNG

Causa:
La carpeta static/qr era de root.

Solución:

sudo chown -R serherm:serherm /var/www/qrapp/static/qr sudo chmod -R 755 /var/www/qrapp/static/qr


6.2. Chrome mostrando “Confirmar reenvío del formulario”

Causa:
La ruta /generar devuelve HTML directamente desde un POST.

Intento:
Implementamos PRG (POST → Redirect → GET).

Resultado:
Decidí mantener el comportamiento original porque el aviso no afecta al uso real.


6.3. Internal Server Error al probar PRG

Causa:
Nginx no tenía ruta para /resultado.

Solución:
Volver al diseño original sin PRG.


6.4. Todo en una línea en el HTML

Causa:
Faltaban saltos de línea.

Solución:
Añadir <br> y separar bloques.


7. Conclusión

QRApp quedó como una aplicación:

Y sobre todo, entendí profundamente cómo se encadenan todos los componentes y por qué cada paso es necesario.