Saltar al contenido
← Blog

Docker Engine 29: qué se rompe

El mínimo de API se movió dos veces, el almacén de imágenes solo cambia en instalaciones nuevas y el límite de descriptores de fichero de todos los contenedores bajó en silencio de 1048576 a 1024. Nueve meses de consecuencias, contrastados con upstream.

·17 min de lectura
  • Docker
  • containerd
  • Linux
  • DevOps

Docker Engine 29 salió en noviembre de 2025 y rompió una cantidad notable de software que llevaba años funcionando sin problemas. Nueve meses después, lo interesante ya no es la rotura: es que casi todo lo que se escribió durante la primera quincena hoy es incorrecto, porque el cambio que documentó todo el mundo se revirtió en parte tres versiones menores después. Si estás actualizando un servidor Linux este mes, estás leyendo recomendaciones escritas para una versión de Docker 29 que ya no existe.

Diagrama de los cambios de Docker Engine 29 que rompen cosas: la versión mínima de API subida a 1.44 y luego bajada a 1.40, el image store de containerd como opción por defecto en instalaciones nuevas, el límite nofile de los contenedores cayendo de 1048576 a 1024, las variables de los links legacy eliminadas y las cadenas de aislamiento de iptables borradas.
Cinco cambios. Solo el primero está bajo el epígrafe de las notas de la versión que dice "breaking changes"; los otros cuatro andan repartidos entre el empaquetado, la red y un recuadro de aviso.

Esto es lo que se rompe de verdad, en el orden en que es probable que te lo encuentres, con la solución y la fuente oficial de cada caso. Está escrito para quien ejecuta Docker Engine en servidores Linux, no para Docker Desktop, donde varias de estas preguntas tienen otra respuesta. Los comandos de diagnóstico solo leen estado; todo lo que modifica el sistema es una edición de configuración, una instalación o un reinicio, y se ve a la legua que lo es. Donde upstream no ha publicado fecha ni versión, este artículo lo dice en lugar de inventarla.

Empieza por el síntoma, no por el changelog

Casi nadie llega a esta página desde el changelog. Se llega desde una cadena de error, desde un contenedor que no arranca o desde un panel que dice que el entorno no está accesible. Por eso esta tabla va primero: localiza tu síntoma y luego lee la sección que lo explica.[rel29]

Lo que vesQué es en realidadDónde se arregla
client version 1.24 is too old. Minimum supported API version is 1.44Un cliente más antiguo que el mínimo de API del daemon. El mínimo subió en 29.0.0 y volvió a bajar en 29.3.0.Actualiza el cliente. La excepción del lado del daemon es un puente, no un arreglo.
client version 1.52 is too new. Maximum supported API version is 1.44La imagen especular: cliente nuevo contra daemon viejo. Casi siempre un servicio Docker-in-Docker fijado en CI.Fija la imagen DinD a la misma versión mayor que el daemon, y muévelas juntas.
docker images sale vacío tras instalar la 29El image store de containerd es el backend por defecto en instalaciones nuevas. Tu contenido de overlay2 está oculto, no borrado.Vuelve a cambiar el backend, o exporta y reimporta a conciencia.
Los contenedores chocan con techos de conexiones que antes no existíanEl límite nofile por defecto dentro de los contenedores bajó de 1048576 a 1024.--ulimit por contenedor, o default-ulimits en el daemon.
Los jobs de CI fallan en la comprobación de salud de un servicio; No HOST or PORT foundLas variables de entorno de los links legacy ya no se inyectan, así que el helper que espera al servicio nunca lo ve.Una red definida por el usuario y DNS. La variable de escape es temporal.
Un contenedor alcanza un puerto publicado al que no debería llegarLas cadenas DOCKER-ISOLATION-STAGE-1 y -STAGE-2 se eliminaron en la 29.0.Publica en 127.0.0.1 en lugar de en la dirección comodín.
Tus reglas de cortafuegos en DOCKER-USER dejaron de aplicarseSolo pasa si activaste el backend de nftables, que es experimental y opt-in.Vuelve atrás, o escribe tu propia tabla de nftables con una prioridad menor.
Una build de Go falla en github.com/docker/dockerEl módulo se movió a github.com/moby/moby/client y .../api.Reescribe los imports; la API del cliente cambió de forma al mismo tiempo.
# Three numbers decide everything that follows. Get them before you change
# anything, on the machine that is actually misbehaving.

# 1. Engine version, and the API floor this daemon enforces.
docker version
# Server: Docker Engine - Community
#  Engine:
#   Version:          29.3.0
#   API version:      1.54 (minimum version 1.40)
#                                             ^^^^
# Do NOT assume 1.44. Engine 29.0.0 raised the floor from 1.24 to 1.44, and
# 29.3.0 lowered it again to 1.40. Which one you get depends on where in the
# 29 series you landed, and almost everything written about this in late 2025
# predates the second change. Read the number, do not remember it.

# 2. Which image store this daemon is using.
docker info -f '{{ .DriverStatus }}'
# [[driver-type io.containerd.snapshotter.v1]]              -> containerd store
# [[Backing Filesystem extfs] [Supports d_type true] ...]   -> legacy overlay2
docker info 2>/dev/null | grep -i 'storage driver'
# Storage Driver: overlayfs   -> containerd snapshotter
# Storage Driver: overlay2    -> graph driver

# 3. Which packet-filtering backend. nftables is opt-in and experimental in 29.x,
#    so on an untouched host this should say iptables.
docker info 2>/dev/null | grep -i 'firewall'

# And the one that is not a Docker question, but constrains Docker anyway:
stat -fc %T /sys/fs/cgroup/
# cgroup2fs -> unified hierarchy, nothing to do
# tmpfs     -> cgroup v1 or hybrid: deprecated in 29.0, supported until May 2029

Tres de esas filas son el mismo cambio de fondo visto desde ángulos distintos, y dos no están documentadas en ningún sitio donde se te ocurriría mirar. Antes de tocar nada, establece los tres datos que determinan cuáles te afectan.

El mínimo de API se movió dos veces, y el número que has leído seguramente está mal

Este es el famoso. Engine 29.0.0 subió hasta 1.44 la versión mínima de la Engine API que acepta hablar el daemon, y 1.44 corresponde a Docker 25.0. El mínimo anterior era 1.24, introducido a su vez en Docker 25.0 en sustitución de un mínimo de 1.12 que se mantenía desde 2016, así que este fue el segundo apretón en dos años y el primero del que se enteró alguien. Cualquier cliente que llevara una versión antigua fijada a fuego, o que nunca aprendiese a negociarla, dejó de funcionar en cuanto se reinició el daemon. Por eso la actualización se llevó por delante Traefik, Portainer, Testcontainers, Watchtower, media docena de paneles self-hosted y unos cuantos pipelines de CI en la misma semana.[pr51186][blog29]

