Yakuniy loyiha — to'liq stack ni konteynerlash va deploy
Muammodan boshlaylik
Siz 44 dars davomida alohida qismlarni o'rgandingiz: image, Dockerfile, volume, tarmoq, Compose, registry, xavfsizlik, CI/CD, deploy, monitoring. Haqiqiy ishda ular birga ishlaydi. Bu yakuniy loyiha — hammasini bitta tizimga yig'ish.
Vazifa: "Dokon" onlayn do'koni uchun to'liq stek:
- Frontend — React (Vite), Nginx orqali
- API — Node.js (Express yoki NestJS)
- Baza — MySQL
- Kesh — Redis
- Navbat ishlovchisi — buyurtma emaillarini yuboradi
- Reverse proxy — Caddy, HTTPS bilan
- CI/CD — GitHub Actions → GHCR → VPS
Har bir bosqichda — qaysi darsga tayanish kerakligi ko'rsatilgan. Avval o'zingiz yozing, keyin yechimni oching.
Loyiha tuzilishi
dokon/
├── frontend/
│ ├── Dockerfile
│ ├── nginx.conf
│ └── src/...
├── api/
│ ├── Dockerfile
│ ├── .dockerignore
│ └── src/...
├── deploy/
│ ├── compose.yaml # serverga ko'chiriladi
│ └── Caddyfile
├── compose.yaml # ishlab chiqish uchun asos
├── compose.override.yaml # dev: watch, portlar
├── .env.example
└── .github/workflows/deploy.yml
Arxitektura:
Internet
│ 80/443
┌────▼────┐
│ caddy │ ← HTTPS (Let's Encrypt)
└─┬─────┬─┘
/api/* │ │ /*
┌─────▼─┐ ┌─▼──────┐
│ api │ │ web │ tarmoq: frontend
└─┬───┬─┘ └────────┘
──────────┼───┼─────────────────────────────────
┌─▼─┐ ┌▼─────┐ ┌────────┐ tarmoq: backend (internal)
│ db│ │redis │◄─┤ worker │
└───┘ └──────┘ └────────┘
1-bosqich: API Dockerfile
Darslar: 12–17, 33
Talablar:
- Multi-stage:
deps,dev,build,production productionda faqat production bog'liqliklar, root siz foydalanuvchi- Healthcheck
nodeto'g'ridan-to'g'ri (npm orqali emas)- BuildKit kesh mount
Yechim
# syntax=docker/dockerfile:1
FROM node:22-alpine AS base
WORKDIR /app
FROM base AS deps
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
FROM deps AS dev
COPY . .
CMD ["npm", "run", "dev"]
FROM deps AS build
COPY . .
RUN npm run build
FROM base AS production
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
COPY --from=build --chown=node:node /app/dist ./dist
USER node
EXPOSE 3000
HEALTHCHECK --interval=15s --timeout=3s --start-period=15s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
.dockerignore:
node_modules
dist
.git
.env*
!.env.example
*.log
coverage
Worker — xuddi shu image, command: ["node", "dist/worker.js"].
2-bosqich: Frontend Dockerfile
Darslar: 16, 36
Talablar:
- Node da build, Nginx da berish
- SPA routing (
try_files) /assets/uzoq keshlanadi,index.html— yo'q- API manzili — nisbiy
/api(bitta image hamma muhitda)
Yechim
# syntax=docker/dockerfile:1
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
nginx.conf:
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location = /index.html {
add_header Cache-Control "no-cache";
}
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
}
Frontend kodida: fetch('/api/mahsulotlar').
3-bosqich: Ishlab chiqish muhiti
Darslar: 26–31
Talablar:
docker compose up --watch— hammasi ko'tariladi, kod o'zgarishi darhol ko'rinadi- Baza va Redis healthcheck bilan, API ular tayyor bo'lgandan keyin
- Baza ma'lumotlari volume da
- Adminer — faqat
--profile toolsbilan
Yechim
compose.yaml:
name: dokon
services:
api:
build:
context: ./api
target: production
environment:
DATABASE_URL: mysql://dokon:${DB_PASSWORD:?}@db:3306/dokon
REDIS_URL: redis://redis:6379
depends_on:
db: { condition: service_healthy }
redis: { condition: service_healthy }
networks: [frontend, backend]
worker:
build:
context: ./api
target: production
command: ["node", "dist/worker.js"]
environment:
DATABASE_URL: mysql://dokon:${DB_PASSWORD}@db:3306/dokon
REDIS_URL: redis://redis:6379
depends_on:
redis: { condition: service_healthy }
networks: [backend]
web:
build: ./frontend
networks: [frontend]
db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?}
MYSQL_DATABASE: dokon
MYSQL_USER: dokon
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--silent"]
interval: 5s
retries: 20
networks: [backend]
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
networks: [backend]
adminer:
image: adminer
profiles: [tools]
ports: ["127.0.0.1:8081:8080"]
networks: [backend]
networks:
frontend:
backend:
volumes:
db-data:
compose.override.yaml (avtomatik qo'llanadi):
services:
api:
build:
target: dev
ports: ["3000:3000"]
develop:
watch:
- action: sync
path: ./api/src
target: /app/src
- action: rebuild
path: ./api/package.json
web:
build:
target: build
command: npm run dev -- --host 0.0.0.0
ports: ["5173:5173"]
develop:
watch:
- action: sync
path: ./frontend/src
target: /app/src
db:
ports: ["127.0.0.1:3306:3306"]
Vite dev serverida /api ni API ga proxy qiling (vite.config.js → server.proxy: { '/api': 'http://api:3000' }).
cp .env.example .env
docker compose up --watch
docker compose --profile tools up -d adminer
Dev da backend internal emas — lokal portlar kerak. Production da (5-bosqich) — internal: true.
4-bosqich: CI/CD
Darslar: 38, 39, 41
Talablar:
- PR — testlar
main— testlar → ikki image (api, web) GHCR ga → Trivy → deploy- Teglar:
sha-...valatest
Yechim
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: ./api
target: dev
load: true
tags: dokon-api:test
cache-from: type=gha,scope=api
cache-to: type=gha,mode=max,scope=api
- run: docker run --rm dokon-api:test npm test
build:
needs: test
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
include:
- name: api
context: ./api
target: production
- name: web
context: ./frontend
target: ""
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: ${{ matrix.context }}
target: ${{ matrix.target }}
push: true
tags: |
ghcr.io/${{ github.repository }}-${{ matrix.name }}:sha-${{ github.sha }}
ghcr.io/${{ github.repository }}-${{ matrix.name }}:latest
cache-from: type=gha,scope=${{ matrix.name }}
cache-to: type=gha,mode=max,scope=${{ matrix.name }}
- uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: ghcr.io/${{ github.repository }}-${{ matrix.name }}:sha-${{ github.sha }}
severity: CRITICAL
exit-code: "1"
ignore-unfixed: true
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: deploy
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /opt/dokon
export TAG=sha-${{ github.sha }}
echo "TAG=$TAG" > .tag
docker compose --env-file .env --env-file .tag pull
docker compose --env-file .env --env-file .tag up -d
docker image prune -f
matrix— bitta job ta'rifi, ikki imagescope— har image uchun alohida kesh.tagfayl — joriy versiya serverda saqlanadi (keyingiup -dlarda ham, rollback uchun ham)
5-bosqich: Production server
Darslar: 25, 37, 39, 40, 42
Talablar:
- Faqat Caddy tashqariga ochiq; backend tarmoq
internal - Barcha xizmatlarda
restart, resurs chegaralari - API:
read_only,cap_drop,no-new-privileges - Sirlar —
.env(chmod 600) - Log hajmi cheklangan
Yechim
deploy/compose.yaml (serverda /opt/dokon/compose.yaml):
name: dokon
x-xavfsiz: &xavfsiz
restart: unless-stopped
read_only: true
tmpfs: [/tmp]
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
logging:
driver: local
options: { max-size: "10m", max-file: "3" }
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports: ["80:80", "443:443", "443:443/udp"]
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
networks: [frontend]
web:
image: ghcr.io/firdavsdev07/dokon-web:${TAG:-latest}
restart: unless-stopped
deploy:
resources:
limits: { memory: 64M }
networks: [frontend]
api:
<<: *xavfsiz
image: ghcr.io/firdavsdev07/dokon-api:${TAG:-latest}
environment:
DATABASE_URL: mysql://dokon:${DB_PASSWORD:?}@db:3306/dokon
REDIS_URL: redis://redis:6379
NODE_OPTIONS: --max-old-space-size=384
deploy:
resources:
limits: { memory: 512M, cpus: "1.0" }
depends_on:
db: { condition: service_healthy }
redis: { condition: service_healthy }
networks: [frontend, backend]
worker:
<<: *xavfsiz
image: ghcr.io/firdavsdev07/dokon-api:${TAG:-latest}
command: ["node", "dist/worker.js"]
environment:
DATABASE_URL: mysql://dokon:${DB_PASSWORD}@db:3306/dokon
REDIS_URL: redis://redis:6379
SMTP_URL: ${SMTP_URL:?}
deploy:
resources:
limits: { memory: 256M }
networks: [backend, egress]
db:
image: mysql:8.4
restart: unless-stopped
command: --innodb-buffer-pool-size=512M
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?}
MYSQL_DATABASE: dokon
MYSQL_USER: dokon
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- db-data:/var/lib/mysql
deploy:
resources:
limits: { memory: 1G }
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--silent"]
interval: 5s
retries: 20
networks: [backend]
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --maxmemory 100mb --maxmemory-policy allkeys-lru
deploy:
resources:
limits: { memory: 128M }
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
networks: [backend]
networks:
frontend:
backend:
internal: true
egress: # worker email yuborishi uchun internetga chiqish
volumes:
caddy-data:
caddy-config:
db-data:
x-xavfsiz+<<: *xavfsiz— YAML anchor: umumiy sozlamalar bir marta yoziladi (31-dars)backend: internal— baza va Redis internetga umuman chiqa olmaydiworkeremail yuborishi uchun — alohidaegresstarmog'i (internaltarmoq tashqariga chiqishni ham yopadi)apifrontendtarmog'i orqali Caddy bilan,backendorqali baza bilan
deploy/Caddyfile:
dokon.uz {
encode gzip zstd
handle /api/* {
reverse_proxy api:3000
}
handle {
reverse_proxy web:80
}
}
www.dokon.uz {
redir https://dokon.uz{uri} permanent
}
/api/* uchun handle (handle_path emas) — prefiks saqlanadi, chunki API marshrutlari /api/... bilan boshlanadi.
6-bosqich: Zaxira va monitoring
Darslar: 21, 42, 43
Talablar:
- Har kecha baza zaxirasi, 14 kun saqlash, boshqa joyga nusxa
- Tashqi uptime monitoring + Telegram ogohlantirish
- Tiklash sinovi
Yechim
/opt/dokon/backup.sh:
#!/bin/sh
set -e
cd /opt/dokon
SANA=$(date +%F_%H%M)
FAYL=backups/dokon_$SANA.sql.gz
docker compose exec -T db sh -c \
'mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction --routines dokon' \
| gzip > "$FAYL"
# Bo'sh yoki buzilgan zaxirani aniqlash
gzip -t "$FAYL"
[ "$(stat -c%s "$FAYL")" -gt 1000 ] || { echo "Zaxira juda kichik!"; exit 1; }
rclone copy "$FAYL" b2:dokon-backups/
find backups -name "*.sql.gz" -mtime +14 -delete
# crontab -e
0 3 * * * /opt/dokon/backup.sh >> /var/log/dokon-backup.log 2>&1
Tiklash sinovi (oyiga bir marta, alohida konteynerda):
docker run -d --name tiklash-sinov -e MYSQL_ROOT_PASSWORD=sinov mysql:8.4
sleep 30
gunzip -c backups/dokon_2026-11-06_0300.sql.gz \
| docker exec -i tiklash-sinov sh -c 'mysql -u root -psinov -e "CREATE DATABASE dokon" && mysql -u root -psinov dokon'
docker exec tiklash-sinov mysql -u root -psinov -e "SELECT COUNT(*) FROM dokon.buyurtmalar"
docker rm -f tiklash-sinov
Monitoring: UptimeRobot / Better Stack (yoki boshqa serverdagi Uptime Kuma) da https://dokon.uz/api/health ni har daqiqada tekshirish + Telegram bildirishnomasi.
Yakuniy tekshiruv
Ishlab chiqish
[ ] git clone → cp .env.example .env → docker compose up --watch → ishlaydi
[ ] Kod o'zgarishi bir necha soniyada ko'rinadi
Image lar
[ ] API image < 200 MB, web image < 60 MB
[ ] Root siz (docker run --rm ... whoami)
[ ] docker history da sirlar yo'q
[ ] Trivy: CRITICAL yo'q
CI/CD
[ ] PR → faqat testlar
[ ] main → GHCR da sha-... teglari → serverda yangi versiya
[ ] Rollback: oldingi TAG bilan up -d — ishlaydi
Production
[ ] https://dokon.uz — yashil qulf, http → https
[ ] Sahifani /mahsulot/42 da yangilash — 404 emas
[ ] Faqat 80/443 ochiq (tashqaridan nmap bilan tekshiring)
[ ] Server qayta yuklandi (reboot) → hammasi o'zi ko'tarildi
[ ] docker stop api → 1-2 soniyada to'xtaydi
[ ] Zaxira har kuni, boshqa joyda, tiklash sinalgan
[ ] Sayt o'chirilsa — Telegram ga xabar keladi
Nima uchun shunday?
Nima uchun bu loyihada shuncha qatlam — oddiy docker run lar bilan ham sayt ishlardi-ku?
Ishlardi — birinchi kun. Lekin production — bu birinchi kun emas, keyingi 1000 kun: yuzlab deploy, server qayta yuklanishlari, xotira oqishlari, xavfsizlik zaifliklari, yangi jamoa a'zolari, va bir kuni — disk buzilishi. Bu loyihadagi har bir qatlam ana shu kunlardan biriga javob: CI — "kim nimani deploy qildi", aniq teglar — "qanday qaytaramiz", chegaralar — "nega hammasi qulab tushdi", internal tarmoq — "baza qanday sizdi", zaxira — "ma'lumotlar qayerda", monitoring — "nega mijozdan bildik". Docker bularning hammasini kod sifatida yozish imkonini beradi — takrorlanadigan, ko'rib chiqiladigan va ishonchli.
Xulosa
Siz Docker kursini tugatdingiz! Endi siz:
- Image va konteyner qanday ishlashini, qatlamlar va keshni tushunasiz
- Har qanday til uchun samarali, xavfsiz, multi-stage Dockerfile yoza olasiz
- Volume, bind mount va tarmoqlar bilan ma'lumotlar va xizmatlarni boshqarasiz
- Compose bilan ko'p xizmatli loyihani bitta buyruq bilan ishga tushirasiz
- Node.js, PHP, Python va frontend loyihalarini konteynerlashtirasiz
- Registry, CI/CD va VPS orqali production ga chiqarasiz
- Xavfsizlik, resurslar, loglar va monitoringni sozlaysiz
- Kubernetes ga o'tish uchun poydevorga egasiz
Keyingi qadam
- Shu loyihani haqiqatan VPS ga chiqaring — eng yaxshi o'rganish usuli
- O'z mavjud loyihalaringizdan birini konteynerlashtiring
- Kubernetes:
kindbilan lokal klaster, rasmiy "Kubernetes Basics" qo'llanmasi - Rasmiy hujjatlar: docs.docker.com
Omad! 🐳