Cómo reconstruí mi blog para que funcionara con Flask (toda la historia técnica completa)
Este es el segundo primer post de mi blog. Primero porque es el primero que aparece, pero segundo porque hubo otro antes, pero dadas las caracterísitcas de lo que tuve que hacer, el otro era inservible y por eso que este sesa el segundo primer post. En este documento quiero dejar por escrito todo el proceso técnico real que he seguido para reconstruir mi blog y convertirlo en una aplicación web completa usando Flask, Gunicorn, Nginx, Markdown dinámico, plantillas, rutas, permisos, venv y servicios systemd.
Este post es largo, técnico y detallado. Es exactamente lo que me habría gustado leer antes de empezar con este lío, así que espero poder inspirar a alguien. Si no, que al menos sirva como entretenimiento o motivación para que le deis una segunda vida a un ordenador viejo!
1. Estructura profesional del proyecto
Lo primero fue reorganizar todo mi servidor para que Flask pudiera servir la web completa. La estructura final quedó así:
/var/www/main_web/ app.py venv-web/ templates/ index.html plantilla.html blog.html static/ css/ estilos.css estilos_blog.css estilos_main.css img/ posts/ md/ post1.md post2.md html/
Permisos
Para que Nginx y Gunicorn puedan leer todo:
sudo chown -R serherm:www-data /var/www/main_web sudo chmod -R 755 /var/www/main_web
2. Entorno virtual (venv)
Para aislar Flask y sus dependencias:
python3 -m venv venv-web source venv-web/bin/activate pip install flask markdown gunicorn
3. El corazón del sistema: app.py
Aquí es donde Flask toma el control de toda la web.
Rutas principales
/ → página principal
/blog → índice del blog
/blog/posts/
Conversión de Markdown a HTML
import markdown from flask import Flask, render_template
app = Flask(name)
@app.route("/blog/posts/
with open(ruta_md, "r", encoding="utf-8") as f:
contenido_md = f.read()
contenido_html = markdown.markdown(contenido_md)
return render_template("plantilla.html",
titulo=post,
contenido=contenido_html)
4. Plantillas HTML con Jinja2
Flask usa Jinja2, así que puedo mezclar HTML con variables.
La plantilla del post (plantilla.html)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>{{ titulo }}</title>
<link rel="stylesheet" href="/static/css/estilos.css">
</head>
<body>
<header class="header">
<div class="logo">serblog</div>
<nav class="nav">
<a href="/" class="nav-btn">Inicio</a>
<a href="/blog" class="nav-btn">Blog</a>
</nav>
</header>
<main class="post-container">
<h1 class="post-title">{{ titulo }}</h1>
<article class="post-content">
{{ contenido|safe }}
</article>
</main>
<footer class="footer">
Sergio © 2026 ®
</footer>
</body>
</html>
5. Sistema de posts en Markdown
Cada post es un archivo .md dentro de:
/var/www/main_web/posts/md/
Ejemplo:
# Título del post
Contenido en Markdown...
Flask lo convierte a HTML en tiempo real, así que no necesito generar archivos HTML.
6. Nginx: el guardián del servidor
Nginx se encarga de:
- servir /static
- manejar HTTPS
- redirigir HTTP → HTTPS
- pasar todo lo demás a Flask/Gunicorn
Mi configuración final
server { listen 80; server_name serblog.hopto.org; return 301 https://$host$request_uri; }
server { listen 443 ssl; server_name serblog.hopto.org;
ssl_certificate /etc/ssl/certs/servidor.crt;
ssl_certificate_key /etc/ssl/private/servidor.key;
root /var/www/main_web;
index index.html;
location /static/ {
alias /var/www/main_web/static/;
}
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}
7. Gunicorn: el servidor WSGI
Gunicorn es el servidor que ejecuta Flask en producción. Flask por sí solo no es apto para producción, así que Gunicorn se encarga de:
- crear varios workers (procesos)
- gestionar peticiones concurrentes
- reiniciar workers cuando fallan
- mantener la aplicación viva
Servicio systemd
Archivo:
/etc/systemd/system/main_web.service
Contenido:
[Unit] Description=Gunicorn service for main_web After=network.target
[Service] User=serherm Group=www-data WorkingDirectory=/var/www/main_web Environment="PATH=/var/www/main_web/venv-web/bin" ExecStart=/var/www/main_web/venv-web/bin/gunicorn -w 4 -b 127.0.0.1:8000 app:app
[Install] WantedBy=multi-user.target
Significado técnico de cada parámetro
User=serherm
Gunicorn se ejecuta como mi usuario, no como root.
Group=www-data
Permite que Nginx (que usa www-data) acceda a los archivos.
WorkingDirectory
Directorio donde está app.py.
Environment="PATH=..."
Obliga a systemd a usar el Python del venv.
ExecStart
Comando real que lanza Gunicorn:
gunicorn -w 4 -b 127.0.0.1:8000 app:app
- -w 4 → 4 workers (4 procesos atendiendo peticiones)
- -b 127.0.0.1:8000 → escucha en localhost:8000
- app:app → archivo app.py, variable app
Comandos para controlar el servicio
sudo systemctl start main_web
sudo systemctl stop main_web
sudo systemctl restart main_web
sudo systemctl status main_web
Reinicio limpio (muy importante)
Cuando cambio plantillas o código:
sudo systemctl stop main_web
pkill gunicorn
sudo systemctl start main_web
Esto garantiza que todos los workers cargan la versión nueva del código.
8. El problema más raro que he tenido (y cómo lo resolví)
Durante el desarrollo, me encontré con un bug extraño:
- A veces el post se veía bonito.
- A veces se veía sin estilos.
- A veces el HTML del Markdown aparecía “a pelo”.
- A veces Flask usaba una plantilla vieja.
- A veces Chrome cargaba un CSS que ya no existía.
La causa final fue:
Chrome estaba usando una versión antigua del HTML en caché.
La solución:
Ctrl + Shift + R
Ctrl + F5
modo incógnito
reinicio limpio de Gunicorn
9. Conclusión
Ahora mi blog funciona con:
- Flask sirviendo toda la web
- Markdown convertido dinámicamente
- Gunicorn manejando los workers
- Nginx gestionando HTTPS y /static
- una estructura profesional
- plantillas limpias
- rutas claras
- permisos correctos
- un despliegue estable
Este post resume todo el camino técnico real que he seguido para reconstruir mi blog y convertirlo en una aplicación web completa.
10. Próximos pasos
- Añadir paginación al blog
- Añadir categorías
- Añadir un panel de administración
- Añadir comentarios locales
- Añadir un sistema de posts con metadatos YAML