Aquí viene la parte que casi ningún artículo publicado incluye. En 29.3.0, publicada en marzo de 2026, upstream volvió a bajar el mínimo: de 1.44 otra vez a 1.40, que corresponde a Docker 19.03. El valor por defecto en el código actual es 1.40, y 1.24 solo sigue siendo alcanzable mediante una excepción explícita. Así que un cliente en API 1.41 o 1.43 que era rechazado en diciembre hoy vuelve a funcionar, y una guía que te diga que el mínimo es 1.44 está describiendo las tres primeras líneas menores de una serie que ya va muy por delante de ellas. Comprueba tu propio daemon en lugar de fiarte de lo que recuerda internet: la correspondencia entre release y versión de API está publicada, y es la única versión de esta historia que se mantiene cierta.[pr52067][config][apimatrix]

Versión del daemonVersión mínima de APIQué rechaza eso
25.0 – 28.x1.24Prácticamente nada. Los engines anteriores tenían un mínimo de 1.12, sin cambios desde 2016.
29.0.0 – 29.2.x1.44Todo cliente anterior a Docker 25.0. Esta es la oleada de roturas de la que escribió todo el mundo.
29.3.0 y posteriores1.40Clientes anteriores a Docker 19.03. Una relajación relevante, y casi nunca mencionada.
Cualquiera, con la excepciónhasta 1.24No hay flag de línea de comandos: la clave de daemon.json o DOCKER_MIN_API_VERSION. Upstream la llama opción solo para casos excepcionales y no publica fecha de eliminación.
# The symptom, produced by the daemon, not the client:
#
#   Error response from daemon: client version 1.24 is too old.
#   Minimum supported API version is 1.44, please upgrade your client to a
#   newer version
#
# And its mirror image, which appears in CI far more often than on servers -
# a NEW client talking to an OLD daemon, typically a pinned docker:dind service:
#
#   Error response from daemon: client version 1.52 is too new.
#   Maximum supported API version is 1.44

# The correct fix is always to update the client. Every tool listed later in
# this article shipped a build that negotiates the API version instead of
# hardcoding one.

# The escape hatch is for the case where the client is a vendor appliance you
# cannot update this week. It is set on the DAEMON, not on the client, and it
# is available in daemon.json only - there is no dockerd command-line flag.
cat /etc/docker/daemon.json 2>/dev/null   # read it first, do not clobber other keys

# /etc/docker/daemon.json
{
  "min-api-version": "1.24"
}

sudo systemctl restart docker
docker version | grep -i 'minimum version'

# The equivalent, if you would rather leave daemon.json alone:
sudo systemctl edit docker.service
# [Service]
# Environment="DOCKER_MIN_API_VERSION=1.24"

# Upstream is unambiguous about the status of both: API versions older than the
# default "are deprecated and to be removed in a future release", and the
# environment variable and configuration option "should only be used for
# exceptional cases". No removal date is published anywhere. Treat this as a
# bridge of unknown length, put a ticket on it, and do not let it become the
# permanent shape of your fleet.

Una advertencia sobre la excepción, porque es de esos ajustes que sobreviven al incidente que los creó. Upstream describe las versiones antiguas de la API como obsoletas y destinadas a desaparecer en una release futura, y describe tanto la variable de entorno como la clave de configuración como algo reservado a casos excepcionales. No hay fecha de eliminación publicada. La propia política de Docker es conservar una función obsoleta durante al menos una release estable y aspirar a dar meses de aviso antes de retirarla, así que esto no va a desaparecer de la noche a la mañana; pero "al menos una release" es un suelo, no un plan. Úsalo para sobrevivir una semana, no para definir una política.[config][lifecycle]

"Han desaparecido todas mis imágenes" es una reinstalación, no una actualización

El segundo problema más reportado es el más alarmante y el menos peligroso: instalas Docker 29, ejecutas docker images y la lista está vacía. No se ha borrado nada. Engine 29 convierte el image store de containerd en el backend por defecto, pero —y esta es la frase que resuelve casi todos los casos— solo en instalaciones nuevas. Una actualización de paquetes in situ mantiene el driver overlay2 y te sigue mostrando tus imágenes. Purgar los paquetes y reinstalarlos cuenta como instalación nueva, y reconstruir el host desde la gestión de configuración también, que es como la mayoría se topa con esto sin creer haber hecho nada raro.[cstore]

Preguntaoverlay2 (graph driver)snapshotter de containerd
Cuándo te tocaCualquier actualización desde 28 o anterior lo mantieneInstalaciones nuevas de 29.0+, salvo con userns-remap
Qué reporta docker infoStorage Driver: overlay2Storage Driver: overlayfs y un driver-type de io.containerd.snapshotter.v1
Dónde vive el contenidoBajo data-root, normalmente /var/lib/dockerEn el almacenamiento propio de containerd, que data-root no mueve
Huella en discoSolo capas sin comprimirComprimidas y sin comprimir, así que bastante más grande para las mismas imágenes
Imágenes multiplataforma y atestacionesNo soportadasSoportadas: el motivo real del cambio de valor por defecto
Cambiar de uno a otroOculta las imágenes y contenedores del otro backendIgual a la inversa. No se borra nada; no hay conversión in situ
# "I upgraded and all my images are gone."
#
# Almost every report of this turns out to be a REINSTALL, not an upgrade.
# Docker 29 makes the containerd image store the default on fresh installations
# only; an in-place package upgrade keeps the overlay2 graph driver. Purging the
# packages and installing them again counts as fresh, and so does rebuilding the
# host from your configuration management.
#
# Nothing has been deleted. The two backends cannot see each other's content:
# switching "temporarily hides images and containers created with the other
# backend. Your data remains on disk."

docker info -f '{{ .DriverStatus }}'
docker system df

# To see the old content again, point the daemon back at where it lives:
# /etc/docker/daemon.json
{
  "features": { "containerd-snapshotter": false },
  "storage-driver": "overlay2"
}
sudo systemctl restart docker
# Understand what this buys you: the legacy graph drivers are themselves now
# deprecated, and Docker states the graph driver backend will be removed in a
# future release. Switching back is a stay of execution, not a destination.

# Moving forward deliberately instead. There is no supported in-place
# conversion: the documented paths are a registry round trip, or save/load.
docker save -o /var/tmp/keep.tar app:1.4 app:1.5
# ... switch the daemon to the containerd store, restart, then:
docker load -i /var/tmp/keep.tar

# An experimental automatic switch exists. Read what it actually does before
# using it: it only fires when there are NO containers at all and the image
# count is at or below the threshold you set.
# /etc/docker/daemon.json
{
  "features": { "containerd-migration": true }
}
# and, for the threshold, a systemd drop-in:
#   Environment="DOCKER_MIGRATE_SNAPSHOTTER_THRESHOLD=5"
# The documentation labels this experimental and tells you to take backups
# first. On a server, exporting the handful of images you actually care about
# is less work than recovering from a migration that half-happened.

# Two operational consequences that surface weeks later, not on day one:
#
#  - The containerd store keeps layers both compressed and uncompressed, so the
#    same images occupy noticeably more disk than they did under overlay2.
#
#  - "data-root" in daemon.json does NOT move containerd's content. If you put
#    /var/lib/docker on its own partition, containerd's storage is configured
#    separately - otherwise it quietly fills the root filesystem instead, which
#    is a page you will read at 03:00 rather than now.
#
#  - The containerd store is unavailable when user-namespace remapping is
#    enabled. That is a known bug, not a policy, and userns-remap hosts are
#    excluded from the fresh-install default for exactly that reason.

