30 վայրկյանում
Docker-ը քո հավելվածը, դրա runtime-ը և գրադարանները փաթեթավորում է image-ի մեջ՝ կնքված, տարբերակավորված արկղի։ Այդ image-ը գործարկելով՝ ստանում ես container՝ մեկուսացված պրոցես, որը նույն կերպ է իրեն պահում քո լափթոփում, CI-ում և սերվերում։ Իսկ Docker Compose-ը նկարագրում է ամբողջ համակարգը (հավելված, տվյալների բազա, քեշ, worker-ներ, վեբ սերվեր) մեկ ֆայլում, որը կարող ես գործարկել մեկ հրամանով։
Ինչու եմ օգտագործում
- Օգտագործում եմ, որովհետև «իմ մեքենայի վրա աշխատում է»-ն այլևս արդարացում չէ․ սերվերը գործարկում է հենց այն image-ը, որը CI-ը կառուցել և թեստավորել է։
- Compose-ով ամբողջ համակարգը նկարագրում եմ մեկ ընթեռնելի ֆայլում, և նոր ծրագրավորողը րոպեների ընթացքում գործարկում է հավելվածը, բազան, Redis-ը և worker-ները։
- Օգտագործում եմ, որովհետև թարմացումները դառնում են ձանձրալի․ փոխում ես image-ի մեկ tag, նորից build անում, իսկ եթե ինչ-որ բան կոտրվի՝ հետ ես գլորում նախորդ tag-ին։
- Այն սերվերները մաքուր է պահում։ Տարբեր հավելվածներին կարող են պետք լինել PHP-ի կամ Node-ի տարբեր տարբերակներ՝ առանց վիճելու, թե ինչ է տեղադրված host-ի վրա։
Ինչպես է աշխատում
01Image-ներ, container-ներ և ինչով են դրանք տարբերվում VM-ներից
Image-ը միայն կարդալու համար նախատեսված կաղապար է՝ ֆայլային համակարգի snapshot՝ գումարած մետատվյալներ, օրինակ՝ որ հրամանը գործարկել։ Container-ը image-ի գործարկված օրինակն է, որի վրա կա գրելու համար բարակ շերտ։ Image-ը պատկերացրու որպես բաղադրատոմսի քարտ, իսկ container-ը՝ դրանով պատրաստված ուտեստ․ մեկ քարտով կարող ես շատ ուտեստներ պատրաստել։ Վիրտուալ մեքենան (VM) էմուլյացիա է անում ամբողջ համակարգիչը՝ իր սեփական kernel-ով, ուստի բեռնում է ամբողջական օպերացիոն համակարգ և սովորաբար զբաղեցնում է գիգաբայթեր ու տասնյակ վայրկյաններ։ Container-ը սովորական Linux պրոցես է, որը kernel-ը մեկուսացնում է namespace-ներով (ինչ կարող է տեսնել՝ ֆայլեր, ցանց, պրոցեսների ցուցակ) և սահմանափակում cgroup-ներով (որքան CPU և հիշողություն կարող է օգտագործել)։ Այն կիսում է host-ի kernel-ը, ուստի մեկնարկում է միլիվայրկյաններում և քիչ overhead է ավելացնում։ Փոխզիջումը՝ մեկուսացումն ավելի թույլ է, քան VM-ինը, և այդ պատճառով cloud-երը հաճախ container-ները գործարկում են VM-ների ներսում։
- Image = անփոփոխ կաղապար, որը նույնականացվում է name:tag-ով և բովանդակության digest-ով։
- Container = image + գրելու շերտ + մեկուսացված պրոցես։
- Container-ները կիսում են host-ի kernel-ը, իսկ VM-ներն ունեն իրենցը։
02Շերտեր և build-ի քեշավորում
Dockerfile-ի յուրաքանչյուր հրահանգ (FROM, COPY, RUN) ստեղծում է շերտ, իսկ image-ը շերտերի կույտ է, որոնք միավորվում են overlay ֆայլային համակարգով։ Շերտերը քեշավորվում են ըստ բովանդակության․ եթե հրահանգը և դրանից առաջ եղած ամեն ինչ չեն փոխվել, Docker-ը վերօգտագործում է քեշավորված շերտը՝ այն նորից գործարկելու փոխարեն։ Հենց որ մեկ շերտ փոխվում է, դրանից հետո եկող բոլոր շերտերը վերակառուցվում են։ Հենց այս կանոնն է որոշում, թե որքան արագ են քո build-երը։ Սկզբում պատճենիր կախվածությունների manifest-ը (package.json և lockfile-ը, կամ composer.json և composer.lock), տեղադրիր կախվածությունները, և միայն դրանից հետո պատճենիր մնացած կոդը։ Այժմ կոմպոնենտը խմբագրելը ամբողջական npm install չի առաջացնում։ .dockerignore ֆայլը node_modules-ը, .git-ը, լոգերը և .env ֆայլերը դուրս է պահում build context-ից, ինչը build-երն արագացնում է և թույլ չի տալիս գաղտնիքներին արտահոսել image-ների մեջ։
# .dockerignore
node_modules
.next
.git
*.log
.env*
Dockerfile
compose*.yaml
03Production Dockerfile՝ multi-stage, փոքր, ոչ root
Multi-stage build-ը մեկ stage-ում՝ կոմպիլյատորներով և dev կախվածություններով, կառուցում է հավելվածը, ապա միայն արդյունքը պատճենում է մաքուր, փոքր runtime stage-ի մեջ։ Վերջնական image-ում չկան ավելորդ source map-եր, build գործիքներ, և patch անելու փաթեթները շատ ավելի քիչ են։ Օգտագործիր slim կամ Alpine բազային image-ներ և ամրագրիր major տարբերակը՝ latest-ի փոխարեն։ Գործարկիր առանց արտոնությունների օգտատիրոջով․ եթե ինչ-որ մեկը շահագործի հավելվածի խոցելիությունը, նա կհայտնվի որպես սովորական օգտատեր, ոչ թե root։ Ստորև օրինակը կառուցում է Next.js հավելված, որի next.config-ում դրված է output: "standalone", ինչը ստեղծում է մինիմալ server.js և միայն անհրաժեշտ node_modules-ը։ Նույն մոտեցումն աշխատում է Laravel-ի համար՝ Composer stage, Node stage՝ asset-ների համար, և վերջնական PHP-FPM stage։
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:24-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:24-alpine AS run
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build --chown=node:node /app/.next/standalone ./
COPY --from=build --chown=node:node /app/.next/static ./.next/static
COPY --from=build --chown=node:node /app/public ./public
USER node
CMD ["node", "server.js"]
04Volume-ներ և ցանցեր
Container-ի գրելու շերտն անհետանում է, երբ container-ը ջնջվում է, ուստի այն ամենը, ինչ ուզում ես պահել (բազայի ֆայլեր, վերբեռնումներ), պետք է ապրի volume-ում։ Անվանված volume-ները կառավարում է Docker-ը, և դրանք ճիշտ ընտրություն են տվյալների բազաների համար։ Bind mount-ները host-ի թղթապանակը քարտեզագրում են container-ի մեջ և իդեալական են development-ում, երբ ուզում ես, որ կոդի փոփոխություններն անմիջապես երևան։ Ցանցի կողմից՝ նույն user-defined ցանցում գտնվող container-ները միմյանց գտնում են service-ի անունով՝ Docker-ի ներկառուցված DNS-ի միջոցով․ հավելվածը միանում է db:5432-ին, ոչ թե localhost-ին։ Container-ի ներսում localhost-ը նշանակում է հենց այդ container-ը, և սա սկսնակների ամենատարածված շփոթությունն է։ Publish արա միայն այն port-երը, որոնք իսկապես պետք են արտաքին աշխարհին, սովորաբար միայն 80-ը և 443-ը reverse proxy-ի վրա։
- Անվանված volume՝ մշտական տվյալներ, որոնք պատկանում են Docker-ին (տվյալների բազաներ)։
- Bind mount՝ host-ի թղթապանակ, հիանալի է development-ում կենդանի կոդի համար։
- Publish արված port-երը շատ կոնֆիգուրացիաներում շրջանցում են ufw-ի կանոնները․ մասնավոր port-երը կապիր 127.0.0.1-ին կամ ընդհանրապես մի publish արա։
05Docker Compose ամբողջական հավելվածի համար
Compose-ը մեկ compose.yaml-ում նկարագրում է մի քանի container, դրանց ցանցերն ու volume-ները, իսկ docker compose up -d-ն գործարկում է ամեն ինչ։ Ստորև տիպիկ վեբ հավելված է՝ nginx-ը առջևում՝ որպես միակ հանրային մուտքի կետ, հավելվածի container-ը, queue worker, որը կառուցված է նույն image-ից, բայց այլ հրամանով, PostgreSQL՝ մշտական volume-ով և health check-ով, և Redis՝ քեշի ու հերթերի համար։ depends_on-ը condition: service_healthy-ով ստիպում է հավելվածին սպասել, մինչև բազան իրականում սկսի միացումներ ընդունել, ոչ թե պարզապես մինչև դրա container-ը մեկնարկի։ Ուշադրություն դարձրու, որ բազան և Redis-ը ընդհանրապես port չեն publish անում․ դրսից հասանելի է միայն nginx-ը։ Գաղտնիքները գալիս են env ֆայլից, որը Git-ում commit չի արվում։
services:
nginx:
image: nginx:stable-alpine
ports: ["80:80", "443:443"]
depends_on: [app]
app:
build: .
env_file: .env
depends_on:
db: { condition: service_healthy }
worker:
build: .
command: php artisan queue:work --tries=3
db:
image: postgres:17
volumes: [db-data:/var/lib/postgresql/data]
healthcheck: { test: ["CMD", "pg_isready", "-U", "postgres"], interval: 5s }
redis:
image: redis:7-alpine
volumes: { db-data: {} }
06Development-ի և production-ի Compose
Development-ը և production-ը տարբեր բաներ են ուզում։ Development-ում ուզում ես bind mount արված կոդ, hot reload, debug գործիքներ և գուցե mail catcher։ Production-ում ուզում ես կառուցված image-ներ՝ ներսում «թխված» կոդով, restart: unless-stopped, ռեսուրսների սահմանափակումներ, լոգերի ռոտացիա, TLS և մոնիթորինգ։ Մի պահիր երկու իրարից շեղվող ֆայլ։ Պահիր բազային compose.yaml՝ ընդհանուրով, և դրա վրա դիր override ֆայլեր․ compose.override.yaml-ը development-ում բեռնվում է ավտոմատ, իսկ production-ը գործարկվում է բացահայտ՝ բազային ֆայլով և production ֆայլով։ Կարող ես ավելացնել նաև այլ շերտեր, օրինակ՝ TLS ֆայլ կամ observability ֆայլ, որն ավելացնում է Prometheus, Grafana և Loki։ Deploy անելուց առաջ միավորված արդյունքը ստուգիր docker compose config-ով։
# development: compose.yaml + compose.override.yaml (auto-loaded)
docker compose up -d
# production: explicit files, no override
docker compose -f compose.yaml -f compose.prod.yaml config --quiet
docker compose -f compose.yaml -f compose.prod.yaml up -d --build
# useful day to day
docker compose ps
docker compose logs -f --tail=100 app
docker compose exec app sh
Օգտագործեք, երբ
- Ցանկացած հավելված, որն ունի մեկից ավելի շարժվող մաս (հավելված, բազա, քեշ, worker-ներ), և որը նոր ծրագրավորողները պետք է արագ գործարկեն։
- Երբ սերվերները պետք է կողք կողքի գործարկեն runtime-ի տարբեր տարբերակներ՝ առանց կոնֆլիկտների։
- Երբ ուզում ես, որ CI-ը կառուցի մեկ artifact, որն անփոփոխ անցնում է թեստավորումից production։
- Որպես առաջին քայլ ցանկացած orchestrator-ից առաջ․ Kubernetes-ը, ECS-ը և Cloud Run-ը բոլորը գործարկում են container image-ներ։
Մի՛ օգտագործեք, երբ
- Մեկ ստատիկ կայք կամ փոքր PHP կայք shared hosting-ում, որտեղ Docker-ը ավելացնում է շարժվող մասեր՝ առանց օգուտի։
- Երբ թիմում չկա մեկը, ով պատասխանատու լինի image-ների թարմացումների և անվտանգության patch-երի համար․ հնացած image-ները լուռ «փտում» են։
- Desktop GUI հավելվածներ կամ workload-ներ, որոնց այլ kernel է պետք․ դրանց համար օգտագործիր VM։
Այլընտրանքներ
- Պարզ VPS կարգավորում
nginx, PHP-FPM կամ Node՝ systemd/pm2-ով, տեղադրված անմիջապես։ Ավելի պարզ է մեկ-երկու հավելվածի համար, ավելի դժվար է վերարտադրել։
- Podman
Առանց daemon-ի, լռելյայն rootless container engine, որը մեծապես համատեղելի է Docker-ի հրամանների և Compose ֆայլերի հետ։
- Վիրտուալ մեքենաներ
Ավելի ուժեղ մեկուսացում և ամբողջական OS յուրաքանչյուր workload-ի համար․ ավելի ծանր են և դանդաղ են մեկնարկում։
- Platform as a service
Heroku-ի ոճի պլատֆորմները քո փոխարեն կառուցում և գործարկում են հավելվածը․ ավելի քիչ վերահսկողություն, ավելի քիչ operations աշխատանք։
Լավագույն պրակտիկաներ
- Օգտագործիր multi-stage build-եր և slim բազային image-ներ․ latest-ի փոխարեն ամրագրիր major տարբերակները։
- Dockerfile-ի քայլերը դասավորիր ամենաքիչ փոփոխվողից դեպի ամենահաճախ փոփոխվողը, որպեսզի քեշն աշխատի քեզ համար։
- Container-ները գործարկիր ոչ root օգտատիրոջով և երբեք գաղտնիքներ մի «թխիր» image-ների մեջ․ փոխանցիր դրանք runtime-ում։
- Դրսից հասանելի դարձրու միայն reverse proxy-ն․ բազաները և Redis-ը պահիր ներքին ցանցում։
- Ավելացրու health check-եր և restart policy-ներ, որպեսզի խափանումները հայտնաբերվեն և վերականգնվեն ավտոմատ։
- Կարգավորիր լոգերի ռոտացիան (լռելյայն json-file driver-ը անվերջ աճում է) և պարբերաբար մաքրիր հին image-ները։
- Image-ները վերակառուցիր ժամանակացույցով, որպեսզի ստանաս բազային image-ի անվտանգության patch-երը։
Տարածված սխալներ
- Բազայի տվյալները պահել container-ի գրելու շերտում և կորցնել դրանք docker compose down -v-ի կամ rebuild-ի ժամանակ։
- Բազայի port-ը publish անել 0.0.0.0-ի վրա և ենթադրել, որ host-ի firewall-ը դեռ արգելափակում է այն։
- Ամբողջ նախագիծը պատճենել կախվածությունները տեղադրելուց առաջ, ինչի պատճառով կոդի յուրաքանչյուր փոփոխություն ամեն ինչ նորից է տեղադրում։
- Container-ի ներսում localhost օգտագործել մեկ այլ container-ին հասնելու համար՝ service-ի անվան փոխարեն։
- Image-ի ներսում .env ֆայլեր կամ .git թղթապանակներ ուղարկել, որովհետև .dockerignore չկար։
Խորը վերլուծություն
07Ինչ է իրականում container-ը․ runtime-ի շղթան և PID 1-ը
Երբ գրում ես docker run, CLI-ը դիմում է Docker daemon-ին (dockerd), որն աշխատանքը փոխանցում է containerd-ին, իսկ սա էլ ցածր մակարդակի OCI runtime-ով, սովորաբար runc-ով, ստեղծում է պրոցեսը։ runc-ը kernel-ից խնդրում է նոր namespace-ներ, պրոցեսը կցում է cgroup-ի, overlayfs-ով mount է անում image-ի շերտերը և հետո հեռանում է ճանապարհից։ Image-ի և runtime-ի ձևաչափերը բաց OCI ստանդարտներ են, այդ պատճառով Podman-ը, Kubernetes-ը և Docker-ը կարող են գործարկել նույն image-ը։ Մի մանրուք production-ում հաճախ է խայթում․ container-ի առաջին պրոցեսը PID 1-ն է։ Kernel-ը PID 1-ին լռելյայն signal handler-ներ չի տալիս, ուստի SIGTERM-ի handler չտեղադրած պրոցեսը պարզապես անտեսում է այն, և PID 1-ից նաև սպասվում է, որ «հավաքի» zombie child պրոցեսները։ Փոքրիկ init-ը, օրինակ՝ tini-ն, որը Compose-ում միացվում է init: true-ով, քո փոխարեն փոխանցում է ազդանշանները և հավաքում zombie-ները։
services:
app:
image: registry.example.com/app:1.8.0
init: true # tini as PID 1: forwards signals, reaps zombies
stop_grace_period: 30s # time between SIGTERM and SIGKILL (default 10s)
stop_signal: SIGTERM
08Container-ի պաշտպանության ամրացում runtime-ում
Non-root user-ը առաջին քայլն է, ոչ թե վերջինը։ Լռելյայն container-ը դեռ ստանում է Linux capability-ների հավաքածու, գրելու հնարավորությամբ root ֆայլային համակարգ և setuid binary-ների միջոցով արտոնություններ ձեռք բերելու հնարավորություն։ Հանիր բոլոր capability-ները և ետ ավելացրու միայն անհրաժեշտը (80 port-ին կապվող web սերվերին պետք է NET_BIND_SERVICE, իսկ proxy-ի հետևում աշխատող Node և PHP հավելվածների մեծ մասին ոչինչ պետք չէ)։ Root ֆայլային համակարգը դարձրու read-only և հավելվածին տուր փոքր tmpfs ժամանակավոր ֆայլերի համար, որպեսզի հարձակվողը չկարողանա գործիքներ գցել դիսկի վրա։ Միացրու no-new-privileges, որպեսզի setuid հնարքները ձախողվեն։ Docker-ի լռելյայն seccomp profile-ն արդեն արգելափակում է տասնյակ վտանգավոր system call-եր․ երբեք մի անջատիր այն seccomp=unconfined-ով։ Ամենակարևորը՝ երբեք /var/run/docker.sock-ը mount մի արա հավելվածի container-ի մեջ․ socket-ի հասանելիությունը համարժեք է host-ի վրա root լինելուն։ Լրացուցիչ շերտի համար rootless ռեժիմը daemon-ն ինքն էլ գործարկում է որպես սովորական user։
services:
app:
user: "1000:1000"
read_only: true
tmpfs: [/tmp]
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
deploy:
resources:
limits: { cpus: "1.0", memory: 512M }
pids_limit: 200
09Docker-ը, iptables-ը և firewall-ի անակնկալը
Երբ port-ը publish ես անում ports: "5432:5432"-ով, Docker-ը iptables-ում գրում է NAT կանոններ, որպեսզի host-ի port-ին եկող տրաֆիկը փոխանցվի container-ին։ Այդ կանոնները ստուգվում են PREROUTING և FORWARD ուղիներում՝ ավելի շուտ, քան INPUT chain-ը, որտեղ ufw-ն պահում է իր կանոնները։ Արդյունքում ufw status-ը ասում է, որ 5432 port-ը արգելված է, իսկ բազան պատասխանում է ինտերնետից։ Կա երեք անվտանգ լուծում։ Ներքին service-ները ընդհանրապես publish մի արա․ նույն ցանցի container-ներին դա պետք չէ։ Եթե host-ի ինչ-որ գործիքի պետք է հասնել container-ին, կապիր loopback-ին՝ 127.0.0.1:5432:5432։ Եթե իսկապես պետք է ֆիլտրել publish արված port-երը, կանոններդ դիր DOCKER-USER chain-ում, որը Docker-ը ստուգում է իր կանոններից առաջ և երբեք չի վերագրում։ Ցանկացած փոփոխությունից հետո ստուգիր դրսից՝ port scan-ով, այլ ոչ թե վստահիր firewall-ի սեփական հաշվետվությանը։
services:
db:
image: postgres:17
# reachable only from the host itself, never from the internet
ports: ["127.0.0.1:5432:5432"]
redis:
image: redis:7-alpine
# no ports at all: only containers on the same network can connect
# from another machine: nmap -Pn -p 5432,6379 203.0.113.10
10Ավելի արագ և անվտանգ build-եր BuildKit-ով
BuildKit-ը, որը Docker-ի ընթացիկ տարբերակներում լռելյայն builder-ն է, ավելացնում է հնարավորություններ, որոնք սովորական շերտային քեշավորումը չունի։ Cache mount-ը package manager-ի ներբեռնումների քեշը պահում է build-երի միջև՝ առանց այն որևէ շերտի մեջ դնելու, ուստի lockfile-ի փոփոխությունից հետո տեղադրումը կատարվում է տաք քեշից, ոչ թե ցանցից։ Secret mount-ը token-ը (մասնավոր npm registry-ի կամ Composer auth-ի համար) հասանելի է դարձնում միայն մեկ RUN քայլի․ այն երբեք չի հայտնվում image-ի պատմության մեջ, ի տարբերություն ARG կամ ENV արժեքների, որոնք ցանկացածը կարող է կարդալ docker history-ով։ CI-ում runner-ները մեկնարկում են դատարկ, ուստի քեշը export արա registry և import արա հաջորդ գործարկման ժամանակ։ Վերարտադրելիության համար base image-ները ամրագրիր ոչ միայն tag-ով, այլև digest-ով, և թող ավտոմատացումը, օրինակ՝ Renovate-ը կամ Dependabot-ը, pull request բացի, երբ digest-ը փոխվում է։ BuildKit-ը կարող է նաև կցել SBOM և provenance attestation, որոնք անվտանգության scanner-ներն ու աուդիտորներն ավելի ու ավելի հաճախ են ակնկալում։
# syntax=docker/dockerfile:1
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
--mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
# build: docker build --secret id=npmrc,src=$HOME/.npmrc .
# CI: docker buildx build --cache-from type=registry,ref=registry.example.com/app:cache \
# --cache-to type=registry,ref=registry.example.com/app:cache,mode=max --push .
Իրական իրավիճակներ
Սերվերի դիսկը լցված է, և պատճառը Docker-ն է
Խնդիր
Մոնիթորինգը զգուշացնում է, որ root դիսկը 95% լցված է։ Հավելվածը դեռ աշխատում է, բայց բազան կդադարի գրառումներ ընդունել, երբ դիսկը լցվի, իսկ սերվերը ամիսներ շարունակ ոչ ոք չի մաքրել։
Ինչպես լուծել
Սկսիր docker system df-ով՝ տեսնելու, թե ինչն է մեծ․ image-նե՞րը, build cache-ը, volume-նե՞րը, թե container-ները։ Հին image-ները ջնջիր docker image prune -a --filter until=168h-ով, իսկ build cache-ը՝ docker builder prune-ով․ volume-ները երբեք կուրորեն prune մի արա, որովհետև բազայի տվյալները հենց այնտեղ են։ Հետո նայիր container-ների log-երը /var/lib/docker/containers-ում, քանի որ լռելյայն json-file driver-ը երբեք rotation չի անում։ /etc/docker/daemon.json-ում սահմանիր max-size և max-file, վերագործարկիր daemon-ը և վերստեղծիր container-ները, որովհետև log-ի կարգավորումները կիրառվում են միայն նոր container-ների վրա։ Վերջում ավելացրու շաբաթական prune job և դիսկի օգտագործման alert 80%-ի վրա, որպեսզի սա այլևս երբեք շտապ խնդիր չդառնա։
Container-ը անընդհատ վերագործարկվում է 137 exit code-ով
Խնդիր
Release-ից հետո հավելվածի container-ը մի քանի րոպեն մեկ վերագործարկվում է։ Log-երը կտրուկ ավարտվում են առանց stack trace-ի, իսկ docker ps-ը ցույց է տալիս աճող restart count։
Ինչպես լուծել
137 exit code-ը նշանակում է, որ պրոցեսը ստացել է SIGKILL, և սովորաբար ուղարկողը out-of-memory killer-ն է։ Հաստատիր docker inspect --format '{{.State.OOMKilled}}' app-ով և հետևիր օգտագործմանը docker stats-ով։ Հետո որոշիր՝ սխալը limit-ի՞ մեջ է, թե հավելվածի։ Node-ի համար --max-old-space-size-ը դիր container-ի limit-ից ցածր, այլապես heap-ը կարող է անցնել այդ սահմանը, մինչև garbage collection-ը կաշխատի։ Leak-ը փնտրիր՝ համեմատելով հիշողությունը ժամանակի ընթացքում․ հաճախ դա անսահմանափակ in-memory cache է կամ listener-ների աճող զանգված։ Limit-ը բարձրացրու միայն այն դեպքում, եթե իրական օգտագործումը դա արդարացնում է, և ավելացրու հիշողության alert, որպեսզի հաջորդ regression-ը երևա ավելի շուտ, քան restart-ները։
Բազան հասանելի է ինտերնետից՝ չնայած ufw-ին
Խնդիր
Սովորական scan-ը ցույց է տալիս, որ PostgreSQL-ը պատասխանում է public IP-ով։ ufw-ն հստակ արգելում է 5432 port-ը, և թիմը չի հասկանում, թե ինչպես է տրաֆիկն անցնում։
Ինչպես լուծել
Նախ վերաբերվիր դրան որպես incident-ի։ Փոխիր բազայի գաղտնաբառը և ստուգիր բազայի log-երում անծանոթ մուտքեր։ Հետո վերացրու պատճառը․ Compose ֆայլը publish է անում 5432:5432, և Docker-ի iptables կանոնները փոխանցում են այդ տրաֆիկը, մինչև ufw-ն կտեսնի այն։ Հեռացրու ports գրառումը, որպեսզի միայն ներքին ցանցի container-ները կարողանան միանալ, կամ կապիր 127.0.0.1-ին, եթե host-ի գործիքին դա պետք է։ Վերստեղծիր service-ը, ապա ստուգիր մեկ այլ մեքենայից՝ nmap -Pn -p 5432 203.0.113.10-ով։ Նույն արտաքին scan-ը ավելացրու ամսական checklist-ին և ստուգիր բոլոր Compose ֆայլերը՝ 80-ից և 443-ից բացի publish արված port-երի համար։
Արագ ուղեցույց
- Տեսնել, թե ինչն է զբաղեցնում դիսկը
docker system df -v- Ջնջել մեկ շաբաթից հին image-ները
docker image prune -a --filter until=168h- Container-ների log-երի rotation (daemon.json)
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }- Ստուգել health check-ի արդյունքը
docker inspect --format '{{json .State.Health}}' app- Հիշողության պատճառո՞վ է սպանվել
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app- CPU և հիշողություն ըստ container-ի՝ իրական ժամանակում
docker stats --no-stream- Վերակառուցել և վերագործարկել միայն մեկ service
docker compose up -d --no-deps --build app- Ստուգել միավորված Compose ֆայլերը
docker compose -f compose.yaml -f compose.prod.yaml config --quiet- Image-ը scan անել հայտնի CVE-ների համար
trivy image registry.example.com/app:1.8.0- Ցույց տալ, թե ով որ շերտն է կառուցել
docker history --no-trunc registry.example.com/app:1.8.0
Իմ իրական նախագծերում
- Multi-tenant POS SaaS-ը աշխատում է dedicated սերվերի վրա Docker Compose-ով՝ առանձին dev, prod, TLS և observability override ֆայլերով։
- Այդ stack-ում nginx-ը որպես reverse proxy կանգնած է հավելվածի առջև, իսկ PostgreSQL-ը, Redis-ը և BullMQ worker-ները առանձին service-ներ են՝ CDN-ի հետևում։
- Laravel նախագծերը լոկալ development-ի համար օգտագործում են Sail (Docker), և յուրաքանչյուր ծրագրավորող ստանում է PHP-ի, բազայի և Redis-ի նույն տարբերակները։
Հարցեր և պատասխաններ
Q1Ո՞րն է տարբերությունը image-ի և container-ի միջև։
Image-ը անփոփոխ, շերտավոր կաղապար է՝ ֆայլային համակարգ՝ գումարած մետատվյալներ, օրինակ՝ լռելյայն հրամանը և բացված port-երը։ Այն նույնականացվում է tag-ով, իսկ ավելի ճշգրիտ՝ բովանդակության digest-ով։ Container-ը այդ image-ի գործարկված օրինակն է․ Docker-ը վերևում ավելացնում է գրելու բարակ շերտ և մեկնարկում մեկուսացված պրոցես՝ իր namespace-ներով և cgroup սահմանափակումներով։ Մեկ image-ից կարող ես գործարկել բազմաթիվ container-ներ, իսկ container-ը ջնջելիս դրա գրելու շերտը կորչում է, այդ պատճառով մշտական տվյալների տեղը volume-ներում է։ Լավ մտավոր մոդել՝ image-ը class-ն է, container-ը՝ object-ը։
Q2Ինչո՞վ է Docker-ը տարբերվում վիրտուալ մեքենայից։
Վիրտուալ մեքենան hypervisor-ի վրա գործարկում է ամբողջական guest օպերացիոն համակարգ՝ իր սեփական kernel-ով, ուստի ծանր է՝ գիգաբայթերով դիսկ, սեփական բեռնման պրոցես և ապարատային մակարդակի ուժեղ մեկուսացում։ Container-ը host-ի սովորական պրոցես է, որը Linux kernel-ը մեկուսացնում է namespace-ներով և սահմանափակում cgroup-ներով։ Այն կիսում է host-ի kernel-ը, ուստի մեկնարկում է միլիվայրկյաններում և գրեթե overhead չունի։ Գինը ավելի թույլ մեկուսացումն է․ kernel-ի խոցելիությունն ազդում է host-ի բոլոր container-ների վրա։ Գործնականում երկուսը համակցվում են․ cloud մատակարարները քո container-ները գործարկում են VM-ների ներսում՝ խտություն ստանալով container-ներից և մեկուսացում՝ VM-ի սահմանից։
Q3Ինչպե՞ս փոքրացնել Docker image-ները և արագացնել build-երը։
Առաջինը՝ multi-stage build-եր․ կոմպիլյացիա արա բոլոր գործիքներով stage-ում, ապա միայն build-ի արդյունքը պատճենիր slim runtime image-ի մեջ։ Երկրորդ՝ ընտրիր փոքր բազա, օրինակ Alpine կամ slim Debian տարբերակ, և տեղադրիր միայն այն, ինչ հավելվածին պետք է runtime-ում։ Երրորդ՝ հրահանգները դասավորիր քեշավորման համար․ պատճենիր lockfile-ը, տեղադրիր կախվածությունները, ապա պատճենիր կոդը, որպեսզի կոդի խմբագրումները փաթեթները նորից չտեղադրեն։ Չորրորդ՝ խիստ .dockerignore-ը node_modules-ը, .git-ը և build-ի արդյունքը դուրս է պահում context-ից։ Վերջապես, BuildKit-ի cache mount-ները և CI-ում registry-ի վրա հիմնված քեշը թույլ են տալիս կրկնվող build-երին վերօգտագործել ներբեռնումները գործարկումների միջև։
Q4Ո՞րն է տարբերությունը CMD-ի և ENTRYPOINT-ի միջև։
ENTRYPOINT-ը սահմանում է այն executable-ը, որը container-ը միշտ գործարկում է․ CMD-ն տալիս է լռելյայն արգումենտներ կամ լռելյայն հրամանը, երբ ENTRYPOINT չկա։ docker run-ին փոխանցված արգումենտները փոխարինում են CMD-ին, բայց ավելացվում են ENTRYPOINT-ին։ Տարածված մոտեցում է entrypoint script-ը, որը նախապատրաստում է միջավայրը, օրինակ՝ սպասում է բազային կամ քեշավորում config-ը, ապա գործարկում exec "$@"՝ կառավարումը CMD-ին փոխանցելու համար։ Օգտագործիր JSON զանգվածի ձևը, օրինակ՝ CMD ["node", "server.js"], որպեսզի քո պրոցեսն աշխատի որպես PID 1 և ստոպ ազդանշաններն ստանա անմիջապես։ Shell ձևը այն փաթաթում է sh-ի մեջ, որը կարող է «կուլ տալ» SIGTERM-ը և կոտրել graceful shutdown-ը։
Q5Ինչո՞ւ իմ հավելվածի container-ը չի կարողանում միանալ բազային localhost-ով։
Որովհետև container-ի ներսում localhost-ը այդ container-ի սեփական ցանցային namespace-ն է, ոչ թե host-ը և ոչ էլ այլ container-ներ։ Compose-ի դեպքում բոլոր service-ները միանում են ընդհանուր ցանցին, և Docker-ը տրամադրում է DNS, ուստի հավելվածը պետք է միանա service-ի անվանը, օրինակ՝ host db և port 5432։ Համոզվիր, որ երկու service-ները նույն ցանցում են, որ բազան իսկապես պատրաստ է (օգտագործիր health check depends_on condition: service_healthy-ի հետ), և որ այն իր container-ի ներսում լսում է բոլոր interface-ները։ Դրա համար պետք չէ բազայի port-ը publish անել․ publish-ը միայն Docker-ից դուրսից եկող տրաֆիկի համար է։
Q6Արդյո՞ք Docker Compose-ը բավարար է production-ի համար։
Մեկ սերվերի համար՝ այո, և հաճախ դա ճիշտ որոշում է։ Compose-ը տալիս է դեկլարատիվ service-ներ, health check-եր, restart policy-ներ, volume-ներ և ցանցեր, և փոքր թիմի համար այն հեշտ է հասկանալ։ Այն, ինչ չի տալիս, բազմաhost-ային scheduling-ն է, մեքենաների միջև ավտոմատ failover-ը կամ ներկառուցված rolling update-ները։ Ուստի այն լավ է համապատասխանում, երբ մեկ ճիշտ չափի սերվերը՝ լավ backup-ներով և մոնիթորինգով, բավարարում է քո հասանելիության նպատակը։ Երբ քեզ պետք են մի քանի node-եր, դրանց վրա zero-downtime rollout-ներ և node-ի մահից հետո ավտոմատ վերաբաշխում, նայիր Kubernetes-ին, ECS-ին կամ ավելի թեթև գործիքի, ինչպիսիք են Kamal-ը կամ Swarm-ը։
Սենիոր մակարդակի հարցեր
S1Ինչպե՞ս է աշխատում container-ների graceful shutdown-ը, և ինչո՞ւ են որոշ հավելվածներ ամեն deploy-ի ժամանակ request-ներ կորցնում։
docker stop-ի կամ Compose-ի recreate-ի ժամանակ Docker-ը PID 1-ին ուղարկում է stop ազդանշանը (լռելյայն SIGTERM), սպասում է grace period-ին (լռելյայն 10 վայրկյան), ապա ուղարկում է SIGKILL։ Request-ները կորչում են, երբ այդ ազդանշանը երբեք չի հասնում հավելվածին, կամ հավելվածն այն անտեսում է։ CMD-ի shell ձևը հավելվածը գործարկում է sh-ի տակ, որը ազդանշանները չի փոխանցում․ PID 1 որպես աշխատող պրոցեսը՝ առանց սեփական handler-ի, ամբողջովին անտեսում է SIGTERM-ը։ Լուծումը CMD-ի exec ձևն է, init, օրինակ՝ tini-ն, և հավելված, որը SIGTERM ստանալիս դադարում է նոր կապեր ընդունել, ավարտում է ընթացիկ request-ները և փակում բազայի pool-երը։ Grace period-ը պետք է ավելի երկար լինի, քան ամենադանդաղ request-ը կամ job-ը։ Proxy-ի հետևում նաև համոզվիր, որ proxy-ն դադարում է տրաֆիկ ուղարկել հին container-ին մինչև դրա կանգնեցումը, կամ կրկնում է idempotent request-ները։
S2Alpine, Debian slim, թե distroless․ ինչպե՞ս ընտրել base image-ը։
Alpine-ը փոքրիկ է և patch անելու քիչ փաթեթներ ունի, բայց glibc-ի փոխարեն օգտագործում է musl։ Կոդի մեծ մասի համար դա նշանակություն չունի, սակայն native module-ները, որոշ Python wheel-ներ, DNS resolution-ի եզրային դեպքերը և արտադրողականության նկատմամբ զգայուն կոդը կարող են այլ կերպ վարվել կամ պահանջել source-ից կոմպիլյացիա, ինչը դանդաղեցնում է build-երը։ Debian կամ Ubuntu slim image-ները ավելի մեծ են, բայց օգտագործում են glibc, ուստի պատրաստի binary-ները պարզապես աշխատում են, և debugging գործիքներ ավելացնելը հեշտ է։ Distroless image-ները պարունակում են միայն runtime-ը և քո հավելվածը՝ առանց shell-ի կամ package manager-ի, ինչը փոքրացնում է հարձակման մակերեսը և CVE-ների ցուցակը, բայց դժվարացնում է ինտերակտիվ debugging-ը․ դրա փոխարեն ապավինում ես log-երին, մետրիկաներին և ժամանակավոր debug container-ներին։ Իմ լռելյայն ընտրությունը slim-ն է՝ համատեղելիության համար, Alpine-ը՝ երբ stack-ը դրա վրա փորձարկել եմ, և distroless-ը՝ լավ observability ունեցող հասուն service-ների համար։
S3Ինչպե՞ս պաշտպանել container-ների supply chain-ը՝ base image-ից մինչև production։
Մտածիր երեք փուլով։ Build-ի ժամանակ՝ նվազագույն, ամրագրված base image-ներ, multi-stage build-եր, որպեսզի կոմպիլյատորները երբեք չհասնեն production, շերտերում ոչ մի secret (դրա փոխարեն BuildKit-ի secret mount-ներ) և image-ի հետ գեներացված SBOM։ Registry-ում՝ ամեն image scan արա հայտնի խոցելիությունների համար Trivy-ով, Grype-ով կամ Docker Scout-ով, pipeline-ը ձախողիր այն կրիտիկական գտածոների դեպքում, որոնք ուղղում ունեն, և image-ները ստորագրիր, օրինակ՝ cosign-ով, որպեսզի կարողանաս ապացուցել, որ image-ը եկել է քո CI-ից։ Runtime-ում՝ deploy արա digest-ով, ոչ թե փոփոխվող tag-ով, գործարկիր non-root-ով, հանված capability-ներով և read-only ֆայլային համակարգով, և պարբերաբար վերակառուցիր, որպեսզի base image-ի patch-երը հասնեն նույնիսկ այն ժամանակ, երբ քո կոդը չի փոխվում։ Փոխզիջումը աղմուկն է․ scanner-ները հաղորդում են բազմաթիվ անուղղելի կամ անկապ CVE-ներ, ուստի սահմանիր հստակ policy, թե ինչն է արգելափակում release-ը։
S4Կարելի՞ է Docker Compose-ով deploy անել մեկ սերվերի վրա առանց downtime-ի։
Ոչ «տուփից դուրս»․ docker compose up-ը փոխված container-ը վերստեղծում է՝ կանգնեցնելով հինը և մեկնարկելով նորը, ուստի մի քանի վայրկյանանոց բաց է առաջանում, մինչ հավելվածը բեռնվում է։ Սա շրջանցելու մի քանի եղանակ կա։ Գործարկիր հավելվածի երկու replica nginx-ի հետևում և թարմացրու դրանք մեկ առ մեկ՝ յուրաքանչյուրի health check-ին սպասելով, նախքան հաջորդին անցնելը։ Կամ օգտագործիր blue-green զույգ service-ներ․ անգործ «գույնը» մեկնարկիր նոր image-ով, ստուգիր դրա health endpoint-ը, ապա փոխիր nginx-ի upstream-ը և reload արա, ինչը graceful է։ Kamal-ի կամ docker-rollout plugin-ի նման գործիքները ավտոմատացնում են հենց այս pattern-ը։ Ինչ էլ ընտրես, հավելվածին պետք է graceful shutdown, իրական readiness endpoint և migration-ներ, որոնք համատեղելի են երկու տարբերակների հետ, որովհետև կարճ ժամանակով հին և նոր container-ները միասին են սպասարկում տրաֆիկը։
Բառարան
- Namespace
- Kernel-ի հնարավորություն, որը պրոցեսին տալիս է ֆայլերի, ցանցի, user-ների կամ process ID-ների սեփական տեսք՝ մեկուսացնելով այն մյուսներից։
- cgroup
- Kernel-ի հնարավորություն, որը սահմանափակում և չափում է, թե պրոցեսների խումբը որքան CPU, հիշողություն և I/O կարող է օգտագործել։
- OCI
- Open Container Initiative՝ image-ների ձևաչափերի և runtime-ների բաց ստանդարտներ, որպեսզի տարբեր գործիքներ կարողանան գործարկել նույն image-ները։
- containerd
- Container runtime daemon, որը ներբեռնում է image-ները և կառավարում container-ների կյանքի ցիկլը․ այն օգտագործում են և՛ Docker-ը, և՛ Kubernetes-ը։
- runc
- Ցածր մակարդակի գործիք, որն իրականում ստեղծում է container-ի պրոցեսը namespace-ներով և cgroup-ներով՝ OCI runtime spec-ի համաձայն։
- Overlay filesystem
- Ֆայլային համակարգ, որը read-only image շերտերը և մեկ գրելի շերտը դնում է իրար վրա, որպեսզի դրանք երևան որպես մեկ ծառ։
- Image digest
- Image-ի բովանդակության sha256 hash․ ի տարբերություն tag-ի, այն հետագայում երբեք չի կարող ցույց տալ այլ բովանդակություն։
- BuildKit
- Docker-ի ժամանակակից build engine-ը, որը ավելացնում է զուգահեռ stage-եր, cache և secret mount-ներ, հեռավոր քեշեր և attestation-ներ։
- PID 1
- Container-ի առաջին պրոցեսը․ այն լռելյայն signal handling չունի և պետք է հավաքի zombie child պրոցեսները։
- Rootless mode
- Docker daemon-ի և container-ների գործարկում սովորական user-ով, որպեսզի container-ից դուրս պրծնելը host-ի վրա root չտա։
Շարունակեք սովորել
Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։
Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։