Lo que no te han contado de las configuraciones en Docker

Vistas: 1
0:00 / 0:00
Lo que no te han contado de las configuraciones en Docker

Hola, soy Lorenzo y esto es Atareao con Linux, episodio número 730. Llevo unas semanas dándole vueltas a los gestores de Docker Compose. Y ya sé que si te suelto así, de golpe, lo de «gestor de Docker Compose», a lo mejor te quedas igual; pero si te digo Portainer, ya caes al instante. Pues por ahí voy. Durante los últimos meses te he ido presentando alternativas, como Arcane o Dockge, y he contado por qué me quedé con una y no con otra.

Al principio me quedé con Arcane, porque me daba cosas que Dockge no me daba. Pero en los últimos días, por circunstancias varias, y sobre todo porque se me estaba comiendo la CPU y la RAM del VPS, acabé volviendo a Dockge. Y volví porque Dockge es sencillo de verdad: me da justo lo que necesito, ni más ni menos. Si te has preguntado alguna vez por qué no uso Portainer, la respuesta está aquí. Portainer me ofrece tantas opciones y está tan sobrecargado, desde mi punto de vista, que me sobra la mitad. Con lo justito de Dockge voy sobrado.

Durante años he trabajado con la terminal, pura y dura, con Neovim abierto y mis ficheros docker-compose.yml, haciendo mi up, mi down y lo que hiciera falta. El problema llega cuando necesito reiniciar un servidor, o una base de datos, o lo que sea, y estoy por la calle. Ahí una interfaz gráfica sencilla, que me deje hacer esas gestiones desde el navegador, me viene de lujo. Eso es lo que me encontré con Dockge.

Y en todo este proceso me topé de bruces con un inconveniente que llevaba años dándome la lata: los ficheros de configuración. Cuando montas un volumen que en realidad es solo un fichero, siempre surgen problemas. He compartido decenas, y casi me atrevo a decir centenares, de Docker Compose con su correspondiente archivo de configuración, y siempre aparecía la misma pregunta. Cómo monto el fichero, qué permisos le doy, si lo monto antes o lo monto después. Un tormento. Hoy te traigo la solución, y no vale solo para Dockge: vale para cualquier Compose. Se llaman configs, los configs de Docker, y me atrevo a decir que casi nadie te lo ha contado. Así que vamos al turrón.

Por qué montar un solo fichero como volumen acaba siempre en drama

Antes de entrar en materia conviene que entiendas bien cuál es el problema, porque si no, la solución te va a parecer magia y no lo es. Cuando tú montas un bind mount en Docker, es decir, cuando le dices al contenedor que una ruta del host aparezca en una ruta de dentro del contenedor, tienes dos escenarios muy distintos. Si montas un directorio, la cosa suele ir bien. El problema aparece cuando montas un solo fichero. Y no es un problema, son varios, y encima se acumulan.

El primero es de esos que te hacen perder una tarde entera. Si la ruta de origen no existe en el host, Docker no te avisa con un error, no. Docker crea un directorio vacío con ese nombre y lo monta tan feliz. Tú arrancas el contenedor, el servicio se queja de que no encuentra su fichero de configuración, y cuando vas a mirar al host te encuentras con una carpeta donde esperabas un archivo. El clásico nginx.conf que resulta ser un directorio. Que le vamos a hacer, es una de esas cosas que solo aprendes cuando te pasa.

El segundo problema, y el que más guerra me ha dado, son los permisos y el propietario. Tú tienes tu fichero en el host, lo creas con tu usuario, con tu umask y con los permisos que buenamente te salen. Pero dentro del contenedor el servicio no corre con tu usuario. Nginx corre como nginx o como un UID bajo, MariaDB corre como mysql, y cada imagen tiene el suyo. Si el fichero está a nombre de root y sin permiso de lectura para el resto, el proceso no puede leerlo y se cae al arrancar. Y como el fichero está en el host, el chmod que le haces afecta a tu sistema, no solo al contenedor. Es un pulso entre el usuario del host y el usuario de la imagen que nunca ganas del todo.