Los dos backends no ven el contenido del otro. Upstream lo dice explícitamente: cambiar de uno a otro "oculta temporalmente las imágenes y contenedores creados con el otro backend" y "tus datos siguen en disco". Si acabas de cambiar y la lista se ve vacía, no empieces a hacer prune para recuperar espacio: estarías borrando la mitad que ves mientras sigues pagando por la mitad que no ves.

Hay dos consecuencias que aparecen semanas después, no el mismo día. El almacén de containerd guarda las capas comprimidas y sin comprimir, así que las mismas imágenes ocupan bastante más disco del que ocupaban con overlay2. Y data-root en daemon.json no mueve el contenido de containerd: si te molestaste en poner /var/lib/docker en su propio volumen, el almacenamiento de containerd se configura aparte y, si no lo haces, llenará el sistema de ficheros raíz en su lugar. Hay además un caso en el que el nuevo valor por defecto sencillamente no se aplica: los hosts con remapeo de espacios de nombres de usuario quedan excluidos, por un bug abierto y no por una decisión de diseño. Y ten claro qué te compra volver atrás: los graph drivers legacy están ellos mismos obsoletos, y Docker ha dicho que ese backend se eliminará en una release futura. Volver a overlay2 es un aplazamiento de la condena.[daemon][iss47377]

El límite de descriptores de fichero de todos los contenedores bajó a 1024

Este es el cambio que apostaría a que se diagnostica mal más veces, porque no está en el apartado de cambios incompatibles: es un subpunto colgando de una subida de versión de containerd. Docker Engine 29.0.0 empaquetó containerd 2.1.5, que dejó de fijar LimitNOFILE=infinity en su unidad de systemd, así que el límite blando por defecto de ficheros abiertos dentro de todos los contenedores baja de 1048576 a 1024. Las releases posteriores de la 29.x llevan versiones más nuevas de containerd, y todas conservan el comportamiento nuevo.[iss51485][ctd215]

# The 29.0 change with the widest blast radius is not under a "breaking
# changes" heading at all. It is a sub-bullet under a containerd version bump.

docker run --rm ubuntu:24.04 bash -c 'ulimit -n; ulimit -Hn'
# soft 1048576   with containerd.io 1.7.x (Docker 28 and earlier)
# soft 1024      with containerd.io 2.1.5, shipped with Docker Engine 29.0.0
#
# Only the SOFT limit is documented as changing. The hard limit becomes
# whatever systemd's DefaultLimitNOFILE gives you on that host, so read both
# numbers on your own machine rather than trusting a value from an article.

# What happened: containerd 2.1.5 stopped setting LimitNOFILE=infinity in its
# systemd unit, so containers now inherit systemd's ordinary default. Docker
# Engine made the same change for BUILD containers back in v25.0; version 29
# extends it to every container. The reasoning is sound - with an effectively
# unlimited value, software that sizes its own buffers from `ulimit -n`
# (MySQL is the classic case) could consume the machine - and it is documented
# in the release notes. It is simply not where anyone looks.

# 1024 is a real ceiling, and the failures it produces rarely mention file
# descriptors: an Nginx worker refusing connections at a number that looks
# arbitrary, a JVM selector loop throwing at load, a connection pool that
# stalls under exactly the traffic it handled last week.

# Per container, which is the right place if only one workload needs it:
docker run --ulimit nofile=65535:65535 ...

# Or restore a default for every container on the host:
# /etc/docker/daemon.json
{
  "default-ulimits": {
    "nofile": { "Name": "nofile", "Soft": 65535, "Hard": 65535 }
  }
}

# The release notes show 1048576 in this example, which restores the exact old
# behaviour. Prefer a considered number. Copying 1048576 back wholesale also
# copies back the problem the change was made to solve, and you will not be the
# one who remembers that in two years.

El razonamiento es bueno y los maintainers lo explican con claridad: un valor efectivamente ilimitado combinado con una peculiaridad del runtime de Go podía elevar el límite blando hasta el duro, y el software que dimensiona sus propios búferes a partir de ulimit -n —MySQL es el ejemplo de siempre— se comía la máquina. Docker ya hizo el mismo cambio para los contenedores de build en la v25.0; la 29 simplemente lo extiende a todos. El problema no es el cambio, es que 1024 es un techo real y los fallos que provoca casi nunca mencionan descriptores de fichero. Te encuentras un worker de Nginx rechazando conexiones en una cifra que parece arbitraria, un selector de la JVM petando bajo carga, un pool que se atasca justo con el tráfico que aguantaba la semana pasada. Si actualizaste y algo se volvió más lento o más inestable bajo carga sin que cambiara ni una línea de código, mira esto primero.[rel29]

Docker 29 dejó de inyectar las variables de entorno de los links legacy —DB_PORT_5432_TCP_ADDR y familia— dentro de los contenedores. Estaban obsoletas desde hace años y el sustituto, DNS en una red definida por el usuario, funciona desde 2016. El problema es que muchísimos scripts de entrypoint de contenedor y helpers de CI las siguen leyendo, y el fallo resultante no menciona a Docker por ningún lado. El caso canónico es GitLab Runner: el contenedor auxiliar que espera a que arranque una entrada de services: termina con FATAL: No HOST or PORT found, así que el job falla en una comprobación de salud mientras el contenedor de servicio funciona perfectamente.[pr50719][gllinks]

# Docker 29 stopped injecting the legacy link environment variables:
#   DB_PORT_5432_TCP_ADDR, DB_PORT_5432_TCP_PORT, DB_NAME, DB_ENV_*
# They were deprecated years ago. A surprising number of CI images and entry
# point scripts still read them, and the failure does not mention Docker.

# The symptom in a GitLab CI job with a services: block is a health check that
# never passes: the runner's wait-for-service helper exits 1 with
#   FATAL: No HOST or PORT found
# and the job fails before your script runs. The service container itself
# started perfectly well - which is why this looks like an infrastructure
# outage rather than a Docker change.

# Confirm it in two lines. The db container must be on the DEFAULT BRIDGE:
# legacy links do not work on user-defined networks at all.
docker run -d --name db postgres:18
docker run --rm --link db:db alpine env | grep -c '^DB_PORT_'
# 0 on Engine 29, non-zero before it

# The fix is to stop parsing those variables. DNS on a user-defined network has
# worked since 2016, does not need --link at all, and survives restarts:
docker rm -f db
docker network create appnet
docker run -d --name db --network appnet postgres:18
docker run --rm --network appnet postgres:18 pg_isready -h db
# In Compose this is already the default: services on the same project network
# resolve each other by service name.

# The escape hatch, when the image is not yours to change and the release is
# next week. Daemon-side, and explicitly temporary:
sudo systemctl edit docker.service
# [Service]
# Environment="DOCKER_KEEP_DEPRECATED_LEGACY_LINKS_ENV_VARS=1"
sudo systemctl restart docker
# Upstream wording: "the escape hatch will be removed in a later version".

