DevOps Academy

Docker

מ-Dockerfile בסיסי לדפוסי production: multi-stage builds, healthchecks, log rotation ו-Compose

תיאוריה

⏱ ~45 דקות

מדוע Docker חשוב בשוק העבודה הישראלי

לפי סריקת שוק העבודה האחרונה (אפריל 2026), Docker מופיע ב-10.6% ממשרות ה-DevOps בישראל — המיקום השלישי אחרי CI/CD ו-Kubernetes. כל ראיון DevOps ב-2026 מניח היכרות עם Docker, ושאלות הראיון לא עוצרות ב-docker run. המראיינים בודקים האם המועמד מבין image layers ו-build cache, האם הוא כותב multi-stage builds, האם הוא יודע לקנפג healthcheck ו-log rotation, ואיך הוא מנהל compose בסביבת production. הסעיפים הבאים מכסים בדיוק את הפערים האלה.

Image layers ו-build cache: כיצד Docker באמת בונה

כל הוראה ב-Dockerfile יוצרת layer חדש. Docker שומר כל layer ב-cache לפי hash של תוכן ה-layer שלפניו. כאשר layer משתנה, כל ה-layers מתחתיו נבנים מחדש — זו הסיבה שסדר ההוראות ב-Dockerfile משנה מאוד לזמני build.

דוגמה מעשית: שימו את COPY requirements.txt . ו-RUN pip install לפני COPY . .. כך, שינוי בקוד האפליקציה לא יגרום להתקנה מחדש של ה-dependencies:

FROM python:3.12-slim
WORKDIR /app
# Layer זה משתנה רק כשמשתנה requirements.txt
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Layer זה משתנה בכל push
COPY . .
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

כלל הזהב: מהיציב ביותר לפחות יציב, מלמעלה למטה.

Multi-stage builds: images קטנים ו-secure ל-production

פער production

הבעיה: image שנבנה עם כלי compile (gcc, go, mvn) נושא את הכלים האלה לתוך production — מיותר, כבד, ומסוכן (כל package נוסף הוא attack surface).

הפתרון: multi-stage build. שלב ה-builder מקמפל; שלב ה-runtime לא מכיר את ה-builder ומקבל רק את ה-artifact:

# שלב 1: builder — כלי הבנייה נמצאים כאן
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# CGO_ENABLED=0 מייצר binary סטטי שלא תלוי ב-glibc
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/app ./cmd/server

# שלב 2: runtime — image מינימלי לחלוטין
FROM gcr.io/distroless/static-debian12
USER nonroot:nonroot
COPY --from=builder /out/app /app
EXPOSE 8080
ENTRYPOINT ["/app"]

תוצאה: image של Go app עם distroless יכול להיות פחות מ-15MB לעומת 800MB+ עם golang:1.22.

לבדיקה: docker build -t myapp . && docker images myapp

נקודה נוספת: ניתן לשתף שלבים בין builds שונים באמצעות --target:

# בנה רק עד שלב ה-builder לצורך debugging
docker build --target builder -t myapp-debug .

ב-CI/CD (GitHub Actions, Jenkins) — תמיד השתמשו ב---cache-from עם registry cache:

docker build \
  --cache-from myregistry/myapp:cache \
  --build-arg BUILDKIT_INLINE_CACHE=1 \
  -t myregistry/myapp:latest .

HEALTHCHECK: כיצד Docker יודע שהאפליקציה עובדת

פער production

סטטוס running של container לא אומר שהאפליקציה בפנים עובדת. container יכול להיות running בזמן שה-web server בפנים קרס, נתקע ב-deadlock, או לא גמר לאתחל. HEALTHCHECK פותר את הבעיה הזו.

קינפוג מלא ב-Dockerfile:

HEALTHCHECK \
  --interval=30s \
  --timeout=5s \
  --start-period=10s \
  --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

הסברים:
- --interval=30s — בדיקה כל 30 שניות. בproduction: לא פחות מ-15s כדי לא להציף את ה-endpoint.
- --timeout=5s — אם הבדיקה לא חזרה תוך 5 שניות, נחשב כ-failure.
- --start-period=10s — grace period לאפליקציה שצריכה זמן לקום (DB migrations, cache warm-up). בדיקות שנכשלות בתוך start-period לא נספרות.
- --retries=3 — אחרי 3 כשלות רצופות, הסטטוס משתנה ל-unhealthy.

בדיקת הסטטוס:

docker inspect --format='{{.State.Health.Status}}' <container_id>
# ערכים: starting | healthy | unhealthy

Docker Compose ו-Kubernetes משתמשים בסטטוס הזה לניהול restart policies וtraffic routing. container שהוא unhealthy יופסק ויוחלף אוטומטית אם מוגדר restart: unless-stopped.

