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

Част 3: Docker Images

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

Ако container-ът е runtime обектът, Docker Image е неговата основа. Не го разглеждам като „един файл, който Docker тегли и стартира“. Image е неизменяем набор от layers и metadata, от който Docker създава container.

1. Какво всъщност е Docker Image

Docker Image е неизменяем шаблон, от който се създават containers. Той съдържа filesystem съдържанието, необходимо за приложението, както и metadata, описваща как трябва да бъде стартирано то.

Това е първото разграничение, което държа да е ясно: image не е container. Един image може да бъде използван за създаването на много различни containers.

Dockerfile
описание
docker build
изграждане
Docker Image
готов шаблон
Container 1
Container 2
Container 3

2. Layers – истинската структура на Image

Docker Image не е един голям монолитен блок. Той е изграден от layers. Всеки layer представлява промяна спрямо предишното състояние на filesystem-а.

Dockerfile
FROM debian:bookworm

RUN apt-get update && apt-get install -y nginx

COPY nginx.conf /etc/nginx/nginx.conf

CMD ["nginx", "-g", "daemon off;"]

Тук FROM задава базовия image, а filesystem промените от build инструкциите формират слоевете на image-а. CMD описва default command и не трябва да се бърка с filesystem layer.

Container writable layer
runtime промени
Application layer
COPY / ADD
Package layer
RUN ...
Base image layers
FROM
Важно: не използвам writable layer на container като място за постоянни данни. За persistent data използвам volumes или bind mounts.

3. Dockerfile – рецептата за Image

Dockerfile е текстов файл, в който описвам стъпките за изграждане на image. Това е build инструкцията, която превръща source кода и зависимостите в повторяем артефакт.

Dockerfile
FROM nginx:alpine

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

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Изграждането е:

TERMINAL
docker build -t my-nginx:1.0 .

Точката в края определя build context – директорията, чието съдържание е достъпно за build процеса.

Практически момент: ако build context-ът е огромен, build процесът може да обработва много повече данни, отколкото реално са необходими. Затова .dockerignore не е дреболия.

4. Основни Dockerfile инструкции

ИнструкцияПредназначениеПример
FROMЗадава базовия image.FROM alpine:3.22
RUNИзпълнява команда по време на build.RUN apk add --no-cache curl
COPYКопира файлове от context-а.COPY app /app
WORKDIRЗадава работната директория.WORKDIR /app
ENVЗадава environment variable.ENV APP_ENV=production
EXPOSEДокументира използван порт.EXPOSE 8080
ENTRYPOINTОпределя основната команда.ENTRYPOINT ["./app"]
CMDЗадава default command/arguments.CMD ["--config", "/etc/app.conf"]

5. Build Cache – мястото, където Docker печели време

Build cache е една от най-практичните причини да обръщам внимание на реда на инструкциите в Dockerfile.

НЕОПТИМАЛНО
FROM node:22-alpine
COPY . .
RUN npm install
RUN npm run build

Ако променя един малък source файл, COPY . . може да направи cache слоя невалиден и да принуди dependency installation да се изпълни отново.

ПО-ДОБРА СТРУКТУРА
FROM node:22-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

RUN npm run build
Моят принцип: най-рядко променящите се инструкции поставям по-рано, а най-често променящите се – по-късно, когато това е логично за build процеса.

6. .dockerignore – малък файл с голям ефект

.dockerignore определя какво да не влиза в build context. Не искам локалните зависимости, Git историята, логове и временни файлове да се изпращат към build процеса.

.dockerignore
.git
.gitignore
node_modules
vendor
*.log
.env
.env.*
tmp
cache

Това има значение и за сигурността. Не искам случайно да попаднат чувствителни или ненужни файлове в image-а.

7. Размерът на Image има значение

„Работи“ не е достатъчен критерий за добър image. Размерът му влияе върху времето за изтегляне, storage потреблението, cache-а и deployment-а.

ДИАГНОСТИКА
docker image ls

docker system df

docker system df -v

Не гоня минимален размер на всяка цена. По-малък image е полезен, но прекомерното оптимизиране може да направи Dockerfile-а труден за поддръжка.

7.1. Multi-stage build

При multi-stage build компилирам приложението в един stage, а в крайния image копирам само необходимото за runtime.

Dockerfile
FROM golang:1.24 AS builder
WORKDIR /src
COPY . .
RUN go build -o /out/app ./cmd/app

FROM alpine:3.22
COPY --from=builder /out/app /usr/local/bin/app
ENTRYPOINT ["/usr/local/bin/app"]

8. Как проверявам Image

Когато искам да разбера какво реално използвам, не разчитам само на името и tag-а.

ОСНОВНИ ПРОВЕРКИ
docker image ls

docker image inspect nginx:alpine

docker image history nginx:alpine