Este tiene su punta: en el momento de escribir esto, la issue correspondiente de GitLab Runner sigue abierta, y el workaround documentado es o bien poner la variable de entorno de escape en el daemon, o bien fijar la versión de Docker del runner. Si tu CI usa contenedores de servicio, pruébalo en un runner antes de desplegar la actualización a toda la flota; no porque sea difícil de arreglar, sino porque el fallo aparece en todos los jobs a la vez y parece una caída de infraestructura en lugar de un cambio de configuración.

Se eliminaron las cadenas de aislamiento, y eso cambia qué es alcanzable

Este es el cambio con menos ruido y más consecuencias. Engine 29 rehizo las reglas de iptables para las redes bridge y eliminó por completo las cadenas DOCKER-ISOLATION-STAGE-1 y DOCKER-ISOLATION-STAGE-2. Las notas de la versión describen los efectos sin rodeos, y merece la pena leerlas dos veces si alguna vez has tratado dos redes bridge separadas como una frontera de seguridad: ahora los contenedores pueden alcanzar puertos publicados en direcciones del host por contenedores de otras redes cuando el proxy de userland no está en marcha, y puertos en direcciones de contenedor de otras redes cuando se usa el modo de gateway nat-unprotected.[pr49981][packet]

# 29.0 reworked the iptables rules for bridge networks and removed the
# DOCKER-ISOLATION-STAGE-1 and DOCKER-ISOLATION-STAGE-2 chains. The release
# notes state the two consequences plainly:
#
#   - containers can now access ports published to host addresses by containers
#     in other networks when the userland proxy is not running
#   - containers can now access ports on container addresses in other networks
#     that have gateway mode "nat-unprotected"
#
# If you were using separate bridge networks as a security boundary, re-read
# that. It is a reachability change, and nothing in the upgrade tells you.

# Find every port you publish to a wildcard address. Each one is a port that
# containers in other networks may now reach through the host.
docker ps --format '{{.Names}} {{.Ports}}' | grep '0.0.0.0'

# The durable fix is to stop publishing to the wildcard when you only meant
# localhost. This has always been the correct form and does not depend on any
# chain existing:
docker run -p 127.0.0.1:5432:5432 postgres:18

# In Compose:
#   ports:
#     - "127.0.0.1:5432:5432"

# Check the current shape of the rules rather than the shape you remember:
sudo iptables -S | grep -c 'DOCKER-ISOLATION'   # 0 on Engine 29
sudo iptables -S DOCKER-USER

# DOCKER-USER still exists and still works under the default iptables backend.
# It is only absent if you deliberately switch the daemon to nftables - which
# is the next section, and the answer there is "not on a production host yet".
ComportamientoHasta 28.xDesde 29.0Qué hacer
Cadenas de aislamiento de redes bridgeDOCKER-ISOLATION-STAGE-1 y -STAGE-2EliminadasRevisa todo lo publicado en una dirección comodín.
Variables de entorno de los links legacySe inyectan solasNo se inyectanUsa DNS en una red definida por el usuario.
Cadena DOCKER-USERPresentePresente con iptables, ausente con nftablesNo actives el backend de nftables si dependes de ella.
Regla de mangle del checksum SCTPSolo con DOCKER_IPTABLES_SCTP_CHECKSUM=1EliminadaLa variable ya no tiene ningún efecto.
Gateway por defecto en macvlan e ipvlan-l2Se deduceSolo si --gateway está en la config de IPAMDefine el gateway explícitamente en la red.
Redes overlay cifradasRotas en 28.2.2 y en 25.0.13–14Rotas de la 29.0.0 a la 29.2.0La 29.2.1 lo arregla, pero un nodo parcheado no puede llevar tráfico a uno sin parchear. Saca a todo el Swarm de las builds afectadas a la vez.

Nada en la actualización te avisa de que esto ha pasado, y tu monitorización tampoco: es una ampliación de lo alcanzable, no un error. La respuesta duradera no es reponer las cadenas, sino dejar de publicar en la dirección comodín cuando lo que querías decir era localhost, que siempre fue la forma correcta y no depende de que exista ninguna cadena. Y ya que estás, apunta un riesgo de vuelta atrás: una red creada en 29 pidiendo un tamaño de prefijo a los pools de direcciones por defecto queda inutilizable en un daemon anterior y hay que borrarla y recrearla, así que registra tus definiciones de red antes de empezar y no después.[pr50114]

nftables existe, es experimental y no tiene cadena DOCKER-USER

Engine 29 también introdujo un backend de nftables, y conviene ser preciso con su estado porque la cobertura que se hizo no lo fue: es experimental y opt-in, el valor por defecto sigue siendo iptables, y no se puede activar en absoluto mientras el daemon esté en modo Swarm. Upstream dice sin matices que las opciones de configuración, el comportamiento y la implementación pueden cambiar. En una distribución actual, tus reglas de iptables ya las ejecuta de todos modos la maquinaria de nftables del kernel, así que cambiar aporta muy poco hoy. La razón para conocerlo ya es lo que le hace a tu cortafuegos: en la implementación de nftables de Docker no existe la cadena DOCKER-USER, y tus reglas no se migran a las tablas nuevas. Que sigan ejecutándose o no depende del historial de la máquina: pasar a nftables un host existente deja en su sitio el viejo salto desde FORWARD, así que las reglas siguen disparándose hasta que ese salto desaparezca o el host se reinicie, mientras que un host que nació en nftables nunca tuvo ese salto y las ignora en silencio. Dos máquinas con configuración idéntica, dos cortafuegos distintos.[nft][pr50476]

# The nftables backend in 29.x is EXPERIMENTAL and opt-in. The default is still
# iptables, which on a current distribution is usually iptables-nft underneath
# anyway - so you are already using the nftables kernel machinery either way.
# Upstream: "configuration options, behavior and implementation may all change
# in future releases", and it "cannot be enabled when the Docker daemon is
# running in Swarm mode".

docker info 2>/dev/null | grep -i 'firewall'

# Opting in, if you are testing it somewhere that is not production:
# /etc/docker/daemon.json
{
  "firewall-backend": "nftables"
}
sudo systemctl restart docker

sudo nft list tables
# table ip docker-bridges
# table ip6 docker-bridges

# The part that quietly changes your security posture:
# "In Docker's nftables implementation, there is no DOCKER-USER chain."
# Your rules are not migrated. Whether they still run depends on history:
# switching an existing host to nftables leaves the old FORWARD jump to
# DOCKER-USER in place, so those rules keep firing until the jump is removed
# or the host reboots. A host that started on nftables never had the jump, so
# the same rules do nothing at all. Both states look identical in your
# configuration management, which is the dangerous part.
sudo iptables -S FORWARD | grep DOCKER-USER   # if this prints, you have both worlds

# The replacement is your own table, with base chains of the same type and hook
# as Docker's and a LOWER priority number, so yours run first. "filter" is the
# same priority Docker uses, so subtract from it:
#
#   table ip my-filter {
#       chain my-forward {
#           type filter hook forward priority filter - 1; policy accept;
#           iifname "eth0" ip saddr != 192.0.2.2 counter drop
#       }
#   }

