🔥 Yarn 4 + Alpine: Когда Документация Врет ( или как я yarn c CI дружил )

Вступление для оптимистов
Решил запустить React на Alpine в CI? Поздравляю, ты только что открыл портал в мир "интересных" сюрпризов. Yarn 4 и Alpine дружат примерно как кошка с водой — технически возможно, но удовольствия ноль.

🐕 Тест на умность собаки #1
Даже дворняга понимает, что если в документации написано "works everywhere", нужно проверить. А ты поверил на слово и уже коммитишь в мастер.

💩 Ожидание vs Реальность
Ожидание: "Zero Installs работает из коробки"
Реальность: Нативные модули крашатся на Alpine из-за musl вместо glibc, и ты проведешь вечер в GitHub Issues от 2022 года с @swc/core.
Ожидание: "Просто добавь nodeLinker: node-modules"
Реальность: А build-tools? А python3 для компиляции? Сюрприз, они тоже нужны.
Ожидание: "PnP — это будущее"
Реальность: Будущее, где треть пакетов не запускается, а ты объясняешь PM почему "легкий фикс" занял 4 часа.

💀 Классический Dockerfile (не делай так)
dockerfileFROM node:20-alpine

WORKDIR /app
COPY . .

RUN yarn install
# Стоп. А где corepack?
# А build-tools для нативных модулей?
# Вопросы, вопросы...

RUN yarn build

Что не так:

Alpine использует musl, не glibc — половина нативных модулей сломается
Node.js 20 включает только npm — Yarn нужно активировать через corepack
Нет build-tools для sharp, bcrypt и компании
Копируешь всё сразу → кэш Docker не работает → каждый билд по 15 минут

🐕 Тест на умность собаки #2
Собака учится на ошибках с первого раза. Ты будешь гуглить "yarn alpine docker" три часа, найдешь решение для yarn 1 со stackoverflow 2019 года, и удивишься почему не работает.
Рабочий вариант (проверено болью)
dockerfileFROM node:20-alpine AS builder

# Активируем Yarn 4 через corepack
RUN corepack enable && corepack prepare yarn@4.1.0 --activate

# Ставим зависимости для нативных модулей
RUN apk add --no-cache \
python3 \
g++ \
make \
git
# Да, все это нужно для "легковесного" образа
# Welcome to reality

WORKDIR /app

# Копируем СНАЧАЛА только файлы зависимостей (для кэша)
COPY package.json yarn.lock .yarnrc.yml ./
COPY .yarn/releases ./.yarn/releases
# ☝️ .yarn/releases критически важен — там лежит сам Yarn 4
# Это .cjs файл на 2MB, который ДОЛЖЕН быть в репе

RUN yarn install --immutable --inline-builds

# Теперь копируем остальное
COPY . .
RUN yarn build

# Multi-stage: берем только собранное, без node_modules
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
# 50MB вместо 1.1GB


🐕 Тест на умность собаки #3

Собака понимает multi-stage builds интуитивно: поел на кухне — зачем тащить миску в будку? А ты тащишь в прод 800MB node_modules "на всякий случай".

🎯 Топ проблем (которые точно встретишь)

#1: Забыл про .yarnrc.yml
yamlnodeLinker: node-modules
enableGlobalCache: false

Без этого получишь PnP, который работает локально, но падает в CI с загадочной ошибкой.
#2: Не скопировал .yarn/releases
Yarn 4 хранит себя в репе как .cjs бандл в .yarn/releases/yarn-4.x.x.cjs. Забыл скопировать? Получи ошибку "Cannot find module" и час удивления.
#3: Игнорируешь --immutable
Кто-то изменит lock-файл в CI, и билды станут недетерминированными. Удачи с дебагом.
#4: Копируешь все файлы сразу
При изменении одного компонента пересобирается весь node_modules. Docker cache работает только если правильно расположить COPY.
#5: Забыл про нативные модули
Sharp, bcrypt, @swc/core требуют python3 и g++. Без них — крэш с "Error: Cannot find module".
#6: На Alpine нужен musl, а не glibc
Некоторые пакеты (например @swc/core) требуют явной настройки через supportedArchitectures в .yarnrc.yml для работы с musl.

📊 Размеры образов (для тех, кто любит метрики)

FROM node:20 — 1.1GB
└─ "Работает же!" (но жрет место)
FROM node:20-alpine без multi-stage — 400MB
└─ Лучше, но можно еще
Multi-stage на nginx:alpine — 50MB
└─ Node.js в проде не нужен, зачем его тащить?


P.S.
Если сейчас у тебя в проде nodeLinker: pnp на Alpine — есть шанс, что все упадет в 3 ночи после обновления зависимости. Проверено.

Специальный дайждест для либеральных граждан