Docker и Docker Compose — професионално ръководство

Част 8: Dockerfile — създаване на собствени Images

В предишните части използвах готови Docker Images. Тук преминавам към следващото ниво — как сам създавам Image, как Docker изпълнява Dockerfile, как изграждам предвидими Images и как избягвам типичните грешки, които правят един Image излишно голям, бавен и труден за поддръжка.

Dockerfile е рецептата за Docker Image. Не е shell script, който просто се изпълнява ред по ред върху host системата. Всяка инструкция описва как да бъде изграден нов layer от Image-а и именно това стои в основата на възпроизводимия build процес.

1. Защо изобщо да създавам собствен Image?

Готовите Images от Docker Hub са чудесни за много задачи. Ако ми трябва Nginx, MariaDB или Redis, няма смисъл да преоткривам колелото. Но в реална система почти винаги идва моментът, в който трябва да добавя собствен код, configuration, dependencies или специфични настройки.

Тогава вече не искам да променям ръчно работещ container. Искам да мога да кажа: ето source кода, ето Dockerfile-а — изгради ми абсолютно същия Image отново.

  • получавам възпроизводим build;
  • версионирам Dockerfile заедно с проекта;
  • мога да изграждам Image в CI/CD;
  • мога да тествам Image-а преди deployment;
  • мога да върна стара версия, ако новата създава проблем.

2. Какво представлява Dockerfile?

Dockerfile е обикновен текстов файл с име Dockerfile, без задължително файлово разширение. В него описвам базовия Image, файловете, които ще добавя, командите за изпълнение при build и поведението на container-а при стартиране.

Dockerfile — минимален пример
FROM nginx:alpine

COPY ./site /usr/share/nginx/html

EXPOSE 80

Този Dockerfile започва от nginx:alpine, копира локалната директория site в директорията на Nginx и документира, че приложението използва порт 80.

Dockerfile

рецепта

docker build

build процес

Image

готов артефакт

Container

работещ процес

3. FROM — откъде започва Image-ът

Почти всеки Dockerfile започва с FROM. Тази инструкция определя базовия Image, върху който ще изграждам моя Image.

Примери
FROM ubuntu:24.04

FROM debian:bookworm-slim

FROM python:3.13-slim

FROM nginx:alpine

Изборът на base Image не е дреболия. Той влияе върху размера, наличните инструменти, security surface и начина, по който ще поддържам системата.

Не използвам сляпо :latest. Tag-ът latest не означава „най-добрата и най-сигурната версия“. При следващ build съдържанието му може да бъде различно. За предвидими builds използвам конкретен tag и при по-строги среди обмислям pin-ване към digest.

4. RUN — инсталиране и подготовка

RUN изпълнява команда по време на build процеса. Това е мястото, където инсталирам packages, създавам директории или подготвям приложението.

Debian / Ubuntu пример
FROM debian:bookworm-slim