עצה לproduction: endpoint ה-/health צריך לבדוק dependencies פנימיים (DB connection, cache) — לא רק שהתהליך חי. מבנה מקובל:

{
  "status": "ok",
  "db": "ok",
  "cache": "ok",
  "version": "1.4.2"
}

Container log rotation: --log-opt ותפעול logs בproduction

פער production

כברירת מחדל, Docker שומר logs של container בקובץ JSON ב-/var/lib/docker/containers/<id>/<id>-json.log — ללא הגבלת גודל. בproduction, container שמדפיס הרבה logs ימלא את ה-disk תוך שעות.

הפתרון: --log-opt עם logging driver json-file:

# בהפעלה ידנית
docker run \
  --log-driver=json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  myapp:latest

פירוש:
- max-size=10m — כל קובץ log מוגבל ל-10 מגה. Docker גולל (rotate) אחרי שמגיע לגבול.
- max-file=3 — שומר מקסימום 3 קבצים (current + 2 backups). בסך הכל: לכל היותר 30MB לcontainer.

הגדרה גלובלית ב-Docker daemon (מומלץ לproduction — חל על כל container שלא מגדיר אחרת):

// /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

אחרי שינוי, יש לעשות: sudo systemctl reload docker (containers קיימים לא מושפעים — רק containers חדשים).

logging drivers נוספים לproduction:
- --log-driver=journald — שולח logs ישירות ל-systemd journal; מאפשר journalctl -u docker.service -f
- --log-driver=fluentd — למערכות log aggregation (EFK stack: Elasticsearch, Fluentd, Kibana)
- --log-driver=awslogs — שולח ישירות ל-AWS CloudWatch Logs

לצפייה ב-logs:

docker logs --tail 100 --follow --timestamps <container_id>

שגיאה נפוצה בproduction: שינוי daemon.json לא מאפס logs של containers שכבר רצים. צריך להפסיק ולהפעיל מחדש את ה-container.

Docker Compose דפוסי production

פער production

Docker Compose מיועד פעם ל-development בלבד, אך בproduction על VPS יחיד (כמו בפלטפורמה הנוכחית) הוא כלי נפוץ ותקף. ההבדל הוא בדפוסי השימוש.

**1. קבצי override: dev vs production**

מוסכמה מקובלת היא לפצל לשני קבצים:

# compose.yml       — הגדרות בסיסיות (services, networks, volumes)
# compose.prod.yml  — overrides לproduction (log-opts, restart, resource limits)

# הרצה בproduction:
docker compose -f compose.yml -f compose.prod.yml up -d

compose.prod.yml לדוגמה:

services:
  api:
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 256M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3001/health"]
      interval: 30s
      timeout: 5s
      start_period: 10s
      retries: 3

**2. secrets ו-environment variables**

לעולם לא מכניסים secrets ל-compose.yml ישירות. במקום:

services:
  api:
    env_file:
      - .env.production
    environment:
      - NODE_ENV=production

קובץ .env.production מוגן בהרשאות:

chmod 600 .env.production
# ו-.gitignore צריך לכלול: .env.production

**3. networks מבודדים**

לا מדברים ישירות עם DB מהאינטרנט — backend ו-DB על network פנימי, רק frontend/proxy חשוף:

networks:
  frontend:
  backend:

services:
  nginx:
    networks: [frontend, backend]
  api:
    networks: [backend]
  db:
    networks: [backend]

**4. depends_on עם condition**

services:
  api:
    depends_on:
      db:
        condition: service_healthy
  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5

כך ה-api לא מתחיל עד שה-DB באמת מוכן — לא רק שה-container רץ.

**5. zero-downtime deploys בVPS יחיד**

# Pull image חדש
docker compose pull api
# Recreate רק את service זה (Compose יחכה ל-healthcheck)
docker compose up -d --no-deps api
# בדוק שה-container healthy
docker compose ps api

CMD vs ENTRYPOINT: ההבדל שצריך לדעת לראיון

זו שאלה קלאסית בראיונות Docker. ההבדל בא לידי ביטוי בשני ממדים:

| | ENTRYPOINT | CMD |
|---|---|---|
| מטרה | הפקודה הראשית, תמיד רצה | ברירת מחדל לארגומנטים |
| ניתן לדריסה | רק עם --entrypoint | כל ארגומנט שמועבר ב-docker run |
| שניהם ביחד | ENTRYPOINT = הפקודה, CMD = ברירות מחדל לארגומנטים |

דוגמה מעשית:

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

docker run myimage → מריץ nginx -g daemon off;
docker run myimage -g "daemon on;" → מריץ nginx -g daemon on; (CMD נדרס)

שימוש נפוץ בproduction: entrypoint script שמטפל ב-signal handling ו-graceful shutdown:

COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["node", "server.js"]

