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:
- estructura de carpetas
- permisos
- preparación de Flask, Gunicorn y Nginx
- orden de las peticiones
- código Python
- HTML y CSS
- problemas que surgieron
- cómo los solucioné
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:
- Flask usa por defecto:
templates/para HTMLstatic/para archivos estáticos- Separar
static/qr/permite: - borrar solo los QR sin tocar otros recursos
- mantener orden y claridad
- Usar
/var/www/mantiene coherencia con otros proyectos del servidor.
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:
- Gunicorn ejecuta Flask como el usuario
serherm. - Si
serhermes el owner, Flask puede escribir PNG sin problemas. 755permite:- escritura solo al owner
- lectura y ejecución a Nginx y otros usuarios
- No se usa
777porque es inseguro.
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)
-
El navegador del usuario hace una petición HTTP. El usuario escribe una URL en el navegador. Esa petición llega al servidor.
-
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:
- si es un archivo estático, lo sirve él mismo,
-
si es una ruta dinámica, la reenvía al backend.
-
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.
-
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.”
-
Gunicorn recibe la petición y ejecuta Flask. Gunicorn carga app.py y la instancia Flask. Cuando llega una petición:
- busca la ruta correspondiente,
- ejecuta la función Python asociada,
-
genera la respuesta (HTML).
-
Flask hace la lógica de la aplicación. Aquí ocurre todo lo importante:
- si es la ruta principal, devuelve el formulario,
- si es la ruta /generar, procesa el POST,
- valida la URL,
- genera el QR,
- guarda el PNG,
- construye la URL pública,
-
renderiza la plantilla HTML.
-
Gunicorn recibe el HTML generado por Flask. Gunicorn no modifica nada, solo actúa como intermediario.
-
Gunicorn devuelve la respuesta a Nginx. Nginx recibe el HTML final.
-
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í:
- El entorno virtual evita conflictos con otros proyectos.
qrcodeyPillowpermiten generar imágenes PNG.gunicornes el servidor WSGI recomendado para producción.
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í:
User=serherm: Gunicorn corre como mi usuario, que tiene permisos sobre el proyecto.Group=www-data: compatible con Nginx.WorkingDirectory: Gunicorn se ejecuta dentro del proyecto.Environment: asegura que use el entorno virtual.gunicorn -w 3: tres workers para manejar varias peticiones.-b 127.0.0.1:8020: Gunicorn escucha solo en localhost, Nginx lo recibe.
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í:
- Nginx recibe la petición del navegador.
- La pasa a Gunicorn en el puerto 8020.
- Gunicorn ejecuta Flask.
- Flask devuelve HTML.
- Nginx lo entrega al navegador.
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:
- El usuario abre
/proyectos/qrapp/. - Nginx recibe la petición.
- Nginx la pasa a Gunicorn (127.0.0.1:8020).
- Gunicorn ejecuta Flask → devuelve el formulario.
- El usuario envía el formulario (POST) a
/proyectos/qrapp/generar. - Nginx pasa el POST a Gunicorn.
- Flask:
- valida la URL
- genera el PNG
- guarda el archivo
- construye la URL pública
- devuelve HTML con enlaces
- El navegador muestra:
- mensaje
- enlace para ver el QR
- 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í:
urlparsepermite validar URLs.timepermite generar nombres únicos.QR_FOLDERes ruta absoluta para evitar errores de contexto.
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í:
- Evita generar QR con texto basura.
- Exige protocolo (
httpohttps). - Exige dominio (
netloc).
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í:
- El POST procesa el formulario.
- Si la URL es inválida → mensaje de error.
- Si es válida:
- se genera el PNG,
- se guarda,
- se construye la URL pública,
- se devuelve HTML con enlaces.
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 %}
Por qué así:
target="_blank"evita perder la página.downloadfuerza la descarga del PNG.- Los
<br>arreglan el problema de que todo salía en una línea. - El botón “Volver” mejora la navegación.
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:
- simple,
- robusta,
- bien estructurada,
- con permisos correctos,
- con validación de URLs,
- con generación de QR en tiempo real,
- con enlaces para ver y descargar,
- con un flujo claro entre Nginx → Gunicorn → Flask.
Y sobre todo, entendí profundamente cómo se encadenan todos los componentes y por qué cada paso es necesario.