# Por qué mi blog no va en Docker

Hace unas semanas conté [por qué este blog no tiene base de datos](/post/por-que-este-blog-no-tiene-base-de-datos), y la conversación que describía allí tiene una segunda parte que también se repite siempre. Cuando el interlocutor asume que no hay base de datos, respira, y pregunta lo siguiente: *«vale, ¿pero en qué lo tienes containerizado?»*. En nada. *«¿Ni un `docker compose`?»*. Ni eso.

El despliegue completo de este blog es **un rsync y un `systemctl restart`**. No hay imagen, no hay registro, no hay Dockerfile, no hay compose. Y como la vez anterior, no es una postura: es que para este problema concreto el contenedor añade una capa y no quita ninguna. Vamos con lo que hay, lo que gano, lo que pierdo, y —esto es importante— por qué en el trabajo sí uso contenedores a diario.

## Qué hay en lugar del contenedor

Sincronizo el código fuente a `/opt/blog` y reinicio el servicio. Eso es todo. El servicio es una unit de systemd que **compila el binario en el propio servidor antes de arrancar**:

```ini
[Service]
Type=simple
User=blog
Group=blog
WorkingDirectory=/opt/blog
ExecStartPre=+/usr/local/go/bin/go build -o /opt/blog/bin/server ./cmd/server
ExecStart=/opt/blog/bin/server
Restart=on-failure
RestartSec=5
EnvironmentFile=/opt/blog/.env
```

Ese `ExecStartPre` es la pieza que a la gente le chirría más y la que a mí me parece más cómoda. Subo fuentes, no artefactos. Reinicio y el servidor construye lo que va a ejecutar, con la biblioteca del sistema que tiene delante. Nunca tengo que preguntarme si el binario que subí se compiló contra la versión correcta de nada.

Y hay una razón concreta para hacerlo así, que además es la mejor objeción que se le puede poner a todo este post, así que la pongo yo: **este binario no es estático**. Uso bimg para el procesamiento de imágenes, bimg es un envoltorio de libvips, y eso significa CGO y una dependencia nativa de C en el servidor. Un `apt-get install libvips-dev` y a correr, pero es una dependencia, y las dependencias nativas son exactamente el terreno donde Docker brilla. Compilar en el destino es mi manera de resolver ese problema sin imagen: si la libvips del servidor cambia, el siguiente reinicio recompila contra ella y me entero al instante en vez de dentro de tres semanas con un fallo raro.

## Lo que Docker resuelve de verdad

Antes de defender lo mío quiero ser honesto sobre lo que estoy renunciando, porque los contenedores no se inventaron por moda.

Docker resuelve, sobre todo, **el aislamiento de dependencias**. Cuando tienes que ejecutar tres aplicaciones en la misma máquina y cada una quiere una versión distinta de una biblioteca, un runtime o un intérprete, la imagen es la respuesta correcta y todo lo demás es sufrimiento. Resuelve la **reproducibilidad**: el mismo digest de imagen corriendo en tu portátil, en CI y en producción, con la garantía de que dentro hay literalmente lo mismo. Resuelve la **multiplicidad**: si necesitas N copias de un servicio detrás de un balanceador, o levantar y tirar entornos efímeros, no hay comparación. Y resuelve la **heterogeneidad**: una plataforma con componentes en varios lenguajes se despliega igual toda, y eso vale mucho dinero en cabezas de operaciones.

Ninguno de esos cuatro problemas lo tengo aquí. Es una aplicación, un proceso, una máquina, un lenguaje y una dependencia nativa.

## Lo que systemd ya te está dando

Y ahora la parte que me parece que más gente se pierde, porque cuando alguien dice «lo meto en un contenedor por seguridad» casi siempre está describiendo cosas que su init ya sabe hacer desde hace años.

Estas son las líneas de mi unit que no he citado antes:

```ini
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/blog/content
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LimitNOFILE=65536
```

Léelas despacio, porque juntas son un sandbox. `ProtectSystem=strict` monta **todo el sistema de ficheros en solo lectura** para ese proceso; `ReadWritePaths` abre la única excepción que necesito, el directorio de contenido. `ProtectHome=true` hace que `/home` no exista para él. `PrivateTmp=true` le da un `/tmp` propio que se destruye al parar. `NoNewPrivileges=true` impide escalada vía setuid, para siempre y para todos sus hijos. Los tres `ProtectKernel*` le cierran la puerta a tocar `sysctl`, cargar módulos y manosear cgroups.

El proceso corre además como un usuario de sistema sin shell (`--shell /usr/sbin/nologin`), y si se cae, `Restart=on-failure` lo levanta a los cinco segundos. Los logs van a journald sin que yo configure nada. Si quisiera límites de recursos, `MemoryMax=` y `CPUQuota=` están a una línea, y usan los mismos cgroups que usaría el contenedor por debajo.

No estoy diciendo que esto sea equivalente a un contenedor —no lo es: no hay namespace de red propio, ni de PID, ni raíz de sistema de ficheros distinta—. Estoy diciendo que **para «que el servicio no pueda tocar lo que no es suyo» ya tenías la herramienta instalada**, y que mucha gente se lleva la complejidad de una imagen para conseguir lo que le daban ocho directivas en un fichero .ini. Lo de [systemd más allá del `systemctl start`](/post/systemd-mas-alla-del-systemctl-start) lo escribí precisamente por esto.