Y luego está la portabilidad, que es el problema silencioso. Tu Docker Compose te lo llevas a otro servidor, lo clonas del repositorio, lo copias por scp, y ese fichero de configuración tiene que viajar contigo. Si te olvidas de él, el Compose es papel mojado. Si además las rutas son relativas, ya sabes lo que pasa: depende de desde dónde ejecutes el docker compose el fichero aparece o no aparece. En SELinux o AppArmor hay que añadir etiquetas como :z o :Z al volumen para que el contexto sea el correcto, y eso es otro detalle más que cargar. Y si el fichero lo genera otro servicio, o un script al arrancar, aparece una condición de carrera preciosa: quién monta primero, quién escribe después.

Todo esto, junto, es lo que llevo años resolviendo a mano con más o menos fortuna. Y resulta que Docker tenía una respuesta desde hace años y casi nadie la usa fuera del mundo del Swarm. Esa respuesta son los configs.

Qué son los configs y en qué se diferencian de los secrets

Vamos con lo más obvio. ¿Qué es esto de los configs? Pues una opción que tiene Docker, y la mejor forma de entenderla es decir que es igual que los secrets, pero para lo que no es secreto. Los secrets de Docker están pensados única y exclusivamente para compartir información sensible: contraseñas, claves, tokens. Los configs están pensados para archivos que no manejan información sensible: ficheros de configuración, plantillas, datos estáticos.

Y ojo con esa distinción, porque es importante y mucha gente la ignora. Los configs no van cifrados por el mero hecho de ser configs. Se almacenan igual que los secretos en el registro del clúster, sin cifrado adicional por defecto. Así que si tu configuración lleva una contraseña dentro, no la metas en un config: para eso están los secretos. Los configs son para el .ini, el .yaml, el .toml, el .conf de toda la vida. Para el fichero de configuración de Nginx, para el php.ini, para una plantilla de Prometheus, para un fichero de reglas de Traefik. Y, como te voy a demostrar a lo largo del episodio, también para cualquier otra cosa que no sea sensible.

Por debajo, el mecanismo es muy parecido al de los secretos. Cuando creas un config en un Swarm, Docker lo guarda en el registro del clúster, que es el raft log, y cuando un contenedor lo consume, lo monta en el sistema de ficheros del contenedor en modo de solo lectura. En los secretos la ruta por defecto es /run/secrets/nombre. En los configs la ruta por defecto es la raíz, /nombre, y esa diferencia despista bastante al principio. Tú puedes cambiar dónde aparece con el parámetro target, y ahí está la gracia de todo esto.

Hay un límite que conviene tener en el mapa: en un clúster de Swarm, secretos y configs no pueden pasar de unos 500 KB. Para un fichero de configuración normalísimo eso es más que suficiente, y aun así es bueno saberlo para no intentar meter ahí una base de datos entera o un binario. Si lo que necesitas es un fichero grande y cambiante, eso no es un config, eso es un volumen.

Las tres ventajas que me hicieron cambiar de idea

Cuando descubres los configs y los pruebas un par de veces, hay tres beneficios que aparecen solos. No son teoría, son las tres razones por las que ahora los uso casi por defecto.

  • El flujo de trabajo. Es mucho más sencillo modificar un fichero de configuración dentro del propio Docker Compose que pelearte con él en el host, con sus permisos y su propietario. Todo está en el mismo sitio.
  • La portabilidad. Como el contenido va dentro del Docker Compose, cuando pasas ese fichero a otro servidor va todo junto. No te dejas nada por el camino y no vuelves a discutir con los permisos.
  • La gestión centralizada. Un config declarado en el Compose se puede compartir con todos los servicios que estén en ese mismo Compose. Lo defines una vez y lo consumes donde lo necesites.

Hay una regla que me repito cada vez que algo me da problemas en Docker: si el fichero no cambia y no es secreto, no es un volumen. Es un config. Me ha ahorrado más tardes de las que puedo contar.

Un Compose de Nginx al uso, para ver el problema en vivo