# One more difference that catches everybody: in nftables an accept is not
# final, so you cannot permit something Docker drops simply by accepting it
# earlier. Use a firewall mark and tell the daemon to honour it:
#   dockerd --bridge-accept-fwmark=1
#   dockerd --bridge-accept-fwmark=0x1/0x3    # with a mask

github.com/docker/docker ya no es tu ruta de importación

Si consumes la API de Docker desde Go, la versión 29 es una reescritura y no una subida de versión, y es el único cambio de esta página que no se puede sortear con una clave de configuración. El módulo github.com/docker/docker queda obsoleto en favor de github.com/moby/moby/client y github.com/moby/moby/api; el módulo padre pasa a ser explícitamente un detalle interno de implementación, y las releases se etiquetan con el prefijo docker-. Además del traslado, la propia API del cliente cambió de forma:[rel29]

  • Structs de opciones en lugar de argumentos posicionales en las operaciones de imagen, config y prune. Es una edición mecánica pero amplia: toca casi todos los puntos de llamada de una integración típica.
  • Los valores de retorno vienen envueltos en structs. ImageInspect, ImageHistory, ImageLoad e ImageSave ya no devuelven lo que devolvían, y ImagePull e ImagePush devuelven objetos que exponen iteradores de mensajes.
  • Los filtros se han movido. El cliente tiene su propio tipo de filtros, así que cualquier cosa que importe el paquete antiguo hay que refactorizarla, no reapuntarla.
  • Las direcciones ahora tienen tipo. Las IP y las subredes son netip.Addr y netip.Prefix en lugar de cadenas y net.IPNet, lo cual es una mejora real y una mañana de trabajo igual de real.
  • client.ImageCreate ha desaparecido, sustituido por ImagePull o ImageImport según lo que estuvieras haciendo realmente con él.

cgroup v1 queda obsoleto, y esta vez con fecha

Docker 29 marca cgroup v1 como obsoleto y, cosa rara en una deprecación, viene con fecha. Upstream afirma que el soporte continúa hasta mayo de 2029, y que puede que la última release de mayo de 2029 ya no soporte cgroup v1, pero al menos una rama mantenida sí lo hará. Es un margen genuinamente generoso, y significa que nada de tus servidores deja de funcionar este año por este motivo.[deprecated][iss51111]

Aun así conviene actuar pronto, por una razón que no tiene nada que ver con el calendario de Docker: tu distribución llegará antes. systemd eliminó las jerarquías legacy e híbrida en la v258, así que un host que siga arrancando con systemd.unified_cgroup_hierarchy=0 —normalmente un parámetro que alguien añadió hace años para tener contento a un runtime viejo y que nunca se quitó— choca contra ese muro a nivel de sistema operativo mucho antes de 2029. El parámetro es trivial de encontrar y casi siempre trivial de quitar; lo incómodo es descubrirlo en plena ventana de mantenimiento y no un martes cualquiera.[systemd258][cgroupv2]

El ecosistema: qué se rompió y en qué versión se arregló

La tabla de compatibilidad es la parte de esta historia que sí está bien documentada, en buena medida porque Portainer fue detallando las consecuencias mientras ocurrían. Casi todo esto se arregló en cuestión de semanas. El valor de leer la lista ahora no está en los arreglos, está en darte cuenta de cuáles de estos usas tú.[portblog]

HerramientaQué se rompióArreglado en
TraefikEl proveedor de Docker llevaba fijada la API 1.24, así que no se configuraba ninguna ruta.3.6.1: la versión fijada se sustituyó por negociación.
PortainerSu cliente empaquetado estaba topado en la API 1.41 sin negociación, y una comprobación estricta de versión mínima rechazaba la conexión; los entornos salían como inalcanzables.2.33.5 LTS / 2.36.0 STS.
Testcontainers para Javadocker-java usaba por defecto la API 1.32; los tests no encontraban un entorno Docker válido.2.0.2, que incluye un docker-java que negocia.
Docker SDK para PythonDEFAULT_DOCKER_API_VERSION era 1.41 en la 6.1.3.7.1.0 (1.44). Mejor aún: negocia en lugar de fijar.
GitLab RunnerVersiones descuadradas entre runner, imagen del job y servicio DinD; aparte, las comprobaciones de salud de los servicios fallan con No HOST or PORT found.Fija la imagen DinD o la versión de API. La issue de la comprobación de salud seguía abierta al escribir esto.
IDEs de JetBrainsEl plugin de Docker era rechazado por un daemon v29.Build 253.28294.x del plugin, incluida en la 2025.2.5 y en las releases 2025.3.
Watchtower (containrrr)Fijado a la API 1.25, en bucle de reinicios contra un daemon v29.Sin arreglo upstream. El repositorio se archivó en modo solo lectura en diciembre de 2025.
CapRoverdocker-modem fijado en la API 1.43: una versión por debajo del mínimo de la 29.0.1.14.1.
Ansible community.dockerKeyError: 'ApiVersion' por el cambio de mayúsculas del JSON de docker version.Engine 29.0.1 restauró los nombres de campo anteriores.
Docker ComposeLas builds muy antiguas quedan por debajo del mínimo de API; no hay matriz de compatibilidad publicada.Instala el docker-compose-plugin actual desde el mismo repositorio que el engine.

El patrón es consistente: todo lo que se rompió llevaba una versión de API fijada a fuego en algún sitio en lugar de negociarla, y todo lo que se arregló se arregló añadiendo negociación. Portainer es la ilustración más clara: su cliente empaquetado estaba topado en la API 1.41 y una comprobación estricta contra el mínimo que reportaba el daemon rechazaba la conexión de plano, contra un daemon que habría estado perfectamente dispuesto a hablar con un cliente que preguntara como es debido. GitLab Runner es incómodo por un motivo distinto: el runner, la imagen del job y el servicio de Docker-in-Docker llevan cada uno su propio cliente y dos cualesquiera pueden acabar en lados opuestos del mínimo. La regla práctica ahí es fijar la imagen del servicio DinD a la misma versión mayor que el daemon que la ejecuta, y cambiar ambas a la vez.[traefik][testcontainers][glrunner]

El resto de la lista es una auditoría bastante decente de tu propia cadena de suministro. Watchtower es el caso ilustrativo: fijado a la API 1.25, sin arreglo oficial y con el repositorio archivado en modo solo lectura en diciembre de 2025, o sea que lo que dejó de funcionar fue un proceso desatendido con acceso permanente a tu socket de Docker, y no hay nadie a quien pedirle el arreglo. Eso merece un momento de reflexión al margen de Docker 29. Si una herramienta tiene tu socket, su estado de mantenimiento forma parte de tu postura de seguridad, y esta actualización fue una auditoría inusualmente clara de cuáles de tus herramientas siguen teniendo mantenedores.[watchtower][portainer][jetbrains]

Actualizar, fijar la versión y volver atrás

Aquí no hay nada ingenioso en la mecánica. Actualizar es una transacción normal de apt o dnf con los cinco paquetes de siempre, y la forma documentada de instalar una versión concreta es la forma documentada de volver a ella. Lo que importa es el orden, y saber de antemano qué cosas no deshace una vuelta atrás.[installubuntu][installrhel]

