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 в Image | Image-ът може да бъде копиран, публикуван или сканиран. |
| Работя като 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-вам и как ги прехвърлям между машини.