Para que veas de qué hablo, piensa en el Docker Compose más típico que existe: un Nginx. Lo normal es tener la imagen, el nombre del contenedor, cuándo o en qué condición se tiene que reiniciar, y luego el volumen, donde defines el fichero de configuración del Nginx, es decir, el nginx.conf. Algo así.

services:
  nginx:
    image: nginx:latest
    container_name: nginx
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

Y ese ./nginx.conf, que es un fichero tan sencillo como el que te muestro a continuación, es donde vive el problema. Aquí tienes el número de procesos, los eventos, el bloque HTTP, dónde está el root y dónde el index.html. La información básica que necesita cualquier Nginx.

worker_processes 1;

events {
    worker_connections 1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    server {
        listen       80;
        server_name  localhost;

        location / {
            root   /usr/share/nginx/html;
            index  index.html index.htm;
        }
    }
}

Pues con este planteamiento es donde empiezan los problemillas. Y son de lo más variopinto. A veces el archivo simplemente lo puede leer el proceso y a veces no, porque tiene permiso de lectura y escritura pero el propietario es root. A veces el fichero no existe donde creías y allí hay un directorio. A veces has montado con :ro y el servicio necesitaba escribir sobre él. Y encima, si llevas este Compose a otra máquina, te tienes que acordar de arrastrar el nginx.conf contigo. Siempre la misma canción. Pues la forma de resolver todo esto es con los configs, que es de lo que veníamos hablando.

Cómo se declara la sección configs en el Docker Compose

Y aquí llega la parte que nadie te cuenta, que es cómo se escribe. Porque todo el mundo conoce la sección de networks y la de volumes, pero casi nadie sabe que existe una sección de configs al mismo nivel. Es exactamente igual de sencilla. Tienes una entrada nueva llamada configs y dentro puedes tener tantos configs como quieras, igual que tienes varias redes o varios volúmenes. Los puedes llamar como te dé la gana: nginx_config, proxy, internal, lo que sea.

Cada config se define, en su forma más básica, con dos parámetros. Uno es el origen. El otro es el destino. Pero antes de llegar al servicio, hay que entender que un config a nivel raíz puede declararse de tres maneras distintas. Te las enseño juntas, porque es de las cosas más útiles que vas a aprender hoy.

configs:
  mi_config_desde_fichero:
    file: ./nginx.conf

  mi_config_en_linea:
    content: |
      worker_processes 1;
      events { worker_connections 1024; }

  mi_config_desde_entorno:
    environment: MI_VARIABLE

Fíjate en las tres posibilidades. Con file referencias un fichero que tienes en el host, junto al Compose. Con content escribes el contenido directamente ahí, en el propio YAML, que es lo que a mí me tiene enamorado. Y con environment lo tomas del entorno donde ejecutas el Compose. La opción content es la que hace que todo el chiringuito vaya dentro del mismo fichero: ya no tienes un .conf suelto al lado, lo tienes ahí mismo.

Luego, en el servicio, tienes que decirle qué config consume y dónde lo quiere. Eso se hace con la clave configs dentro del propio servicio. Hay dos sintaxis. La corta es una sola línea, y con eso el fichero aparece en la raíz del contenedor con el nombre del config. La larga te deja fijar el target, que es la ruta exacta dentro del contenedor, el mode, que son los permisos, y opcionalmente uid y gid. Yo casi siempre uso la larga, porque quiero controlar el destino y los permisos.

services:
  nginx:
    image: nginx:latest
    configs:
      - source: mi_config_desde_fichero
        target: /etc/nginx/nginx.conf
        mode: 0644

Y aquí está el meollo de todo. Ese target es justo la ruta donde antes montabas el volumen. Ese mode es justo el permiso que antes peleabas en el host. Y el source es el nombre que le diste arriba, en la sección raíz. Con esto, Docker se encarga de meter el contenido en el contenedor con los permisos que tú le digas, sin que el sistema de ficheros de tu máquina tenga nada que ver. Se acabó el pulso con el propietario.

El ejemplo completo: Nginx con su configuración inyectada

