Tar en macOS destroza archivos en Linux: lo validé en mi pipeline real de Railway y documenté los 3 casos que nadie menciona
Hay un hilo en Hacker News que resurfaceó esta semana con 107 puntos sobre un artículo de 2024: tar en macOS crea archivos que Linux no puede extraer limpiamente. La comunidad reaccionó como siempre: "usá GNU tar", "instalá gtar con homebrew", "esto es conocido desde hace años". Y sí, todo eso es correcto.
Pero hay algo que nadie está diciendo: los 3 escenarios específicos donde esto efectivamente rompe producción no son iguales entre sí, y cada uno tiene un fix distinto. Aprendí esto de la peor manera posible — con un deploy fallido a las 11pm que me llevó dos horas diagnosticar. Mi tesis es que la respuesta "usá GNU tar" es necesaria pero insuficiente si no sabés exactamente por qué tu caso particular explota.
Tar macOS Linux error de extracción en producción: el contexto que importa
Desde que migré de Vercel a Railway en 2024 (un fin de semana que me enseñó más sobre infraestructura real que meses de tutoriales), mi pipeline de deployment depende de artefactos .tar.gz que genero en macOS y extraigo en contenedores Linux. Durante meses funcionó bien. Hasta que no funcionó.
El problema de fondo es que BSD tar (el que viene en macOS) y GNU tar (el que corre en Ubuntu, Alpine, Debian) no son el mismo programa. Comparten nombre y sintaxis básica, pero difieren en cómo manejan metadatos extendidos. macOS agrega metadata del sistema de archivos HFS+/APFS que GNU tar no espera encontrar, y cuando la encuentra, puede ignorarla silenciosamente, fallar con warnings que no interrumpen el proceso, o — el peor caso — extraer archivos corruptos sin avisarte.
Verificá qué versión de tar tenés en macOS:
# En macOS
tar --version
# Output típico:
# bsdtar 3.5.3 - libarchive 3.5.3 zlib/1.2.11 liblzma/5.0.5 bz2lib/1.0.8
# En tu contenedor Linux (Alpine, Ubuntu, etc.)
tar --version
# Output típico:
# tar (GNU tar) 1.34
# Copyright (C) 2021 Free Software Foundation, Inc.
No son el mismo programa. Nunca lo fueron.
Los 3 casos reales donde tar macOS rompe un pipeline de Railway
Caso 1: Los archivos ._* de metadata Apple
Este fue el primer bug que encontré. Cuando macOS crea un .tar.gz desde una carpeta que tocaste con el Finder (o que tuvo atributos extendidos en algún momento), incluye archivos ._nombre_archivo con metadata HFS. Son invisibles en el Finder, pero están en el tar.
# Comprimí desde macOS sin precaución:
tar -czf artefacto.tar.gz ./dist/
# En el contenedor Linux, extraí y listé:
tar -tzf artefacto.tar.gz | grep "^\._"
# Output:
# ._index.html
# ._main.css
# ._chunk-abc123.js
# ... (uno por cada archivo del build)
En mi caso específico, tenía un script de Railway que tomaba el primer archivo .js del directorio para calcular un hash de verificación. El script encontraba ._chunk-abc123.js antes que chunk-abc123.js y el hash fallaba. El deploy completaba, pero la verificación post-deploy lanzaba una alerta. Tardé 90 minutos en conectar esos puntos.
Fix para el Caso 1:
# Opción A: Eliminar metadata antes de comprimir (en macOS)
COPYFILE_DISABLE=1 tar -czf artefacto.tar.gz ./dist/
# Opción B: Filtrar en extracción (en Linux)
tar -tzf artefacto.tar.gz | grep -v "^\._" | tar -xzf artefacto.tar.gz -T -
# Opción C: La que uso yo en el Dockerfile de Railway
# Instalar GNU tar en macOS via Homebrew y usarlo explícitamente
brew install gnu-tar
# Luego en el script de build:
gtar -czf artefacto.tar.gz --exclude="._*" --exclude=".DS_Store" ./dist/
La variable de entorno COPYFILE_DISABLE=1 es la más limpia porque actúa en el momento de creación. Sin embargo, si ya tenés archivos .tar.gz viejos en storage, necesitás la opción de filtrado en extracción.
Caso 2: Permisos que cambian silenciosamente
Este me costó más porque no había error. El deploy completaba verde, la app levantaba, pero ciertos endpoints devolvían 403. El contenedor no podía leer archivos que, en mi máquina local, tenían permisos 644.
El problema: BSD tar en macOS puede serializar permisos de manera diferente para archivos con ACLs (Access Control Lists) de APFS. Cuando GNU tar los extrae, interpreta esos permisos de una forma que puede resultar en bits distintos a los originales.
# En macOS, creé un archivo y verifiqué permisos:
ls -la config/settings.json
# -rw-r--r-- 1 juan staff 2048 jun 15 22:31 config/settings.json
# Empaquetá con BSD tar:
tar -czf config.tar.gz config/
# En el contenedor Linux, extraí y verifiqué:
tar -xzf config.tar.gz
ls -la config/settings.json
# -rw------- 1 1000 1000 2048 jun 15 22:31 config/settings.json
# ↑ Los permisos cambiaron de 644 a 600 — el grupo y otros perdieron lectura
No pasa siempre. Pasa cuando el archivo tuvo algún atributo extendido en algún momento de su historia en el filesystem de macOS. Es el tipo de bug que aparece en producción pero no en staging porque staging tiene otro historial de archivos.
Fix para el Caso 2:
# En el Dockerfile, forzar permisos explícitos después de extraer:
RUN tar -xzf artefacto.tar.gz && \
find ./config -name "*.json" -exec chmod 644 {} \; && \
find ./scripts -name "*.sh" -exec chmod 755 {} \;
# O mejor: en el script de build en macOS, normalizar permisos antes de empaquetar:
find ./dist -type f -exec chmod 644 {} \;
find ./dist -type d -exec chmod 755 {} \;
COPYFILE_DISABLE=1 gtar -czf artefacto.tar.gz ./dist/
La segunda opción es superior porque resuelve el problema en el origen, no en el destino. Si el problema aparece en el destino, ya dependés de que todos los Dockerfiles tengan el fix — y eventualmente alguien va a crear uno nuevo sin él.
Caso 3: Paths con espacios en nombres de archivos
Este es el más silencioso y el que el artículo de HN original no menciona con suficiente detalle. Si empaquetás desde macOS y algún archivo en el path tiene un espacio (cosa que en macOS el Finder hace completamente normal), el comportamiento de extracción en Linux depende de la versión exacta de GNU tar y cómo procesás la lista de archivos.
# En macOS tenía un directorio de assets:
ls "dist/static/Open Graph/"
# og-image.png
# og-video.mp4
# Al empaquetar con BSD tar, el path quedó como:
tar -tzf artefacto.tar.gz | grep "Open"
# dist/static/Open Graph/og-image.png
# dist/static/Open Graph/og-video.mp4
# En Linux, al extraer con un script que procesaba la lista:
for archivo in $(tar -tzf artefacto.tar.gz); do
# ⚠️ Esto rompe: "Open" y "Graph/og-image.png" son dos tokens separados
echo "Procesando: $archivo"
done
El tar en sí extrae correctamente con tar -xzf. El problema aparece cuando cualquier script downstream procesa la lista de archivos asumiendo que no hay espacios. En mi caso era un script de invalidación de CDN que leía los paths del tar para saber qué cachés limpiar.
Fix para el Caso 3:
# Mal: iterar con for sobre $(tar -t...)
for archivo in $(tar -tzf artefacto.tar.gz); do
invalidar_cache "$archivo" # rompe con espacios
done
# Bien: usar --null y read para manejar espacios correctamente
tar -tzf artefacto.tar.gz | while IFS= read -r archivo; do
invalidar_cache "$archivo" # funciona con espacios
done
# Mejor: evitar el problema desde macOS renombrando antes de empaquetar
find ./dist -name "* *" -exec bash -c 'mv "$0" "${0// /_}"' {} \;
COPYFILE_DISABLE=1 gtar -czf artefacto.tar.gz ./dist/
La renominación en origen es más robusta porque eliminás la fuente del problema. La iteración con read es un parche que funciona pero que el próximo developer va a romper cuando copie el loop sin entender por qué estaba escrito así.
Los errores que cometí antes de entender el patrón
Error 1: Confiar en que "el tar funcionó antes, va a funcionar siempre". Los archivos ._* aparecieron después de que empecé a abrir esa carpeta de assets con el Finder para preview. Antes del Finder, no había metadata. Después del Finder, sí. El pipeline era el mismo; el filesystem no.
Error 2: Leer solo los exit codes. GNU tar extrae archivos ._* con exit code 0. No hay error. Tu deploy está "verde" y en producción tenés basura metida en el directorio de build. Necesitás validación post-extracción, no solo exit codes.
Error 3: Instalar gtar pero seguir usando tar en los scripts. Después de brew install gnu-tar, en macOS el binario se llama gtar, no tar. Si seguís escribiendo tar en el script de build, seguís usando BSD tar. Lo hice durante una semana.
# Verificar qué tar está ejecutando tu script:
which tar # /usr/bin/tar → BSD tar (macOS default)
which gtar # /opt/homebrew/bin/gtar → GNU tar (Homebrew)
# Si querés que tar sea GNU tar sin cambiar los scripts:
echo 'export PATH="/opt/homebrew/opt/gnu-tar/libexec/gnubin:$PATH"' >> ~/.zshrc
source ~/.zshrc
tar --version # Ahora debería mostrar GNU tar
Este override de PATH es lo que terminé usando para mantener los scripts existentes sin modificarlos.
Mi configuración actual en Railway
Después de validar los tres casos, mi pipeline de build en macOS quedó así:
#!/bin/bash
# scripts/build-artefacto.sh
# Genera el tar.gz para deployment en Railway
set -euo pipefail
BUILD_DIR="./dist"
OUTPUT="artefacto-$(date +%Y%m%d-%H%M%S).tar.gz"
# 1. Normalizar permisos antes de empaquetar
echo "→ Normalizando permisos..."
find "$BUILD_DIR" -type f -exec chmod 644 {} \;
find "$BUILD_DIR" -type d -exec chmod 755 {} \;
find "$BUILD_DIR" -name "*.sh" -exec chmod 755 {} \;
# 2. Eliminar archivos de metadata macOS
echo "→ Limpiando metadata Apple..."
find "$BUILD_DIR" -name "._*" -delete
find "$BUILD_DIR" -name ".DS_Store" -delete
# 3. Crear el tar con GNU tar y sin metadata extendida
echo "→ Empaquetando con GNU tar..."
COPYFILE_DISABLE=1 gtar \
--exclude="._*" \
--exclude=".DS_Store" \
--exclude=".AppleDouble" \
--exclude=".LSOverride" \
-czf "$OUTPUT" \
"$BUILD_DIR"
# 4. Verificar que no haya archivos de metadata en el tar resultante
METADATA_COUNT=$(tar -tzf "$OUTPUT" | grep -c "^\._" || true)
if [ "$METADATA_COUNT" -gt 0 ]; then
echo "❌ ERROR: El tar contiene $METADATA_COUNT archivos de metadata Apple"
exit 1
fi
echo "✅ Artefacto creado: $OUTPUT"
echo " Archivos: $(tar -tzf "$OUTPUT" | wc -l | tr -d ' ')"
Y en el Dockerfile de Railway, el paso de extracción tiene su propia verificación:
# Dockerfile — fragmento relevante
FROM node:20-alpine AS runner
WORKDIR /app
# Copiar el artefacto
COPY artefacto.tar.gz .
# Extraer con verificación explícita
RUN tar -xzf artefacto.tar.gz && \
rm artefacto.tar.gz && \
# Verificar que no haya archivos ._* que se colaron
METADATA=$(find . -name "._*" | wc -l) && \
if [ "$METADATA" -gt 0 ]; then \
echo "ERROR: Metadata Apple detectada en extracción" && exit 1; \
fi && \
echo "Extracción limpia: $(find . -type f | wc -l) archivos"
Esta doble verificación — en el momento de crear y en el momento de extraer — es lo que me da confianza real. No confío en que el proceso siempre sea perfecto; confío en que si falla, lo voy a saber antes del deploy y no después.
Esto conecta con algo más grande que tar
Hace unas semanas escribí sobre mis specs en YAML para agentes y sobre la migración de pgbackrest a Barman. En ambos casos el patrón fue el mismo: una herramienta estándar que "funciona" en la mayoría de los casos, hasta que encuentra el edge case específico de producción. Tar es otro ejemplo de esto.
El riesgo real no es que tar sea difícil. Es que tar es tan familiar que nadie lo considera un punto de falla. Cuando el deploy se rompe a las 11pm, nadie piensa "probablemente sea tar". Y eso es exactamente por qué estos bugs duelen más de lo que deberían.
FAQ: tar macOS Linux error de extracción en producción
¿Por qué tar en macOS genera archivos ._* y cuándo aparecen?
Los archivos ._nombre son Resource Forks de HFS+/APFS — un mecanismo heredado para almacenar metadata de archivos. Aparecen cuando un archivo tuvo atributos extendidos en algún momento: permisos especiales, metadatos de Finder, etiquetas de color, o simplemente cuando el Finder abrió la carpeta para mostrar previews. No aparecen en todos los archivos; aparecen en los que tocó el sistema de archivos de macOS de cierta manera. Es no-determinístico desde la perspectiva del desarrollador.
¿COPYFILE_DISABLE=1 es suficiente o necesito GNU tar igual?
COPYFILE_DISABLE=1 evita que BSD tar incluya metadata extendida en el momento de creación. Es suficiente para el Caso 1 (archivos ._*). Para el Caso 2 (permisos con ACLs) y el Caso 3 (paths con espacios en scripts downstream), necesitás GNU tar y normalización de permisos. En la práctica, uso ambas cosas juntas porque el costo es cero y la combinación cubre más casos.
¿GNU tar en macOS via Homebrew tiene algún tradeoff?
El único tradeoff real es que se instala como gtar, no como tar, para no romper el sistema. Si sobreescribís el PATH para que tar apunte a GNU tar, tenés que ser consciente de que algunas herramientas del sistema macOS asumen BSD tar con comportamientos específicos. En la práctica, en 18 meses usando el override de PATH no tuve ningún problema, pero es algo que hay que saber.
¿Esto afecta a GitHub Actions o solo a builds locales? Afecta principalmente a builds locales en macOS y a cualquier runner de CI que corra en macOS. Los runners de GitHub Actions en Ubuntu ya usan GNU tar, así que el problema no aparece ahí. El riesgo real es cuando comprimís en macOS local y subís el artefacto para que un sistema Linux lo extraiga — que es exactamente el workflow de deploy manual o semi-manual.
¿Hay alguna forma de detectar si un tar.gz existente tiene metadata Apple sin extraerlo? Sí, con una línea:
# Listar archivos ._* sin extraer
tar -tzf artefacto.tar.gz | grep "^\._"
# Si no devuelve nada, el tar está limpio de metadata Apple
Podés incluir esto como validación en el CI antes de publicar el artefacto.
¿Por qué Docker build no protege contra esto?
Docker build copia los archivos al contexto de build, pero si el .tar.gz ya tiene metadata Apple dentro, esa metadata viaja dentro del tar — Docker no inspecciona el contenido del tar al copiarlo. El problema ocurre cuando tu Dockerfile hace RUN tar -xzf y extrae el tar corrupto dentro del contenedor. Docker ve un comando que termina con exit code 0 y asume que todo está bien.
Conclusión: el fix es fácil, pero la trampa no es técnica
GNU tar + COPYFILE_DISABLE=1 + verificación post-extracción resuelve los tres casos. La parte técnica está documentada arriba y podés copiarla en cinco minutos.
La trampa real es de actitud: tar es tan viejo y tan familiar que nadie lo agrega a la lista de cosas que pueden fallar. Yo tampoco lo tenía en la lista. Hasta que tuve un deploy roto a las 11pm con logs completamente verdes y media hora mirando código que no tenía ningún error.
Si trabajás con Kimi K2, Claude o cualquier LLM para generación de código, ninguno te va a advertir sobre este problema a menos que lo conozcas y lo preguntes explícitamente. Si tu stack toca Railway o cualquier infra containerizada, el problema puede aparecer sin ningún indicador visible.
Mi recomendación concreta: auditá los tars que actualmente tenés en producción o en storage con tar -tzf archivo.tar.gz | grep "^\._". Si devuelve resultados, tenés trabajo pendiente. Si no devuelve nada, bien — pero igual agregá la verificación al pipeline para que el próximo build local en macOS no rompa esa garantía silenciosamente.
Este es el tipo de problema que aparece en los logs de Railway como síntoma de otra cosa. Y eso es exactamente lo que lo hace caro.
Fuente original: Hacker News
Artículos Relacionados
Agentic coding no es una trampa: le respondí al post viral de HN con mis propios logs de producción
367 puntos en HN dicen que agentic coding es una trampa. Yo tengo logs que dicen algo más incómodo: a veces ahorra 3 horas, a veces me manda a un rabbit hole de 4. La diferencia no es el agente — es el contrato que firmás antes de mandarlo a laburar.
Barman reemplaza a pgbackrest: migré mis backups de Postgres en producción y esto encontré
pgbackrest quedó sin mantenimiento. Barman aparece trending en HN justo después. Hice la migración real en Railway, medí tiempos de restore, tamaño de backup y complejidad de configuración. Mi conclusión es incómoda: la comunidad está celebrando demasiado rápido.
Kimi K2.6 vs Claude vs GPT-5.5: lo puse contra mis casos reales de coding y los números me sorprendieron
El hype dice que Kimi K2.6 venció a Claude y GPT-5.5 en coding. Lo corrí contra mi propio codebase — no contra HumanEval cherry-picked — y lo que encontré cambia la pregunta que deberías estar haciendo.
Comentarios (0)
¿Qué pensás de esto?
Dejá tu comentario en 10 segundos.
Usamos tu login solo para mostrar tu nombre y avatar. Nada de spam.