## Lo que sí me llevé de Docker

Aquí conviene una confesión, porque si no parecería que llevo diez años sin enterarme de nada.

Los contenedores me cambiaron la forma de escribir servicios **aunque no los use para este**. Antes de que existieran, un servicio típico mío leía la configuración de un fichero .ini que había que editar a mano en cada máquina, escribía sus propios logs rotados en un directorio que se llenaba, guardaba estado en cualquier sitio y asumía que la máquina era suya. Hoy no hago nada de eso, y no porque me obligue Docker: porque el modelo era mejor.

**La configuración va en variables de entorno.** Este blog lee `PORT`, `CONTENT_DIR`, `BASE_URL` y `API_TOKEN`, y punto. El `EnvironmentFile` de la unit es un fichero de líneas `CLAVE=valor` que no está en git. Lo mismo que haría un contenedor con su `--env-file`, sin contenedor.

**Los logs van a la salida estándar.** El binario no sabe qué es un fichero de log ni una rotación: escribe con `slog` a stdout y es systemd quien se ocupa de recogerlo en journald. Si mañana quisiera mandarlos a otro sitio, cambiaría la unit, no el código.

**El proceso hace una cosa.** No hay demonios hijos, no hay cron interno, no hay supervisión propia. El scheduler que publica los drafts vive dentro del mismo proceso porque es una gorutina, no un servicio aparte. Si se cae, se cae entero y systemd lo levanta.

**El estado está en un sitio identificable.** Todo lo mutable vive bajo `content/`, y eso es exactamente lo que `ReadWritePaths` deja escribir y lo único que hay que respaldar. Es la disciplina del volumen, aplicada a un directorio normal.

Esas cuatro reglas son lo que un contenedor te impone por la fuerza y lo que hace que un servicio sea desplegable en cualquier sitio. Yo me quedé con las reglas y me ahorré la imagen, que es una postura que solo se puede sostener mientras la varianza sea cero. El día que deje de serlo, el código ya está listo y lo único que faltará será escribir el Dockerfile.

## Lo que pierdo

Seamos serios, que si no esto es un panfleto.

**Pierdo el artefacto inmutable.** No hay un digest que identifique exactamente qué está corriendo. Tengo el commit de git, que es casi lo mismo, pero no es lo mismo: entre el commit y el binario hay un compilador y una libvips que podrían cambiar bajo mis pies.

**Pierdo el rollback instantáneo.** Volver a la versión anterior de una imagen es cambiar un tag y reiniciar. Aquí es un `git checkout` del commit anterior, otro rsync y otro reinicio, con su compilación de por medio. Son treinta segundos en lugar de tres, y todavía no he tenido la urgencia de necesitar esos veintisiete.

**Pierdo el build separado del arranque**, y esta es la de verdad. Si un día subo código que no compila, el `ExecStartPre` falla y el servicio no arranca. El proceso viejo ya se ha parado, así que eso es caída, no despliegue fallido. Lo mitigo compilando a mano antes de reiniciar —`go build` sin tumbar el servicio en marcha— pero es disciplina, no diseño, y algún día me morderá.

**Y el servidor necesita el toolchain de Go y el código fuente.** Un servidor de producción con compilador es algo que a mucha gente de seguridad no le gusta, y lo entiendo. Es un compromiso consciente para resolver lo de la dependencia nativa sin imagen.

## Cuándo sí uso contenedores

Todos los días laborables, para que quede claro.

La plataforma que mantengo en el trabajo es multi-tenant, tiene componentes en varios lenguajes, servicios que hay que escalar en horizontal, entornos de preproducción que se levantan y se tiran, y un CI que necesita construir siempre sobre lo mismo. Ahí un contenedor no es una capa: es la única manera sensata de que quince piezas distintas se desplieguen con el mismo verbo. Y para desarrollo local, cuando un proyecto necesita una versión concreta de una base de datos o de un runtime, levanto un compose sin pensarlo dos veces, porque la alternativa es ensuciarme la máquina.

La diferencia no es ideológica. Es de forma del problema. **El contenedor resuelve la varianza**: muchas piezas, muchas versiones, muchas máquinas, muchos entornos. Cuando la varianza es cero —una pieza, una versión, una máquina—, lo único que te queda del contenedor es su coste.

## La regla

He acabado con una regla sencilla que me sirve para decidir rápido: **si no puedo nombrar la varianza que estoy conteniendo, no meto un contenedor**. ¿Varias versiones del mismo runtime? Nómbrala, y adentro. ¿Necesito N copias idénticas? Nómbrala, y adentro. ¿Quiero que CI y producción sean bit a bit lo mismo? Nómbrala, y adentro.

¿Un proceso, en una máquina, escrito en un lenguaje que produce un binario, con una dependencia nativa que instala `apt`? Ahí no hay varianza que contener. Hay un `rsync`, una unit bien escrita y ocho directivas de aislamiento que ya estaban pagadas.

Y como decía en el otro post: no es que haya evitado la complejidad. Es que era opcional, y elegí el lado que se depura con `systemctl status`.