RUN apt-get update \\
    && apt-get install -y --no-install-recommends curl ca-certificates \\
    && rm -rf /var/lib/apt/lists/*

Почиствам временните package lists в същия layer, в който ги създавам. Така не оставям ненужни данни в Image-а.

5. COPY — добавяне на файлове

COPY копира файлове от build context-а в Image-а. Това е основният механизъм, с който добавям source code, configuration и други необходими файлове.

Пример
FROM nginx:alpine

COPY ./html/ /usr/share/nginx/html/

Тук съдържанието на локалната директория html става част от Image-а. При стартиране на container-а Nginx вече разполага с тези файлове.

6. ADD — защо обикновено предпочитам COPY

Docker има и инструкция ADD. Тя може да прави повече неща от COPY, включително автоматично разопаковане на някои локални архиви. Именно затова предпочитам COPY, когато просто трябва да копирам файлове.

Практическо правило: използвам COPY по подразбиране. Използвам ADD само когато конкретна негова функционалност действително ми е необходима.

7. WORKDIR — работната директория

WORKDIR определя текущата директория за следващите инструкции като RUN, CMD, ENTRYPOINT и COPY.

Node.js пример
FROM node:22-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

Освен по-чиста структура, това помага и за доброто използване на build cache-а.

8. Docker build context

Когато изпълня:

Build
docker build -t myapp:1.0 .

последната точка означава текущата директория и тя е build context. Docker може да използва файлове от този context, но не и произволни файлове извън него.

Тук много хора си правят беля. Ако build-ваш от огромна директория с ISO файлове, backups, .git, логове и други ненужни файлове, build context-ът става излишно голям. Затова използвам .dockerignore.

9. .dockerignore

Файлът .dockerignore определя какво да не влиза в build context-а.

.dockerignore
.git
.gitignore
node_modules
__pycache__
*.log
.env
.env.*
README.md

Това има значение не само за скоростта. Ако случайно попаднат secrets в build context-а и бъдат копирани в Image-а, вече имам security проблем.

10. CMD и ENTRYPOINT

Тук описвам поведението при стартиране на container-а, а не при build.

CMD
FROM python:3.13-slim

WORKDIR /app
COPY . .

CMD ["python", "app.py"]

CMD задава default command. Тя може да бъде заменена при стартиране на container-а.

ENTRYPOINT
ENTRYPOINT ["python", "app.py"]

ENTRYPOINT е подходяща, когато Image-ът има ясно определено основно приложение и искам аргументите да се подават към него.

11. EXPOSE — какво означава всъщност?

Dockerfile
EXPOSE 8080

EXPOSE не публикува порта към host системата. Той документира кой порт използва приложението. Реалното публикуване става при стартиране:

docker run
docker run -d -p 8080:8080 myapp:1.0

12. ENV и ARG

ARG се използва по време на build. ENV задава environment variable, която съществува и при изпълнение на container-а.

Dockerfile
ARG APP_VERSION=1.0
ENV APP_ENV=production

RUN echo "Building version ${APP_VERSION}"
Не слагам passwords и API keys в ARG или ENV, за да ги „скрия“. Secrets не трябва да се вграждат в Docker Image. Те трябва да се подават по подходящ механизъм при deployment/build.

13. Layer-и и Docker build cache

Dockerfile създава поредица от layers. Docker използва build cache, за да не изпълнява отново стъпки, които не са се променили.

Добър модел
FROM node:22-alpine
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["node", "server.js"]

Ако променя само server.js, Docker може да използва cache за npm ci. Това е малка промяна в структурата, която при големи проекти може да спести много време.

14. Multi-stage builds

При много приложения ми трябва един Image за build, но не ми трябват всички build инструменти в production container-а.

Multi-stage пример
FROM node:22-alpine AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

EXPOSE 80

Първият stage съдържа Node.js и dependencies за build. Вторият съдържа само Nginx и готовия резултат. Така production Image-ът може да бъде значително по-малък и с по-малко ненужни компоненти.

15. USER — не изпълнявам всичко като root

Ако приложението позволява, предпочитам процесът в container-а да не работи като root.

Пример
FROM python:3.13-slim

RUN useradd --create-home --uid 10001 appuser
WORKDIR /app
COPY --chown=appuser:appuser . .

USER appuser

CMD ["python", "app.py"]

Това не е универсално правило за всеки Image, но когато приложението може да работи като непривилегирован user, това е предпочитаният вариант.

16. HEALTHCHECK

Процесът може да съществува, а приложението вътре да е неизползваемо. HEALTHCHECK позволява да опиша проверка за health състоянието.

Пример
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \\
    CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/ || exit 1

17. Как изграждам Image-а

Build
docker build -t myapp:1.0 .

След това проверявам получения Image:

Проверка
docker image ls

docker image inspect myapp:1.0

И го стартирам:

Run
docker run -d --name myapp -p 8080:8080 myapp:1.0

18. Dockerfile + Docker Compose

Dockerfile и Compose не са конкуриращи се технологии. Dockerfile описва как се изгражда Image, а Compose описва как се стартират services и как са свързани помежду си.

compose.yaml
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"

  db:
    image: mariadb:11
    environment:
      MARIADB_DATABASE: app
      MARIADB_USER: app
      MARIADB_PASSWORD: change-me
      MARIADB_ROOT_PASSWORD: change-me-too

Тук web се изгражда от моя Dockerfile, докато database service използва готов Image. Именно така изглеждат много реални Compose stacks.

19. Практически правила, които следвам

ПравилоЗащо
Използвам малък и подходящ base ImageПо-малък размер и по-малка attack surface.
Избягвам :latest при важни buildsПолучавам предвидими версии.
Поддържам .dockerignoreНе изпращам ненужни файлове и secrets в context-а.
Подреждам инструкциите според cache-аУскорявам повторните builds.
Използвам multi-stage buildsОтделям build средата от runtime средата.
Не вграждам secrets в ImageImage-ът може да бъде копиран, публикуван или сканиран.
Работя като non-root, когато е възможноНамалявам привилегиите на приложението.

20. Най-честите грешки

  • един огромен Dockerfile с десетки ненужни инструменти;
  • използване на latest навсякъде;
  • копиране на целия проект без .dockerignore;
  • вграждане на passwords, tokens и API keys в Image-а;
  • стартиране на всичко като root без причина;
  • смесване на build dependencies с production runtime;
  • използване на shell команди без разбиране как работят layer-ите и cache-ът.
Моят принцип: Dockerfile трябва да бъде скучен, предвидим и възпроизводим. Не търся най-късия Dockerfile, а такъв, който след шест месеца мога да отворя и веднага да разбера защо е написан точно така.

21. Заключение

С Docker Compose вече мога да описвам цял stack. С Dockerfile правя следващата крачка — описвам самия Image, от който този stack ще работи.

Това е фундаменталната разлика между „пускам някакъв container“ и изграждам контролирана container среда. Когато Dockerfile, Image и Compose конфигурацията са правилно подредени, deployment-ът престава да зависи от това какво съм правил ръчно на конкретния server.

В следващата част ще мина към Docker Registry и управлението на Images — къде ги съхранявам, как ги version-вам и как ги прехвърлям между машини.