Какой командой можно изменить пользователя в Dockerfile?

Ответ

Для смены пользователя в Dockerfile используется инструкция USER. Это критически важно для безопасности, чтобы контейнер не работал от root без необходимости.

Базовый пример:

FROM alpine:3.18
RUN addgroup -g 1000 appgroup && 
    adduser -u 1000 -G appgroup -D appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["node", "index.js"]

Практические сценарии:

  1. Снижение привилегий после установки пакетов:

    FROM ubuntu:22.04
    RUN apt-get update && apt-get install -y python3 
    && rm -rf /var/lib/apt/lists/*
    RUN useradd -m -u 1001 apprunner
    USER apprunner
  2. Использование UID вместо имени (рекомендуется для Kubernetes):

    USER 1001
  3. Многоступенчатая сборка со сменой пользователя:

    
    FROM golang:1.21 AS builder
    WORKDIR /build
    COPY . .
    RUN CGO_ENABLED=0 go build -o app .

FROM scratch COPY --from=builder /build/app /app USER 1000:1000 ENTRYPOINT ["/app"]



**Важные моменты:**
- Пользователь должен существовать (создан через `RUN useradd/adduser`)
- `USER` влияет на все последующие `RUN`, `CMD`, `ENTRYPOINT`
- В Kubernetes SecurityContext может переопределять пользователя контейнера
- Для доступа к привилегированным портам (<1024) всё равно потребуется root или `NET_BIND_SERVICE` capability

Ответ 18+ 🔞

Э, слушай, вот история про безопасность в докере, которая многих накрывает медным тазом. Все лепят образы от рута, а потом охуевают, когда какой-нибудь хитрожопый эксплойт выгребает из контейнера всё, включая соседские сервисы. Так делать — это пиздец какой риск, доверия ебать ноль.

Короче, чтобы не быть распиздяем, в Dockerfile есть инструкция USER. Её задача — переключить того, кто внутри контейнера команды выполняет, с всесильного root на какого-нибудь смертного юзера. Без этого твой контейнер — это как дом с открытой дверью и табличкой «заходите, всё ваше».

Самый простой пример, с чего начать:

FROM alpine:3.18
RUN addgroup -g 1000 appgroup && 
    adduser -u 1000 -G appgroup -D appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["node", "index.js"]

Видишь? Сначала создали группу и юзера, скопировали файлы, а потом уже сказали USER appuser. Всё, что после — CMD, ENTRYINT — будет крутиться от его имени. Уже не так страшно.

А вот как это в жизни применяется, смотри:

  1. Сделал грязную работу — смени перчатки. Установил пакеты от рута, а потом переключился на простого работягу.

    FROM ubuntu:22.04
    RUN apt-get update && apt-get install -y python3 
        && rm -rf /var/lib/apt/lists/* # Почистил за собой, молодец
    RUN useradd -m -u 1001 apprunner
    USER apprunner # И пошёл работать под непривилегированным юзером

    Логика простая: всё, что требует прав — делаем в начале, а потом опускаемся до уровня обычного пользователя. Красота.

  2. Для кубера — только цифры. Там своя банда, свои понятия. Им часто похуй на имена, они работают с UID напрямую. Поэтому просто:

    USER 1001

    Коротко и ясно. Главное, чтобы этот UID (и GID) в итоговом образе реально существовали, а то будет волнение ебать при старте.

  3. Многоступенчатая сборка — высший пилотаж. Собираем в одном образе, а живём в другом, максимально стерильном.

    FROM golang:1.21 AS builder
    WORKDIR /build
    COPY . .
    RUN CGO_ENABLED=0 go build -o app .
    
    FROM scratch # Вообще пустой образ, ёпта!
    COPY --from=builder /build/app /app
    USER 1000:1000 # И сразу говорим, под кем работать
    ENTRYPOINT ["/app"]

    Вот это подход! Из scratch даже рута нет, только твой бинарь и указанный UID/GID. Безопасность почти абсолютная.

Важные моменты, которые в голове держать надо:

  • Сначала создай, потом используй. Нельзя написать USER вася, если этого Васи в образе нет. Сначала RUN useradd..., потом уже USER. Иначе будет тебе хиросима на старте.
  • USER — это надолго. Всё, что идёт после этой инструкции (RUN, CMD, ENTRYPOINT), будет выполняться от указанного пользователя. Это навсегда (пока следующий USER не перебил).
  • Кубер может всё переопределить. Не обольщайся. Если в поде в SecurityContext прописан runAsUser: 0, то твой красивый USER 1001 нахуй не сдался. Кубер его перетрет. Но лучше, чтобы в образе было не от рута по умолчанию.
  • Порты ниже 1024 — привилегия рута. Если твоему сервису надо висеть на 80-м порту, просто так под обычным юзером не выйдет. Варианты: слушать на высоком порту (типа 8080), использовать NET_BIND_SERVICE capability (это уже тонкости), или всё-таки стартовать от рута (но это крайний случай, и тогда терпения ноль ебать к его безопасности).

Короче, суть в чём: не будь пидарасом шерстяным, не ленись прописывать USER. Это не просто «хорошая практика», это базовый способ не наебаться с безопасностью. Делай с умом.