Contenedores, proxy y tráfico HTTP La capa donde todo se cruza y se rompe de formas creativas. Caddy Bloquear países en Caddy con MaxMind Introducción Bloqueo de IPs por territorios directamente en Caddy v2, sin depender de Cloudflare. Esto corta accesos no deseados antes de que lleguen al backend, devolviendo un 403 en la misma capa del proxy. En este caso, el ejemplo corresponde a lo que he aplicado en mi propia configuración: se usa para aquellos dominios que no están protegidos por Cloudflare, como capa extra de defensa junto con CrowdSec. Características Se aplica en Caddy antes de Authentik, file_server o reverse_proxy. Snippet reutilizable en cualquier dominio. Actualización automática de bases de MaxMind. Permite bypass para LAN/loopback y CGNAT. Refuerzo adicional en dominios sin Cloudflare. Requisitos previos Caddy compilado con plugin github.com/porech/caddy-maxmind-geolocation usando xcaddy. Cuenta gratuita en MaxMind con licencia. Paquete geoipupdate instalado y configurado. Desarrollo / pasos 1. Instalar MaxMind Instalación de la herramienta: sudo apt install geoipupdate Actualizar manualmente la base de datos: sudo geoipupdate -v El archivo GeoIP.conf se obtiene al generar una clave en la web de MaxMind. En mi caso, borré el contenido original y lo sustituí por el archivo que me dio MaxMind directamente al crear la clave. Las bases se descargan en: /var/lib/GeoIP/GeoLite2-{Country,City,ASN}.mmdb 2. Verificar ruta en Caddy El matcher usará la base de países: /var/lib/GeoIP/GeoLite2-Country.mmdb Caddy no necesita reinicio al actualizar el archivo. 3. Snippet de geobloqueo (geoblock_matcher) { @geoblock { # No geobloquees LAN/CGNAT/loopback not remote_ip private_ranges 127.0.0.1 ::1 # Bloquea si NO está en la lista permitida not maxmind_geolocation { db_path "/var/lib/GeoIP/GeoLite2-Country.mmdb" allow_countries AD AL AM AT AX BA BE BG BY CH CY CZ DE DK EE ES FI FO FR GB GI GR HR HU IE IM IS IT JE LI LT LU LV MC MD ME MK MT NL NO PL PT RO RS SE SI SK SM UA VA XK CA GL PM US AG AI AW BB BL BM BS BZ CR CU CW DM DO GD GP GT HN HT JM KN KY LC MF MQ MS NI PA PR SX SV TC TT VC VG VI MX AR BO BR CL CO EC FK GF GY PE PY SR UY VE AQ } } respond @geoblock 403 } 4. Uso en un sitio midominio.tld { import tls_estricto import seguridad_basica import log_json_global import geoblock_matcher route { reverse_proxy 192.168.1.3:8080 } } Errores comunes o decisiones importantes Los @matchers no pueden ir en global, solo dentro de un sitio o importados con snippet. El plugin no soporta continentes, solo países. Sin la exclusión remote_ip private_ranges, se bloquea la LAN. Validar siempre con caddy validate antes de recargar. Si el dominio va tras un proxy/CDN, configurar servers { trusted_proxies ... } para ver la IP real. Resumen breve Caddy con plugin MaxMind. Bases MMDB en /var/lib/GeoIP/. Snippet geoblock_matcher con lista de países permitidos. Importar snippet dentro de cada sitio. Validar y probar con VPN de otros países. En mi caso, aplicado solo a dominios sin Cloudflare, junto con CrowdSec. Referencias Repositorio del plugin caddy-maxmind-geolocation Usar binario de Caddy customizado Documentación oficial MaxMind GeoIP2 Caddyfile con variables de entorno Para evitar dejar tokens y credenciales a la vista en el Caddyfile, se pueden cargar como variables de entorno desde systemd y luego referenciarlas dentro de la configuración. Definir variables en systemd Se añaden en drop-ins bajo /etc/systemd/system/caddy.service.d/: # /etc/systemd/system/caddy.service.d/cloudflare.conf [Service] Environment=CLOUDFLARE_AUTH_TOKEN=A_ZYXS-LjFdbxxxxxxx42cmb7KH-iM8xxxxxxxxx # /etc/systemd/system/caddy.service.d/openwebui.conf [Service] Environment="OPENWEBUI_APP_TOKEN=V9APMi3w..." Después: systemctl daemon-reload systemctl restart caddy Usar las variables en el Caddyfile Caddy permite referenciar cualquier variable de entorno con {env.VARIABLE}: # Opciones globales { admin 127.0.0.1:2019 email admin@example.org acme_dns cloudflare {env.CLOUDFLARE_AUTH_TOKEN} } # Matcher compuesto: cabecera + path de API o WS @conduit_combined { header X-App-Token {env.OPENWEBUI_APP_TOKEN} path /api/* /ws* } Validar cambios antes de reiniciar Para asegurarse de que el Caddyfile es válido y que las variables se pasan correctamente: caddy fmt Caddyfile --overwrite sudo env CLOUDFLARE_AUTH_TOKEN=$CLOUDFLARE_AUTH_TOKEN \ OPENWEBUI_APP_TOKEN=$OPENWEBUI_APP_TOKEN \ caddy validate /etc/caddy/Caddyfile Si se usan otras variables, deben adaptarse al comando anterior. Verificar que systemd exporta las variables systemctl show caddy | grep Environment Deberían aparecer las variables declaradas en los drop-ins. Si no, revisar el nombre del archivo y volver a recargar/reiniciar. Resumen Variables sensibles → en drop-ins de systemd, no en el Caddyfile. Referencia en Caddyfile → {env.NOMBRE}. Recargar y reiniciar para aplicar cambios. Validar con caddy fmt + caddy validate pasando las variables al entorno. Comprobar con systemctl show si se están cargando. Directivas de Caddy (uso personal) Apunte rápido para tener recogidas las directivas que uso hoy en día. Si añado más en el futuro, irán aquí. Recordatorio: cómo importar snippets En Caddy los snippets se definen entre paréntesis y luego se importan con import. Ventaja: evitan repetir configuraciones y mantienen el Caddyfile más limpio. Si un snippet cambia, la modificación se aplica automáticamente en todos los sitios donde se importe. Ejemplo usando la directiva más corta (TLS estricto), con un sitio definido para mostrar la estructura completa: (tls_estricto) { tls { protocols tls1.3 } } # Ejemplo ficticio de uso de imports blog.ejemplo.org { import tls_estricto import seguridad_basica import log_json_global reverse_proxy http://10.0.0.5:8080 { import ip_headers } } Authentik Snippet para preautenticación y proxy de assets. Asegura que todas las rutas pasen por Authentik y copia claims útiles. (authentik) { reverse_proxy /outpost.goauthentik.io/* http://IP_INTERNO_AUTHENTIK:PUERTO forward_auth http://IP_INTERNO_AUTHENTIK:PUERTO { uri /outpost.goauthentik.io/auth/caddy copy_headers { X-Authentik-Username X-Authentik-Email X-Authentik-Groups X-Authentik-Name X-Authentik-Uid } header_up X-Real-IP {client_ip} header_up X-Client-IP {client_ip} } } TLS estricto Snippet para forzar que todo vaya con TLS 1.3. Evita negociar versiones anteriores. (tls_estricto) { tls { protocols tls1.3 } } Seguridad básica Cabeceras básicas de seguridad que suelo meter para endurecer la configuración. (seguridad_basica) { header { X-Frame-Options "SAMEORIGIN" X-Content-Type-Options "nosniff" Referrer-Policy "strict-origin-when-cross-origin" Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" } } X-Frame-Options: SAMEORIGIN → Evita que embeban la web en iframes externos. X-Content-Type-Options: nosniff → Bloquea detección automática de tipo. Referrer-Policy: strict-origin-when-cross-origin → Controla qué se envía en Referer. Strict-Transport-Security → Obliga HTTPS y permite preload en navegadores. IP real en headers Útil para que los servicios backend reciban la IP del cliente en lugar de la del proxy. (ip_headers) { header_up X-Real-IP {client_ip} header_up X-Client-IP {client_ip} header_up -X-Forwarded-Port header_up X-Forwarded-Port 443 } Registro de logs en JSON Genera logs en formato JSON. En mi caso lo uso para CrowdSec, pero puede adaptarse a cualquier otra necesidad (centralizar logs, enviar a un SIEM, etc.). (log_json_global) { log { output file /var/log/caddy/crowdsec-global.json format json level INFO } } Errores comunes Poner import fuera del bloque del sitio → no funciona, siempre debe ir dentro. Reutilizar el mismo nombre en dos snippets → Caddy solo acepta uno, el último pisa al anterior. Olvidar importar un snippet en un sitio concreto → el sitio queda sin la configuración esperada (ej. sin TLS estricto o sin logs). Colocar import después de una directiva que dependa de él → puede dar lugar a comportamientos inesperados. Implementación de Cloudflare en Caddy Caddy gestiona certificados HTTPS de forma automática, pero si usas Cloudflare como DNS, puedes mejorar la seguridad y ocultar la IP de tu servidor. Para eso, puedes configurar el ACME DNS Challenge, que permite validar tus dominios usando Cloudflare en lugar del típico challenge HTTP. Requisitos previos Dominio gestionado en Cloudflare. Caddy instalado. Un API Token de Cloudflare con permisos DNS sobre tu dominio. Paso 1: Crear el API Token en Cloudflare Accede a Cloudflare Dashboard. En tu perfil, entra a API Tokens. Crea un token con: Zone:DNS:Edit (limitado al dominio en cuestión). Guarda el token en un sitio seguro. Paso 2: Añadir el token al entorno Guarda el token como variable de entorno para evitar escribirlo directamente en el Caddyfile. # En ~/.bashrc o ~/.zshrc export CLOUDFLARE_AUTH_TOKEN=tu_token_aqui source ~/.bashrc # o ~/.zshrc Paso 3: Configurar el Caddyfile Bloque global con soporte para DNS Challenge usando Cloudflare: { acme_dns cloudflare email admin@example.com } example.com { reverse_proxy http://192.168.1.10:8080 } subdomain.example.com { reverse_proxy http://192.168.1.10:8081 } El token se toma de la variable de entorno CLOUDFLARE_AUTH_TOKEN. Paso 4: Validar y recargar Caddy caddy validate --config /etc/caddy/Caddyfile sudo systemctl reload caddy Beneficios de usar Cloudflare con Caddy Ocultas la IP real del servidor. Añades mitigación contra ataques DDoS. Puedes aprovechar el cacheo de contenido estático. Detalles adicionales Modo SSL en Cloudflare: activa "Full (Strict)" para que Cloudflare acepte los certificados de Caddy. Compilar Caddy con soporte DNS personalizado: si no usas un binario precompilado, consulta esta guía: 👉 Usar binario de Caddy customizado Docker Lo típico que siempre acabo buscando otra vez en Google. Ahora, por fin, lo tengo aquí. Activar conexiones externas en Docker Introducción Docker no expone su API por red de forma predeterminada, pero en algunos casos puede ser útil activarla (por ejemplo, para usar Portainer desde otro host). Aquí se explica cómo hacerlo en el puerto 2375. ⚠️ Advertencia de seguridad Exponer la API sin protección es peligroso. Solo debería hacerse en entornos controlados. Lo ideal es usar firewall, túneles SSH o TLS. Habilitar la API en el puerto 2375 1. Editar daemon.json sudo nano /etc/docker/daemon.json Contenido: { "hosts": ["tcp://0.0.0.0:2375", "unix:///var/run/docker.sock"] } Esto permite conexiones tanto locales (socket) como externas (2375). 2. Override del servicio Docker Crea el directorio si no existe: sudo mkdir -p /etc/systemd/system/docker.service.d Archivo de override: sudo nano /etc/systemd/system/docker.service.d/override.conf Contenido: [Service] ExecStart= ExecStart=/usr/bin/dockerd Esto borra cualquier ExecStart previo y lo reemplaza por uno limpio. 3. Recargar systemd y reiniciar Docker sudo systemctl daemon-reload sudo systemctl restart docker.service Verificar si funciona ss -lntp | grep dockerd Debes ver algo parecido a: LISTEN 0 128 0.0.0.0:2375 0.0.0.0:* users:("dockerd") Seguridad adicional Limitar IPs con iptables o el firewall del sistema. Usar TLS para conexiones seguras. Opción mejor: en lugar de abrir el puerto, usar un túnel SSH. Conclusión Si realmente necesitas acceso remoto a Docker, esto lo habilita. Pero solo debería usarse con medidas adicionales. Nunca abras 2375 sin protección en producción. Referencias Configurar Docker con systemd Gist de ejemplo de configuración Gatus Ajustes finos, healthchecks y demás necromancia para que los avisos tengan sentido y no me spamee Telegram. Caddy + Gatus - Chequear salud de los endpoints tras Authentik Introducción Apunte sobre cómo crear snippets en Caddy para fabricar endpoints de salud /healthz protegidos con un token. Esto permite que Gatus monitorice backends reales aunque las aplicaciones no tengan /healthz ni /status propio. La idea es que Caddy genere un check “virtual” hacia un path que sabemos que responde 200 si el servicio está vivo, pero manteniendo el endpoint oculto tras un header secreto. Objetivo Crear endpoints /healthz uniformes para cualquier servicio. Que devuelvan 200 si el backend responde y 5xx si está caído. Que Gatus pueda consultarlos mediante X-Health-Token. Saltarse Authentik solo para los checks, sin romper la autenticación normal. Snippets creados # Healthz con path configurable (cuando hay que mapear a otra ruta sana) (healthz_path) { @health { path /healthz header X-Health-Token gatus-SECRET-TOKEN } handle @health { rewrite * {args[1]} # /healthz → path real del backend (/, /login, /admin...) reverse_proxy {args[0]} { import ip_headers } } } # Healthz directo (cuando basta con apuntar al root u otro path estable) (healthz) { @health { path /healthz header X-Health-Token gatus-SECRET-TOKEN } handle @health { reverse_proxy {args[0]} { import ip_headers } } } El token ( gatus-SECRET-TOKEN) se anota en el Caddyfile. Así Caddy solo responde 200 si el backend funciona, y exige la cabecera para no exponerlo. Ejemplo práctico Para un servicio sin health propio: subdominio.dominio.tld { import tls_estricto import seguridad_basica import log_json_global # healthz externo → proxyea al backend / import healthz_path http://192.168.1.35:8300 / route { import authentik reverse_proxy http://192.168.1.35:8300 { import ip_headers } } } Con esto: curl -i https://subdominio.dominio.tld/healthz \ -H 'X-Health-Token: gatus-SECRET-TOKEN' Si el backend está vivo → 200 OK. Si está caído → 502/5xx. Integración con Gatus En config.yaml se define el endpoint con el header: endpoints: - name: Subdominio (/healthz) group: SubHerramientas url: https://subdominio.dominio.tld/healthz headers: X-Health-Token: "gatus-SECRET-TOKEN" interval: 30s conditions: - "[STATUS] == 200" - "[RESPONSE_TIME] < 2000ms" - "[CERTIFICATE_EXPIRATION] > 168h" Así, Gatus monitoriza el backend real a través del /healthz fabricado en Caddy. Beneficios ✅ Checks reales: si el contenedor muere, Caddy devuelve 502 y Gatus lo detecta. ✅ Compatibilidad con Authentik: los usuarios pasan por login, los checks no. ✅ Endpoint oculto: solo accesible con X-Health-Token. ✅ Flexibilidad: eliges qué ruta interna usar como prueba de vida. Notas finales Usa rutas que siempre den 200 sin login ni redirecciones (/, /login, /admin…). Nunca expongas /healthz sin token. Si el backend necesita cabecera Host, añade header_up Host {upstream_hostport} en el reverse_proxy. Referencias o enlaces de interés Instalación de Gatus Configuración de referencia en Gitea Gatus: Configuración de referencia de la UI, postgres, checks de conectividad y alertas en Telegram Introducción Apunte de configuración base de Gatus usando UI personalizada, almacenamiento en Postgres, alerting por Telegram y un ejemplo mínimo de cada tipo de check (HTTP, DNS, ICMP, TCP, UDP). No entra en detalles finos: solo lo necesario para replicar la estructura. Nota: uso X-Health-Token porque detrás hay Authentik protegiendo los servicios; genero endpoints /healthz “ocultos” con token para que Gatus compruebe el backend real. Si no hay Authentik/forward-auth, basta con apuntar al host real y eliminar la sección headers. Requisitos previos Gatus funcionando (ver apunte básico) y Postgres accesible. Carpeta con config/config.yaml y datos de conexión a la DB. Repo con el archivo final: config.yaml en Gitea (ChronosCMPS). Configuración (estructura general) ui: title: "Status | Gatus" header: "Status | Gatus" description: "Si algo se cae, no llores: reinicia y maldice." logo: "https://example.org/assets/img/clock.png" dark-mode: auto storage: type: postgres path: "postgres://gatus:xxxx@gatus-db:5432/gatus?sslmode=disable" # Ajustar a tu stack alerting: telegram: token: "000000:REEMPLAZA_CON_TOKEN" id: "-1000000000000" # chat/supergrupo # topic-id: 1 # opcional default-alert: send-on-resolved: true failure-threshold: 3 success-threshold: 1 endpoints: # HTTP con healthz y TLS, healthz es prescindible, lo uso por mi caso particular - name: Web principal group: HTTP url: "https://app.example.org/healthz" interval: 30s headers: # Eliminar si no usas Authentik/forward-auth X-Health-Token: "REEMPLAZA_CON_TOKEN" conditions: - "[STATUS] == 200" - "[RESPONSE_TIME] < 1500ms" - "[CERTIFICATE_EXPIRATION] > 168h" alerts: - type: telegram # DNS (NS de dominio contra 1.1.1.1) - name: DNS NS example.org group: DNS url: "1.1.1.1:53" interval: 2m dns: query-name: "example.org" query-type: "NS" conditions: - "[DNS_RCODE] == NOERROR" alerts: - type: telegram # ICMP (ping) - name: Ping Cloudflare group: ICMP url: "icmp://1.1.1.1" interval: 30s conditions: - "[CONNECTED] == true" - "[RESPONSE_TIME] < 200ms" alerts: - type: telegram # TCP (ej. Redis) - name: Redis interno group: TCP url: "tcp://redis.internal:6379" interval: 30s conditions: - "[CONNECTED] == true" alerts: - type: telegram # UDP (conectividad) - name: Servicio UDP ejemplo group: UDP url: "udp://udp.example.org:12345" interval: 30s conditions: - "[CONNECTED] == true" alerts: - type: telegram ¿Qué hace cada bloque? UI Ajusta el título, encabezado, descripción, logo y modo oscuro o claro automático. Solo cosmética: no afecta a los checks. Storage (Postgres) Persistencia de resultados y estado. La path es una URL de conexión estándar de Postgres; debe coincidir con los datos reales del docker-compose. Alerting (Telegram) Config global de alertas. default-alert marca umbrales: tras 3 fallos avisa; con 1 éxito resuelve. Se referencian con type: telegram en cada endpoint. Endpoints Un ejemplo por tipo: HTTP: valida código, tiempo de respuesta y caducidad del cert. DNS: consulta explícita ( query-name, query-type) contra un resolver. ICMP: ping básico (conectividad y latencia). TCP/UDP: solo conectividad (que el puerto responda). Resumen breve Configuración mínima y versionable en config.yaml. Postgres para persistencia y Telegram para avisos. Un check por tipo para tener plantillas claras. X-Health-Token solo si hay forward-auth (Authentik); si no, quitar headers y healthz y apuntar al host real. Referencias o enlaces de interés Instalación de Gatus Configuración de referencia en Gitea Repositorio oficial en GitHub Documentación oficial Nextcloud Apuntes que solo entiendes cuando Nextcloud te ha hecho perder media tarde por una tontería. Nextcloud + Caddy: Elimina el HTTPS autofirmado en Docker Nextcloud en Docker viene con HTTPS autofirmado por defecto. Está bien para pruebas, pero en producción da problemas. Aquí te explico cómo desactivarlo para que Caddy se encargue del HTTPS con certificados válidos de Let's Encrypt. Aplicable a la imagen de LinuxServer. Requisitos previos Tienes Nextcloud funcionando en Docker. Puedes acceder al archivo de configuración de Nginx dentro del contenedor. Pasos para desactivar el HTTPS autofirmado 1. Localiza el archivo default.conf Suele estar en: /ruta/a/tu/directorio/Nextcloud/config/nginx/site-confs/default.conf 2. Edita el archivo Ábrelo con tu editor favorito y busca estas líneas: listen 443 ssl http2 default_server; listen [::]:443 ssl http2 default_server; 3. Coméntalas Déjalas así para que no se activen: # listen 443 ssl http2 default_server; # listen [::]:443 ssl http2 default_server; 4. Reinicia el contenedor Guarda los cambios y reinicia Nextcloud: docker-compose restart nextcloud Verifica que funciona Nextcloud ya no debería escuchar en el puerto 443. Ahora Caddy puede ocuparse del HTTPS sin interferencias. Puedes comprobarlo así: docker logs nextcloud O accediendo por HTTP en el puerto 80 para ver que sigue funcionando. Notas extra ¿Por qué hacerlo? Los certificados autofirmados generan advertencias y problemas en apps y navegadores. Caddy gestiona certificados válidos automáticamente. Más cómodo y más seguro. ¿Y ahora qué? Toca configurar Caddy como proxy para tu dominio. Instalación de Caddy Documentación oficial de Caddy Si ya tienes Caddy preparado, esto es todo lo que necesitas para dejar tu Nextcloud listo y limpio, sin certificados falsos por medio. phpMyAdmin Apuntes para hacer lo que necesito sin romper nada… o al menos sin romperlo demasiado. Almacenamiento persistente de configuración y sesiones Introducción Este artículo documenta un tip técnico para habilitar almacenamiento persistente en phpMyAdmin, evitando la pérdida de preferencias, sesiones y configuraciones internas tras reinicios de contenedores. Por defecto, phpMyAdmin funciona correctamente sin persistencia, pero ciertas funcionalidades avanzadas (preferencias de usuario, bookmarks SQL, historial, relaciones, etc.) requieren almacenamiento adicional. Este ajuste permite que el comportamiento del panel sea consistente a lo largo del tiempo. Enfoque general El almacenamiento persistente en phpMyAdmin se apoya en dos pilares: Persistencia de configuración mediante archivos montados como volumen. Base de datos interna de phpMyAdmin para guardar metadatos y preferencias. Este enfoque no afecta a las bases de datos de las aplicaciones gestionadas, únicamente a los datos internos de phpMyAdmin. Qué se gana con persistencia Conservación de preferencias de usuario entre reinicios. Mantenimiento de sesiones y opciones personalizadas. Uso de funcionalidades avanzadas (bookmarks SQL, historial, relaciones). Comportamiento predecible del panel tras actualizaciones o reinicios. Archivo de configuración de usuario phpMyAdmin permite definir ajustes personalizados mediante el archivo: config.user.inc.php Este archivo se monta como volumen dentro del contenedor y permite definir parámetros de comportamiento sin modificar la imagen base. Ubicación recomendada en el host: Docker/phpMyAdmin/phpmyadmin/config.user.inc.php Desde este archivo se configuran, entre otros: Base de datos de configuración ( $cfg['Servers'][$i]['pmadb']). Tablas internas utilizadas por phpMyAdmin. Opciones de interfaz y comportamiento. Base de datos interna de phpMyAdmin Para habilitar persistencia completa, phpMyAdmin utiliza una base de datos propia donde almacena: Bookmarks SQL. Preferencias de usuario. Historial de consultas. Metadatos adicionales. Esta base de datos es independiente del resto de bases gestionadas y debe inicializarse manualmente. Creación de tablas de configuración phpMyAdmin proporciona un script oficial para crear sus tablas internas. En este entorno se utiliza una versión versionada en Gitea. Script disponible en: Almacenamiento persistente en Gitea El script crea las tablas necesarias para habilitar todas las funcionalidades de almacenamiento interno. Inicialización de la base de datos Pasos generales para preparar la base de datos interna: Crear una base de datos dedicada (por ejemplo, phpmyadmin). Importar el script de creación de tablas. Configurar phpMyAdmin para que apunte a esta base mediante config.user.inc.php. No es necesario que esta base tenga acceso externo ni permisos elevados fuera del propio phpMyAdmin. Verificación Una vez aplicada la configuración, se puede validar el correcto funcionamiento comprobando: Que las preferencias se mantienen tras reiniciar contenedores. Que phpMyAdmin no muestra avisos sobre almacenamiento de configuración. Que funcionalidades como bookmarks o historial están disponibles. La interfaz suele indicar explícitamente cuando el almacenamiento está correctamente habilitado. Consideraciones importantes La base de datos interna debe incluirse en las copias de seguridad si se desean conservar preferencias. No es recomendable reutilizar esta base para otros fines. La persistencia no afecta al rendimiento de las bases gestionadas. Resumen breve Habilitar almacenamiento persistente en phpMyAdmin mejora la experiencia de uso y desbloquea funcionalidades avanzadas. Mediante una combinación de archivos de configuración y una base de datos interna dedicada, se consigue un panel estable y coherente incluso tras reinicios o actualizaciones.