Vamos a juntar las piezas en un Compose entero y funcional. Este es el que uso para probar la idea, y es tan corto que cabe en una pantalla. Un servicio de Nginx, una sección de configs con dos entradas, y las referencias dentro del servicio. Nada más.

services:
  nginx:
    image: nginx:latest
    container_name: nginx
    restart: unless-stopped
    ports:
      - "8080:80"
    configs:
      - source: nginx_config_file
        target: /etc/nginx/nginx.conf
        mode: 0644
      - source: nginx_index
        target: /usr/share/nginx/html/index.html
        mode: 0644

configs:
  nginx_config_file:
    content: |
      worker_processes 1;
      events {
          worker_connections 1024;
      }
      http {
          include       mime.types;
          default_type  application/octet-stream;
          server {
              listen       80;
              server_name  localhost;
              location / {
                  root   /usr/share/nginx/html;
                  index  index.html index.htm;
              }
          }
      }

  nginx_index:
    content: |
      <!DOCTYPE html>
      <html lang="es">
        <head><meta charset="utf-8"><title>Hola</title></head>
        <body><h1>Hola</h1></body>
      </html>

Y ahora lo levantas como cualquier otro Compose. Nada especial, el mismo comando de siempre.

docker compose up -d

Si te apetece comprobar que el fichero resuelto está bien antes de arrancar nada, y yo lo hago casi siempre, tienes docker compose config, que te resuelve el YAML completo, con los valores por defecto aplicados y las interpolaciones hechas. Es la mejor forma de ver qué va a pasar antes de que pase.

docker compose config

Y si quieres ver el fichero dentro del contenedor, entras y lo miras.

docker compose exec nginx cat /etc/nginx/nginx.conf

Ahí lo tienes. El fichero de configuración está dentro del contenedor, en su sitio, con los permisos que le pediste, y en tu host no existe ningún nginx.conf suelto con el que pelearte. Todo va en el Compose. Esta es la parte que me parece brillante, y me sorprende la de gente que sigue montando volúmenes de un solo fichero sin saber que esto existe.

Permisos, modos y propietarios: la letra pequeña de los configs

Vamos a hablar de los permisos con calma, porque aquí hay un detalle que confunde y que quiero dejar claro. Cuando tú no pones mode, Docker asigna a los configs un modo de 0444 por defecto, es decir, lectura para todo el mundo y escritura para nadie. Tiene todo el sentido del mundo, porque un config es un fichero de solo lectura: se inyecta al arrancar el contenedor y no se toca. Si lo piensas, es exactamente el comportamiento que quieres.

Ahora bien, en el episodio yo he usado 0644, que añade el permiso de escritura para el propietario. ¿Por qué? Pues por costumbre y porque venía de pelearme con estos ficheros en el host, donde el 644 es el permiso de referencia para un fichero de configuración. La diferencia práctica es pequeña, porque el fichero lo va a leer el proceso y punto. Si quieres ser purista y dejar el fichero intocable, quédate con el 0444 por defecto. Si eres un maniático del orden como yo, ponlo explícito y que quede en el Compose. Lo importante es que ese permiso lo fijas tú desde el Compose y no desde el sistema de ficheros del host.

Y luego están uid y gid, que son el propietario y el grupo del fichero dentro del contenedor. Si tu servicio corre con un usuario concreto y necesita ser el dueño del fichero para algo, aquí lo indicas. Es la puntilla que te permite clavar hasta el último detalle de los permisos sin tocar el host. Fíjate en la sintaxis larga completa, con todo lo que puedes controlar.

services:
  nginx:
    image: nginx:latest
    configs:
      - source: nginx_config_file
        target: /etc/nginx/nginx.conf
        mode: 0444
        uid: "101"
        gid: "101"

Y ojo con un matiz, porque aquí hay gente que se lía: eso controla quién es el dueño del fichero dentro del contenedor, pero no cambia el usuario con el que corre el proceso. Son dos cosas distintas y complementarias. Si el proceso arranca como root, va a poder leer el fichero pase lo que pase. Si arranca como un usuario sin privilegios, necesitas que el fichero sea legible por ese usuario o por su grupo. Tú decides dónde poner el límite, eso sí, todo desde el Compose.

