Infraestructura en la nube para mi portafolio: Oracle Cloud, Docker y Nginx
Proyecto personal
El objetivo
Quería que mi portafolio no solo mostrara proyectos, sino que también fuera uno: alojado en un servidor que configuré yo, sin hosting administrado ni paneles que lo hagan todo por mí. Este sitio, n8n y Uptime Kuma corren en esa misma infraestructura.
El servidor
Una máquina virtual en Oracle Cloud (capa gratuita Always Free) con procesador ARM y Ubuntu 24.04, con IP pública reservada para que la dirección no cambie si el servidor se reinicia. Configuré una alerta de presupuesto para que cualquier cobro inesperado me avise de inmediato.
Contenedores
Todos los servicios corren en Docker, orquestados con Docker Compose: WordPress con MySQL, phpMyAdmin, n8n y Uptime Kuma. Ningún contenedor queda expuesto a internet: cada uno escucha solo en la red interna del servidor (127.0.0.1), y las contraseñas viven en un archivo de variables de entorno con permisos restringidos, nunca en el código.
Nginx como única puerta de entrada
Nginx recibe todo el tráfico y lo reparte a cada servicio según el dominio: el portafolio, n8n y la página de estado tienen cada uno su subdominio. Todos funcionan con HTTPS gracias a certificados de Let’s Encrypt que se renuevan solos. Para n8n y Uptime Kuma configuré soporte de WebSockets, que necesitan para actualizarse en tiempo real.
Seguridad en capas
- Firewall de red en Oracle Cloud y firewall propio del sistema (UFW): solo están abiertos los puertos de HTTP, HTTPS y SSH.
- SSH endurecido: sin acceso como root, solo un usuario autorizado y llave con frase de contraseña.
- Fail2Ban bloquea automáticamente las IPs que intentan adivinar credenciales.
- Cabeceras de seguridad HTTP en los tres dominios: HSTS, protección contra clickjacking y control de permisos del navegador.
- El registro de errores de WordPress se guarda fuera de la carpeta pública y Nginx bloquea cualquier intento de leerlo.
Al migrar el firewall de iptables a UFW seguí una regla fija: nunca cerrar la sesión SSH activa hasta comprobar desde una conexión nueva que el acceso seguía funcionando. En un servidor remoto, un error de firewall puede dejarte fuera sin forma de volver a entrar.
Al revisar los registros del servidor encontré más de 10.000 intentos automatizados de adivinar contraseñas contra el inicio de sesión de WordPress. Agregué una regla de Fail2Ban, probada antes contra esos ataques reales, que bloquea por 24 horas a quien falle cinco veces, y cerré en Nginx la puerta que más usaban (XML-RPC), que el sitio no necesita. Así esos ataques ya no llegan a ejecutar código.
Rendimiento
Configuré caché en Nginx para las páginas públicas, con excepciones para el panel de administración, el inicio de sesión y el formulario de contacto, que siempre deben responder en tiempo real. El tiempo de respuesta en visitas repetidas bajó de 100 ms a unos 37 ms.
Correo del formulario de contacto
Los mensajes llegan a mi correo a través de un servidor SMTP real con una contraseña de aplicación dedicada, cargada como variable de entorno. En desarrollo local, el mismo código envía todo a una bandeja de pruebas (Mailpit), así nunca se mezclan las pruebas con correos reales.
Backups automáticos
Todos los días a las 3 de la mañana un script respalda la base de datos y las imágenes subidas, y las copia a Oracle Object Storage, fuera del servidor. Si el disco de la máquina fallara, los datos siguen a salvo. Se conservan las copias de los últimos 7 días.
Problemas reales que resolví
- Un símbolo de PHP dentro de la configuración de Docker Compose se interpretaba como variable y rompía WordPress con un error 500. Lo corregí escapándolo.
- n8n no arrancaba por un problema de permisos entre el servidor y el contenedor. Lo resolví asignando el usuario correcto a la carpeta de datos.
- El sitio caía de forma intermitente por un bucle de redirecciones guardado en la caché. Lo detectó mi propio sistema de monitoreo, y lo corregí limitando la caché a respuestas correctas.
- Después de actualizar y reiniciar el servidor, comprobé uno por uno que todos los servicios, el firewall y la protección de SSH volvieran a levantarse solos, sin intervención manual.
Cómo lo construí
Lo desarrollé con Claude como asistente técnico. Yo ejecuté cada comando en el servidor, revisé cada resultado y verifiqué que cada pieza funcionara antes de pasar a la siguiente, siempre probando primero en mi entorno local antes de tocar producción.