# Upgrading is an ordinary package transaction. The care is in the order, and
# in knowing in advance what a rollback will and will not undo.

# --- Debian / Ubuntu ---------------------------------------------------------
apt list --all-versions docker-ce | head
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io \
                 docker-buildx-plugin docker-compose-plugin

# Staying on 28 deliberately - read the last section before choosing this:
sudo apt-mark hold docker-ce docker-ce-cli containerd.io
apt-mark showhold

# Going back. Take the exact version strings from the lists above. Pin
# containerd.io too: leaving it unpinned keeps containerd 2.x and its systemd
# unit, so the file-descriptor change described earlier survives the rollback.
apt list --all-versions containerd.io | head
VERSION_STRING=5:28.5.2-1~ubuntu.24.04~noble
CONTAINERD_STRING=1.7.29-1~ubuntu.24.04~noble
sudo apt install docker-ce="$VERSION_STRING" docker-ce-cli="$VERSION_STRING" \
                 containerd.io="$CONTAINERD_STRING" \
                 docker-buildx-plugin docker-compose-plugin

# --- RHEL / Rocky / AlmaLinux / Fedora ---------------------------------------
dnf list docker-ce --showduplicates | sort -r | head
sudo dnf install docker-ce docker-ce-cli containerd.io \
                 docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
# A specific version, in the same shape as the apt form:
sudo dnf install docker-ce-3:28.5.2-1.el9 docker-ce-cli-3:28.5.2-1.el9 \
                 containerd.io docker-buildx-plugin docker-compose-plugin
# Pinning is a dnf feature, not a Docker one, and the plugin package name
# depends on which dnf you have:
#   RHEL / Rocky / Alma (dnf4):  sudo dnf install python3-dnf-plugin-versionlock
#   Fedora (dnf5):               sudo dnf install dnf5-plugin-versionlock
#   then:                        sudo dnf versionlock add docker-ce docker-ce-cli containerd.io

# Four things a downgrade does not undo:
#
#  1. If the daemon was switched to the containerd image store, a 28.x daemon
#     cannot see those images. They are still on disk; they are not in the
#     graph driver, and there is no conversion.
#  2. A network created on 29 by asking the default pool for a prefix size
#     (--subnet 0.0.0.0/24) is unusable on an older daemon. It has to be
#     deleted and recreated - so record your network definitions before you
#     start, not after.
#  3. Rootless installations from 29.5 onwards no longer receive slirp4netns
#     through Docker packaging. Reinstall it from your distribution if you go
#     back to a build that expects it.
#  4. The container file-descriptor limit, unless you pinned containerd.io as
#     well. That change lives in containerd's systemd unit, not in dockerd.

Una cosa más si tu automatización parsea la salida de Docker en lugar de llamar a la API: la 29.0.0 cambió las mayúsculas de los campos en docker version --format=json, lo que rompió la colección de Docker de Ansible con un KeyError a secas. Se corrigió en la 29.0.1, así que solo afecta a quien siga en la primerísima release, pero es un buen recordatorio de que --format json es una interfaz con versión, y fijar una versión de parche es un seguro barato para cualquier cosa que la raspe.[ansible]

Verificar, en serio

"El daemon ha arrancado" no es verificar. Ejecuta esto antes de actualizar, guarda la salida, vuelve a ejecutarlo después y compara las dos: la clave es que casi todo lo que cambió en la versión 29 es un valor por defecto, y un valor por defecto distinto produce una salida distinta, no un error.

#!/usr/bin/env bash
# docker29-check.sh - Run it BEFORE the upgrade, keep the output, run it again
# afterwards and diff the two.
#
# Everything here only reads state, with one exception: the ulimit probe starts
# a throwaway container and will PULL ubuntu:24.04 if the host does not already
# have it. On a metered link, or on a host where you are watching disk, pull it
# in advance or swap in an image you already have.
set -uo pipefail

echo "=== engine, api floor, client ==="
docker version

echo "=== image store, storage driver, cgroup driver, firewall backend ==="
docker info 2>/dev/null | grep -Ei 'storage driver|driver-type|cgroup|firewall|logging driver|userns'

echo "=== the ulimit that changed underneath you ==="
docker run --rm ubuntu:24.04 bash -c 'ulimit -n; ulimit -Hn'

echo "=== everything that should be running, is ==="
docker ps --format '{{.Names}} {{.Status}} {{.Image}}' | sort

echo "=== images the daemon can actually see, and how much space they take ==="
docker images --format '{{.Repository}}:{{.Tag}}' | sort | head -50
docker system df

echo "=== published ports, from the host's point of view ==="
# -p needs root; without it you get the ports but not the owning process.
sudo ss -tlnp 2>/dev/null

echo "=== packet filtering: which world are we in ==="
sudo iptables -S DOCKER-USER 2>/dev/null || echo "no DOCKER-USER chain"
sudo iptables -S | grep -c DOCKER-ISOLATION
sudo nft list tables 2>/dev/null || echo "nft binary not present"

echo "=== every client that talks to this daemon ==="
# Anything listed here is a candidate for the API floor problem: an agent, a
# management UI, a CI runner, a monitoring exporter.
sudo ss -xp 2>/dev/null | grep docker.sock | sort -u

Dos cosas que el script no puede comprobar por ti. Primera, Compose: no hay publicada ninguna matriz de compatibilidad entre versiones de Compose y versiones de la Engine API, y la issue de upstream que la pedía se cerró como pregunta, así que el único enfoque fiable es instalar el docker-compose-plugin actual desde el mismo repositorio que el engine, en vez de razonar sobre qué build antigua podría seguir funcionando. Segunda, todos los procesos que tienen tu socket de Docker: un agente, un exporter, una UI de gestión, un runner de CI. Cada uno lleva su propio cliente de API, y cada uno es candidato a la primera sección de este artículo.[compose]

Entonces, ¿actualizo o no?

La respuesta es sí, y el motivo no es la lista de novedades. Upstream marca docker-28.x como unmaintained, y define unmaintained como que ya no se desarrolla activamente, no acepta contribuciones y está fuera del alcance de los avisos de seguridad. La única otra rama mantenida es 25.0, viva solo para dos distribuciones downstream concretas y con final de mantenimiento previsto en diciembre de 2026: dentro de cuatro meses, lo que la convierte en un plan B y no en un plan. Quedarse en la 28 no es la opción conservadora que parece; es ejecutar una versión sin soporte para evitar la molestia de una con soporte.[branches]