docker image inspect дава подробна metadata, а docker image history е особено полезна, когато търся откъде идва размерът на image-а.

9. Tags, Image ID и Digest

Tag-ът е удобен за човека, но не трябва да го приемам като вечна идентичност на image.

ПРИМЕР
nginx:alpine
nginx:1.29
nginx:latest

Tag може да бъде преместен към друг image. При deployment, където е нужна точно определена версия, digest-ът дава по-прецизна идентификация.

Практически съвет: в production не разчитам сляпо на latest. Искам да знам точно какъв артефакт ще бъде стартиран.

10. Registry – откъде идват Images

Image може да бъде локално изграден, но в реална инфраструктура често идва от registry. Registry е хранилище, от което images могат да бъдат push-вани и pull-вани.

Dockerfile
docker build
Registry
Server A
docker pull
Server B
docker pull
Server C
docker pull
ПРИМЕР
docker login registry.example.com
docker tag myapp:1.0 registry.example.com/myapp:1.0
docker push registry.example.com/myapp:1.0

11. Какво се случва при docker pull

При docker pull nginx:alpine Docker получава metadata и необходимите layers от registry. Ако даден layer вече съществува локално, той може да бъде използван повторно.

TERMINAL
docker pull nginx:alpine

Точно затова layer моделът е важен: общите layers могат да бъдат използвани от множество images.

12. BuildKit и съвременният build процес

Съвременният Docker build процес използва BuildKit. Той добавя възможности за по-ефективно изграждане, cache, parallelism и multi-platform builds.

MULTI-PLATFORM BUILD
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t myapp:1.0 \
  --push .

Не приемам автоматично, че image, изграден на една архитектура, е подходящ за всеки друг host.

13. Security започва още при Image

Ако image съдържа ненужни пакети, инструменти и права, проблемът вече съществува още преди container да бъде стартиран.

  • избирам поддържан base image;
  • ограничавам ненужните пакети;
  • не записвам secrets в Dockerfile;
  • проверявам дали приложението трябва да работи като root;
  • поддържам dependencies актуални.
Никога не слагам secrets в Dockerfile. Password, API key или private key не трябва да бъдат записвани като обикновен текст в build context-а или в инструкции, които могат да останат в историята на image-а.

14. Когато Images започнат да пълнят диска

Docker натрупва images, stopped containers, volumes и build cache. При host, който се използва дълго време, това може да се превърне в реален проблем.

ДИАГНОСТИКА
docker system df

docker image ls

docker ps -a
Моето правило: диагностика преди cleanup. Особено при production host не приемам prune като универсално решение.

15. Практики, които използвам при Docker Images

  1. Избирам подходящ и поддържан base image.
  2. Използвам .dockerignore.
  3. Подреждам Dockerfile така, че cache-ът да се използва ефективно.
  4. Използвам multi-stage build, когато приложението го позволява.
  5. Не записвам secrets в image.
  6. Не разчитам сляпо на latest за production deployment.
  7. Проверявам размера и layer историята.
  8. Поддържам base images и dependencies актуални.
  9. Разделям build средата от runtime средата.

16. Моето мислене при диагностика на Image

Когато image стане прекалено голям, build-ът стане бавен или container-ът не стартира както очаквам, не започвам да променям произволни инструкции.

1. Какъв е base image?
2. Какви layers са създадени?
3. Къде се губи cache?
4. Какво влиза от build context?
5. Какво реално трябва да има в runtime image?

Този подход е по-надежден от „пробвам още една команда и гледам дали ще стане“. Image е артефакт от build процес. Ако разбера как е построен, много по-лесно разбирам и поведението му.

17. Най-важното от тази част

  • Docker Image е неизменяем шаблон, от който се създават containers.
  • Image е изграден от layers.
  • Dockerfile описва как се изгражда image.
  • Build cache ускорява повторните builds.
  • .dockerignore контролира build context.
  • Multi-stage builds отделят build средата от runtime.
  • Tags са имена, а не гаранция за неизменна идентичност.
  • Registry разпространява images между системи.
  • Размерът на image-а има значение за storage и deployment.
  • Добрата image архитектура е част от security модела.

Заключение

Container-ът е runtime инстанцията. Image е артефактът, от който тя се създава. Ако image-ът е лошо структуриран, прекалено голям, неясен или съдържа ненужни зависимости, проблемът няма да изчезне, когато го стартирам.

За мен добрият Docker Image не е просто image, който работи. Той трябва да бъде предвидим, повторяем, поддържаем и достатъчно малък, без да жертвам надеждността заради няколко мегабайта.

В следващата част ще премина към Docker Containers – как се създават от image, какво се случва с writable layer-а, как се управлява жизненият им цикъл и защо docker run, docker create, docker start и docker exec не са едно и също нещо.