Un segundo fichero: la página de inicio con un simple hola

Ahora viene lo que demuestra que los configs no sirven solo para ficheros de configuración al uso. En el episodio añadí un segundo fichero al mismo Nginx, y ese segundo fichero no era configuración, era contenido. Ni más ni menos que el index.html. Lo definí exactamente igual que el anterior, con su entrada en la sección de configs y su referencia en el servicio. El contenido, para la prueba, era un «hola» bien grande. Y con eso ya tienes el servidor con su página de inicio levantada.

Si te fijas en el ejemplo completo de antes, la entrada nginx_index hace justo eso. Es un config con content que lleva un HTML mínimo, y en el servicio se monta sobre /usr/share/nginx/html/index.html. Levantas, abres el navegador en http://localhost:8080, y ahí está el «hola». Así de sencillo. Y aquí está la reflexión: si funciona para un index.html, funciona para cualquier archivo de texto que no sea sensible. Una plantilla, un fichero de cabeceras, un CSV de ejemplo, lo que se te ocurra.

Esto es lo que decía antes de que los configs no están atados a la extensión ni al tipo. La única frontera es que no sea información sensible y que no necesites escribirlo en caliente. Si es un archivo que solo se lee, cabe en un config. Punto.

Modificar y reiniciar sin salir del navegador

Y llegamos a la parte que de verdad me enganchó, y que tiene que ver con Dockge. Con todo el contenido dentro del Docker Compose, cuando quiero cambiar algo de la configuración, lo cambio ahí mismo. Paro el servicio, edito el config, y lo vuelvo a levantar. Nada de ir al servidor a editar un fichero, nada de pelearme con el propietario, nada de acordarme de dónde estaba.

En la consola eso son un par de comandos de siempre. Paras, y cuando el YAML ya tiene el contenido nuevo, lo vuelves a levantar. Como el fichero va dentro de la definición del servicio, Compose se encarga de recrear el contenedor con lo nuevo cuando detecta el cambio.

docker compose down
docker compose up -d

Y si eres más de recargar que de bajar todo, también puedes recrear solo el servicio afectado sin tocar el resto.

docker compose up -d --force-recreate nginx

El caso es que el flujo es siempre el mismo: edito, reinicio, miro. Y esto lo puedo hacer directamente desde cualquier navegador. No necesito estar con el móvil a mano, ni con Termux abierto, ni conectarme por SSH. Entro al panel de Dockge desde donde esté, toco el config, le doy a reiniciar, y ya tengo la línea nueva funcionando. Es así de fácil, es así de facilote. Y esto, cuando administras un servidor que no tienes delante, vale su peso en oro.

Un detalle de transparencia, porque me gusta decir las cosas como son: si cambias el contenido de un config y no tocas nada más, puede que Compose no te recree el contenedor por las buenas, así que cuando quiero estar seguro uso el --force-recreate y me quito de dudas. Es un segundo más y me evita pensar que he cambiado algo que en realidad no ha llegado a aplicarse.

Configs en local frente a configs en modo Swarm

Hay una cosa que quiero que tengas clara, porque es donde más gente se lía: los configs no funcionan igual en Docker Compose normal que en un clúster de Swarm. Y no pasa nada, pero conviene saber qué pasa en cada caso.

En modo Swarm, los configs son objetos de primera clase. Los creas con docker config create, quedan guardados en el raft log del clúster y los consumes al desplegar un stack o un servicio. Ahí sí que hay un objeto config con nombre que vive en el clúster, y las rutas por defecto son las que te decía antes.

docker config create nginx_config ./nginx.conf
docker config ls
docker service create \
  --name nginx \
  --config source=nginx_config,target=/etc/nginx/nginx.conf \
  nginx:latest