Tu situaciónLa respuesta honesta
Parque de Compose en un solo host, con imágenes que construyes túActualiza. Audita antes el ulimit -n de los contenedores; es el único cambio que probablemente te sorprenda.
Runners de CI con Docker-in-DockerPrueba primero en un runner. Fija la imagen del servicio DinD a la versión mayor del daemon, y cambia ambas a la vez.
Dependes de una UI de gestión self-hostedMira su estado de mantenimiento antes que el de Docker. En varias de estas herramientas el bloqueo era la UI, no el engine.
Mantienes reglas de cortafuegos propias en DOCKER-USERActualiza: siguen funcionando con el backend de iptables por defecto. No te pases a nftables.
Usas redes bridge separadas como fronteraLee la sección de red antes de actualizar, no después. Este es el cambio con consecuencias reales de seguridad.
Te quedas en la 28 por seguridaddocker-28.x está sin mantenimiento upstream y fuera del alcance de los avisos de seguridad. Esta es la opción más arriesgada, no la más segura.
  1. Inventaría los clientes antes que el daemon. Todo lo que tiene tu socket de Docker lleva su propio cliente de API. Esa lista, y no la versión del engine, es lo que determina si esta actualización es un no-evento o una tarde entera.
  2. Haz la auditoría de ulimit primero, todavía en 28. Ejecuta hoy docker run --rm ubuntu:24.04 bash -c 'ulimit -n' y anota a qué cargas les importa. Son los diez minutos más baratos de esta página y el cambio con más papeletas para aparecer como una regresión misteriosa de rendimiento quince días después.
  3. Actualiza in situ. No reinstales. Una actualización in situ mantiene overlay2 y mantiene tus imágenes a la vista. Si reconstruyes hosts desde la gestión de configuración, decide a conciencia qué image store debe usar el host reconstruido, porque el valor por defecto cambió por debajo de ese camino.
  4. Revisa otra vez tus puertos publicados. Las cadenas de aislamiento ya no están. Todo lo que publiques en 0.0.0.0 ahora es alcanzable desde contenedores de otras redes en casos en los que antes no lo era. Mueve a 127.0.0.1 los que querías dejar en local.
  5. No toques nftables. Es experimental, elimina la cadena DOCKER-USER y no está disponible en Swarm. Todavía no hay ninguna razón de producción para activarlo.

Si además vas a actualizar el sistema operativo de debajo, actualizar Ubuntu 24.04 a 26.04 en servidores cubre la release que elimina cgroup v1 por completo y cambia otros seis valores por defecto de paso. Si al leer esto lo que te ha quedado sobre todo es la duda de si la plataforma de contenedores compensa su complejidad, cuándo no usar Kubernetes defiende la respuesta pequeña, y arquitecturas cloud aburridas es el argumento largo a favor de elegir infraestructura que cambia despacio a propósito.

Preguntas frecuentes

¿Cuál es la versión mínima de API en Docker Engine 29?

Depende de la versión de parche, que es el detalle que se salta casi toda guía. Engine 29.0.0 subió el mínimo de 1.24 a 1.44, y la 29.3.0 volvió a bajarlo a 1.40. El valor por defecto actual en el código de upstream es 1.40, y 1.24 solo es alcanzable mediante una excepción explícita. Comprueba tu propio daemon con docker version —la sección del servidor imprime la versión de API y el mínimo en la misma línea— en lugar de fiarte de un número sacado de un artículo escrito a finales de 2025.

¿Cómo soluciono el error "client version is too old. Minimum supported API version is 1.44"?

Actualiza el cliente, que casi siempre es una herramienta y no la CLI de docker: Traefik 3.6.1, Portainer 2.33.5 LTS o 2.36.0 STS, Testcontainers para Java 2.0.2, el Docker SDK para Python 7.1.0, CapRover 1.14.1. Si de verdad no puedes actualizarlo esta semana, se le puede decir al daemon que acepte versiones antiguas poniendo "min-api-version": "1.24" en /etc/docker/daemon.json (no hay flag de línea de comandos equivalente) o DOCKER_MIN_API_VERSION=1.24 en un drop-in de systemd. Upstream describe ambas como algo para casos excepcionales y dice que las versiones antiguas se eliminarán en una release futura, sin publicar fecha.

¿Por qué han desaparecido todas mis imágenes de Docker tras actualizar a la versión 29?

Casi con total seguridad no han desaparecido. Engine 29 pone el image store de containerd por defecto solo en instalaciones nuevas, y los dos backends no ven el contenido del otro: upstream describe el cambio como que oculta imágenes y contenedores mientras los datos siguen en disco. Si hiciste una actualización de paquetes in situ deberías seguir en overlay2 y seguir viéndolo todo; si purgaste y reinstalaste, o reconstruiste el host desde la gestión de configuración, te tocó el nuevo valor por defecto. Ejecuta docker info -f '{{ .DriverStatus }}' para ver qué backend está activo. Para recuperar la vista anterior, pon "features": {"containerd-snapshotter": false} en daemon.json y reinicia. No ejecutes docker system prune mientras estés hecho un lío con esto.

¿Por qué mis contenedores empezaron a dar "too many open files" tras actualizar?

Porque el límite de descriptores de fichero por defecto dentro de los contenedores bajó de 1048576 a 1024. Docker Engine 29 empaqueta containerd 2.1.5, que dejó de fijar LimitNOFILE=infinity en su unidad de systemd, así que los contenedores heredan ahora el valor por defecto normal de systemd. Docker hizo el mismo cambio para los contenedores de build en la v25.0 y la versión 29 lo extiende a todos los contenedores. Arréglalo por carga con --ulimit nofile=65535:65535, o define default-ulimits en daemon.json. Elige un número que puedas justificar en vez de restaurar 1048576: el valor antiguo es precisamente de lo que se quería huir con este cambio.

¿Docker 29 rompe mis reglas de cortafuegos en DOCKER-USER?

Con el backend de iptables por defecto no, ahí DOCKER-USER sigue existiendo y sigue funcionando. Solo desaparece si activas deliberadamente el backend experimental de nftables, donde upstream dice sin rodeos que no hay cadena DOCKER-USER y que tus reglas no se migran. Hay una trampa en cómo dejan de aplicarse: pasar a nftables un host existente deja en su sitio el viejo salto desde la cadena FORWARD, así que las reglas siguen disparándose hasta que ese salto se elimine o el host se reinicie, mientras que un host instalado de cero con nftables las ignora desde el principio. Lo que sí cambió para todo el mundo es que las cadenas DOCKER-ISOLATION-STAGE-1 y DOCKER-ISOLATION-STAGE-2 se eliminaron, lo que amplía lo que los contenedores de una red pueden alcanzar en otra. Eso es un cambio de alcanzabilidad, no de cortafuegos, y es la parte de esta release que merece una auditoría.

¿Conviene activar el backend de nftables en Docker 29?

En un host de producción no. Es explícitamente experimental —upstream avisa de que las opciones de configuración, el comportamiento y la implementación pueden cambiar—, no se puede activar mientras el daemon esté en modo Swarm y elimina la cadena DOCKER-USER. En una distribución moderna tus reglas de iptables ya las ejecuta la maquinaria de nftables del kernel, así que la ganancia práctica hoy es pequeña. Si aun así quieres probarlo, hazlo en un host donde te puedas permitir equivocarte con el cortafuegos.

¿Es seguro quedarse en Docker Engine 28?

Menos seguro que actualizar, que es justo lo contrario de lo que parece. La tabla de ramas de upstream marca docker-28.x como sin mantenimiento, y define eso como que ya no se desarrolla activamente, no acepta contribuciones y está fuera del alcance de los avisos de seguridad. Las únicas ramas mantenidas son docker-29.x y 25.0, y la 25.0 se mantiene viva para dos distribuciones downstream concretas, con final de mantenimiento previsto en diciembre de 2026. Fijarte a la 28 durante quince días mientras arreglas un cliente es razonable; fijarte a ella como política significa ejecutar una versión que no va a recibir arreglos de seguridad.