חשוב: תמיד השתמשו ב-exec form ["cmd", "arg"] ולא ב-shell form cmd arg. Shell form מריץ את הפקודה כ-/bin/sh -c, כך שהפרוסס לא מקבל SIGTERM ישירות — גורם לבעיות graceful shutdown.

Volumes ו-bind mounts: אחסון נתונים בין הפעלות

כל נתון שנכתב בתוך container מאבד בעת מחיקת ה-container. שלושה סוגי אחסון:

**1. Named volumes** (מומלץ לDB data בproduction):

volumes:
  pgdata:

services:
  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data

Docker מנהל את המיקום בפועל; ניתן לגבות עם docker run --rm -v pgdata:/data alpine tar czf - /data.

**2. Bind mounts** (מומלץ לdev בלבד):

docker run -v $(pwd)/src:/app/src myapp

בעיות בproduction: תלות ב-path בhost, בעיות הרשאות (UID mismatch), לא ניתן לשכפל.

**3. tmpfs** (בזיכרון, לנתונים זמניים רגישים):

docker run --tmpfs /tmp:rw,noexec,nosuid,size=100m myapp

אבטחת containers ב-production

עשרת הפרקטיקות הנפוצות שנשאלות בראיונות:

1. **אל תריצו כ-root**: USER 1000:1000 ב-Dockerfile. בדיקה: docker run --rm myapp whoami.
2. **Read-only filesystem**: docker run --read-only --tmpfs /tmp myapp — container לא יכול לכתוב לשום מקום מלבד /tmp.
3. **Drop capabilities**: docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp.
4. **No new privileges**: docker run --security-opt no-new-privileges myapp.
5. **Scan images**: docker scout cves myapp:latest או trivy image myapp:latest.
6. **Pin base image tags**: FROM python:3.12.3-slim לא FROM python:latest.
7. **Multi-stage**: אל תשלחו dev tools לproduction.
8. **Secrets בmount, לא ב-ENV**: --secret id=dbpass,src=./dbpass עם RUN --mount=type=secret,id=dbpass.
9. **Resource limits**: --memory=256m --cpus=0.5 — מונע resource exhaustion.
10. **Network segregation**: containers שלא צריכים לדבר האחד עם השני לא אמורים להיות באותו network.


תרגול מעשי

1. אימות מבנה multi-stage Dockerfile

פער production

הדפס Dockerfile לפלט הסטנדרטי ועבד אותו עם grep ו-awk כדי לאמת שיש בו בדיוק 2 שלבי FROM, שהשלב הראשון מוגדר כ-AS builder, שיש RUN עם CGO_ENABLED=0, ושיש COPY --from=builder. כל בדיקה תדפיס PASS או FAIL. הדפסת כל ה-PASS מוכיחה שה-Dockerfile עומד בדרישות multi-stage production.

2. אימות הגדרות HEALTHCHECK ב-docker inspect JSON

פער production

JSON המדמה פלט docker inspect מוזרם ל-python3. חלץ ואמת שה-container מוגדר עם HEALTHCHECK: Interval=30s, Timeout=5s, StartPeriod=10s, Retries=3. חלץ גם את סטטוס ה-health הנוכחי. תרגיל זה בוחן ניתוח פלט inspect — משימה יומיומית בdiagnostics production.

3. ניתוח docker inspect JSON ואיתור הגדרות log rotation

פער production

JSON המדמה פלט docker inspect מוזרם ל-jq. חלץ ואמת: (1) LogConfig.Type הוא json-file, (2) הערך של max-size הוא 10m, (3) הערך של max-file הוא 3, (4) RestartPolicy הוא unless-stopped. תרגיל זה בוחן הבנת log rotation configuration ויכולת קריאת inspect output עם jq.


שאלות חיבור

חבר את מה שלמדת בנושא זה לנושאים קודמים. אין תשובה אחת נכונה — חשיבה ביקורתית היא המטרה.

שאלת חיבור 1

ב-Linux Advanced למדת על logrotate ל-application logs ועל systemd units לניהול שירותים ארוכי טווח. כיצד --log-opt max-size ו-max-file ב-Docker משלימים את logrotate, ולמה פתרונות אלה לא ניתן להחליף האחד את השני כאשר האפליקציה רצה בcontainer?

מחבר ל:
Linux Advanced

שאלת חיבור 2

ב-Python for DevOps למדת לכתוב scripts שמשתמשים ב-subprocess ו-os.environ לניהול environment. כיצד הידע הזה קשור לאופן שבו Docker Compose production מנהל secrets וenvironment variables, ומה הסכנה שאתה מזהה מניסיון Python שלך כאשר מישהו מכניס secrets ישירות ל-compose.yml?

מחבר ל:
Python for DevOps

מוכן לבחינה?

בצע הערכה תיאורטית, תרגול CLI ושאלות חיבור כדי לסיים את הנושא.