En cambio, con Docker Compose normal, el config no es un objeto del clúster. Compose coge el contenido del file o del content y lo mete en el contenedor. Por debajo, lo empaqueta en un archivo tar y lo copia al contenedor antes de arrancarlo. Por eso funciona sin Swarm, sin clúster y sin nada más: es Compose el que hace la inyección. Y por eso el resultado es el mismo que en Swarm desde tu punto de vista, un fichero de solo lectura dentro del contenedor con los permisos que pediste.

Esta es la razón de fondo por la que los configs resuelven el problema de los permisos que te contaba al principio. Ya no hay un fichero en tu host que se monta tal cual: hay un contenido que Docker, o Compose, coloca dentro del contenedor con el modo que tú has decidido. El sistema de ficheros del host deja de estar en medio, y con él se van de vacaciones los chmod, los propietarios root y las etiquetas de SELinux.

Y aquí viene la noticia buena para mí, que últimamente ando metido en el mundo de Podman: las implementaciones compatibles con Compose, incluidas las que funcionan sobre Podman, van entendiendo cada vez más estas claves. No es algo que quiera prometerte al cien por cien para cualquier versión, porque esto se mueve, pero la tendencia es clara. Si escribes tu docker-compose.yml con configs, estás escribiendo hacia adelante y no hacia atrás.

Cuándo no te sirven los configs

Y no todo va a ser bondad. Hay casos en los que un config no es la herramienta, y si lo fuerzas te vas a pelear con el contenedor igual que antes. Te los enumero para que no caigas en la trampa.

  • Ficheros que la aplicación escribe. Un config es de solo lectura por diseño. Si tu servicio necesita modificar ese fichero en caliente, escribe logs ahí o guarda estado, eso no es un config: es un volumen.
  • Directorios enteros. Los configs son para ficheros sueltos. Si tienes que inyectar una carpeta con veinte archivos, eso se hace con un volumen, no con veinte configs.
  • Ficheros grandes o cambiantes. Recuerda el límite de los 500 KB y que el config se resuelve al desplegar. Para datos pesados o que cambian a menudo, sigue siendo mejor un volumen.
  • Información sensible. Contraseñas, claves y tokens van en secrets, no en configs. Los configs no están cifrados por defecto.

Y una última advertencia de sentido común: los configs no eximen de pensar. En desarrollo, con recarga en caliente, un volumen sigue siendo más cómodo porque refleja los cambios al instante. Los configs brillan en despliegues donde quieres que el fichero quede fijo, con permisos conocidos, y que viaje con el Compose. Ahí no hay rival.

Conclusiones

Hagamos balance, porque el episodio de hoy ha sido corto en herramientas y largo en la idea que hay detrás. La idea es que los ficheros de configuración que solo se leen no deberían ser volúmenes. Deberían ser configs. Y a partir de ahí, todo lo demás cae por su peso.

Me llevo tres conclusiones del episodio, y me las apunto para no olvidarlas. La primera, que los configs son para los ficheros la misma solución que los secrets para lo sensible, con la diferencia de que tú decides el target y el mode desde el propio Compose, y te olvidas para siempre de los permisos del host. La segunda, que meter el contenido dentro del docker-compose.yml, ya sea con content o con file, convierte tu despliegue en algo portable de verdad: te llevas un fichero y va todo dentro. Y la tercera, la que más me ha cambiado el día a día, que esto se combina de maravilla con un gestor como Dockge, porque puedo editar el config y reiniciar el servicio desde cualquier navegador, sin tener el servidor delante.

Si algo de lo que hemos visto te ha picado la curiosidad, coge tu Compose más problemático, ese que tiene un fichero de configuración suelto y te ha dado la lata con los permisos, y pásalo a configs. Empieza por el nginx.conf, que es el ejemplo de siempre, ejecuta un docker compose config para ver que todo resuelve bien, y levanta. Si te funciona, ya no vas a querer volver atrás.

Y para el próximo episodio te tengo preparada una sorpresa que llevo semanas montando: todos los accesos que hay a mi servidor, con un Docker Compose que se ha convertido en un auténtico monstruo, lleno de ficheros de configuración. Un sitio perfecto para poner en práctica todo lo que hemos visto hoy con los configs. Nos vemos el próximo día. 🐧


Más información

Deja una respuesta