¿Se puede volver de Docker 29 a la 28?

Sí: instala la versión antigua concreta con tu gestor de paquetes, exactamente como describe la documentación de instalación. Hay tres cosas que la vuelta atrás no deshace. Si el daemon se pasó al image store de containerd, un daemon 28.x no puede ver esas imágenes; están en disco pero no en el graph driver, y no hay conversión. Una red creada en 29 pidiendo un tamaño de prefijo a los pools de direcciones por defecto es inutilizable en un daemon anterior y hay que borrarla y recrearla. Y las instalaciones rootless a partir de la 29.5 ya no reciben slirp4netns a través del empaquetado de Docker, así que reinstálalo desde tu distribución si vuelves a una build que lo espera.

¿Docker Engine 29 sigue soportando cgroup v1?

Sí. La versión 29 lo marca como obsoleto, pero upstream se compromete a soportarlo hasta mayo de 2029, y señala que incluso entonces al menos una rama mantenida conservará el soporte. Nada de tus servidores deja de funcionar este año por culpa de Docker. La presión viene del otro lado: systemd eliminó en upstream las jerarquías legacy e híbrida, así que tu distribución de Linux dejará caer cgroup v1 bastante antes que Docker. Un host que siga arrancando con systemd.unified_cgroup_hierarchy=0 conviene limpiarlo ahora, mientras es un cambio de una línea y no un bloqueo para actualizar.

Fuentes

Las notas de versión de Docker, su documentación y el árbol de código de moby son la autoridad para todo lo relativo al engine; los issue trackers de los propios proyectos afectados, para las afirmaciones de compatibilidad. Dos advertencias honestas sobre la lista. La versión en la que se arregló cada herramienta de terceros sale por lo general de las notas de versión de ese proyecto y no del informe de bug enlazado aquí, así que sigue al proyecto si necesitas confirmarla. Y donde upstream no publica fecha ni versión —el caso destacado es la eliminación de la excepción de versión de API—, este artículo lo dice en vez de adivinar.

  1. Docker — Docker Engine v29 release notes: the authoritative changelog for every behaviour change described here, including the API floor, the containerd packaging bump and the networking rules
  2. Docker — Docker Engine v29: Foundational Updates for the Future; the announcement post, and the source for the DOCKER_MIN_API_VERSION workaround
  3. Docker — Engine API reference: the version matrix mapping each Engine release to its maximum and minimum API version, which is the only reliable way to know what your daemon will accept
  4. Docker — Deprecated Engine features: the table that carries the cgroup v1 deprecation and its May 2029 support horizon
  5. Docker — containerd image store: fresh installs versus upgrades, how to check which backend is active, and the fact that switching hides rather than deletes content
  6. Docker — Docker and nftables: the experimental backend, the tables it creates, and the statement that there is no DOCKER-USER chain
  7. Docker — Packet filtering and firewalls: the iptables model, the DOCKER-USER chain and the gateway modes referenced in the networking section
  8. Docker — dockerd reference: daemon.json keys, default-ulimits, storage-driver and the feature flags used in this article
  9. Docker — Configure the daemon, including the data directory location and why containerd's storage is configured separately
  10. Docker — Install Docker Engine on Ubuntu: the apt repository, the exact package set and the documented way to install a specific version
  11. Docker — Install Docker Engine on RHEL: the dnf repository and the equivalent version-pinning procedure
  12. Docker — Release lifecycle: the stages Docker applies to features and the notice it commits to before retiring them
  13. moby/moby — Branches and tags: the branch maintenance table showing docker-29.x maintained and docker-28.x unmaintained, and the definition of unmaintained
  14. moby/moby — daemon/config/config.go: the MaxAPIVersion, defaultMinAPIVersion and MinAPIVersion constants, and the comment describing min-api-version as an exceptional-case option
  15. moby/moby #51186 — daemon: raise minimum API version to v1.44, the change that shipped in 29.0.0
  16. moby/moby #52067 — lower minimum API version from v1.44 to v1.40, the change that shipped in 29.3.0 and that most published coverage predates
  17. moby/moby #49981 — the bridge iptables rework that removed the DOCKER-ISOLATION-STAGE-1 and DOCKER-ISOLATION-STAGE-2 chains
  18. moby/moby #50719 — legacy link environment variables are no longer added automatically, with the DOCKER_KEEP_DEPRECATED_LEGACY_LINKS_ENV_VARS escape hatch
  19. moby/moby #50476 — the --bridge-accept-fwmark daemon option that lets a firewall mark override Docker's drop rules
  20. moby/moby #50114 — requesting a prefix size from the default address pools, and the warning that such networks are unusable after a downgrade
  21. moby/moby #51485 — LimitNOFILE is silently changed to the host soft limit with the new containerd: the report, the maintainer's explanation and the resulting release-note text
  22. moby/moby #51111 — the cgroup v1 deprecation tracking issue for Docker Engine
  23. moby/moby #47377 — the userns-remap bug that keeps the containerd image store unavailable when user-namespace remapping is enabled
  24. containerd v2.1.5 — the runtime version packaged with Docker Engine 29, and the origin of the changed LimitNOFILE default
  25. Linux kernel documentation — Control Group v2, the hierarchy Docker will require once cgroup v1 support ends
  26. systemd v258 release notes — the removal of the legacy and hybrid cgroup hierarchies upstream, which is why your Linux distribution will drop cgroup v1 well before Docker does
  27. traefik #12253 — the Docker provider's hardcoded API version 1.24 against a v29 daemon, fixed by moving to version negotiation
  28. portainer #12925 — local Docker environment unreachable on Engine 29, the primary report carrying the maintainers' fixed-version announcement; builds before 2.33.5 capped their Docker client at API 1.41 and checked the daemon's reported minimum strictly
  29. Portainer — Docker v29, and the fall-out: the most complete public inventory of management tools broken by the API floor and their fixed versions
  30. testcontainers-java #11235 — docker-java's default API version rejected by Engine 29, and the properties file workaround
  31. GitLab Runner #39129 — API version mismatches between the runner, the job image and the Docker-in-Docker service on Engine 29
  32. watchtower #2122 — a tool pinned to API version 1.25 whose repository has since been archived read-only, and what that means for anything holding your Docker socket that nobody maintains
  33. docker/docker-py — the Docker SDK for Python, whose DEFAULT_DOCKER_API_VERSION moved from 1.41 in 6.1.3 to 1.44 in 7.1.0
  34. community.docker #1185 — KeyError: 'ApiVersion' from the docker version JSON shape change in 29.0.0, corrected in 29.0.1
  35. JetBrains IJPL-217878 — the IDE Docker plugin rejected by a v29 daemon until the plugin build shipped with the 2025.3 releases
  36. docker/compose #13371 — the absence of a published Compose-to-Engine API compatibility matrix, and why old Compose builds fail against v29

¿Te ha resultado útil?