Dockerfile i docker build — pravljenje sopstvenog image-a
Do sada si u Dockeru koristio tuđe image-e — povukao docker pull-om i pokrenuo docker run-om. Da bi spakovao svoju aplikaciju, treba ti sopstveni image.
Dockerfile je tekstualni fajl sa uputstvom kako se image pravi.
Komanda docker build to uputstvo izvršava i pravi image.
Zašto ne docker commit
Image se može napraviti i tako što uđeš u kontejner, ručno instaliraš šta ti treba i sačuvaš stanje sa docker commit. Radi — ali niko ne zna šta je unutra, ne može se ponoviti, i ne može se držati u Git-u.
Dockerfile je recept: čitljiv, verzionisan i ponovljiv. Svako ko ga ima dobija identičan image.
Prvi Dockerfile
Napravi direktorijum i u njemu fajl imena Dockerfile — bez
ekstenzije, sa velikim D:
$ mkdir moja-app && cd moja-app $ vi Dockerfile
FROM rockylinux:9 RUN dnf install -y httpd && dnf clean all COPY index.html /var/www/html/ EXPOSE 80 CMD ["httpd", "-D", "FOREGROUND"]
Napravi i index.html:
<h1>Radi!</h1>
Sagradi image:
$ docker build -t moja-app:1.0 .
Tačka na kraju je kontekst gradnje — direktorijum čiji sadržaj Docker može da kopira. Nije ukras; bez nje komanda ne radi.
Pokreni:
$ docker run -d -p 8080:80 --name web moja-app:1.0 $ curl localhost:8080 <h1>Radi!</h1>
Instrukcije
FROM — osnovni image
FROM rockylinux:9 FROM python:3.12-slim FROM node:20-alpine
Uvek prva instrukcija. Bira osnovu na kojoj gradiš.
Nikad ne piši latest. Image koji si sagradio danas i
onaj za tri meseca biće različiti, a ti nećeš znati zašto je nešto prestalo
da radi. Uvek navedi verziju.
Nastavci slim i alpine označavaju smanjene
varijante. Alpine je najmanji, ali koristi drugu C biblioteku (musl umesto
glibc) — neki softver na njemu radi drugačije ili ne radi uopšte.
RUN — izvršava komandu pri gradnji
RUN dnf install -y httpd && dnf clean all
Izvršava se jednom, dok se image pravi. Ovde ide instalacija paketa, kompajliranje, priprema direktorijuma.
COPY — ubacuje fajlove u image
COPY index.html /var/www/html/ COPY src/ /app/src/ COPY . /app
Postoji i ADD, koji ume da raspakuje arhive i preuzima sa
URL-a. To „ume" je razlog da ga izbegavaš — ponašanje je manje predvidivo.
Koristi COPY, osim ako ti baš treba automatsko raspakivanje.
WORKDIR — radni direktorijum
WORKDIR /app COPY . . RUN npm install
Važi za sve naredne instrukcije. Bolje nego RUN cd /app,
jer cd u jednom RUN-u ne utiče na sledeći.
ENV — promenljive okruženja
ENV APP_MODE=production ENV PATH="/app/bin:$PATH"
Vidljive i pri gradnji i u pokrenutom kontejneru.
Za vrednosti potrebne samo pri gradnji koristi ARG:
ARG VERZIJA=1.0 RUN echo "Gradim verziju $VERZIJA"
$ docker build --build-arg VERZIJA=2.0 -t moja-app:2.0 .
Nikad ne stavljaj lozinke ni ključeve ni u ENV ni u
ARG. Ostaju zapisani u slojevima image-a i vide se sa
docker history. Tajne se prosleđuju pri pokretanju.
EXPOSE — dokumentacija porta
EXPOSE 80
Ne otvara port. Samo govori onome ko koristi image koji port aplikacija
sluša. Stvarno objavljivanje radi -p uz
docker run.
USER — ne radi kao root
RUN useradd -r -u 1001 app USER app
Podrazumevano kontejner radi kao root, što nije potrebno i predstavlja
nepotreban rizik. Sve instrukcije posle USER izvršavaju se kao
taj korisnik.
CMD i ENTRYPOINT
Ovo dvoje najviše zbunjuje, pa polako.
CMD zadaje podrazumevanu komandu koja se
lako zamenjuje:
CMD ["httpd", "-D", "FOREGROUND"]
$ docker run moja-app # pokreće httpd $ docker run moja-app /bin/bash # pokreće bash umesto toga
ENTRYPOINT zadaje komandu koja se uvek izvršava, a
argumenti se dodaju na nju:
ENTRYPOINT ["ping"] CMD ["-c", "3", "localhost"]
$ docker run moja-app # ping -c 3 localhost $ docker run moja-app google.com # ping google.com
Praktično pravilo: ako je image jedan alat, koristi
ENTRYPOINT plus CMD za podrazumevane argumente.
Ako je servis, dovoljan je CMD.
Oblik sa zagradama, ne bez
CMD ["httpd", "-D", "FOREGROUND"] # exec oblik — ovako CMD httpd -D FOREGROUND # shell oblik — izbegavaj
U shell obliku komanda se pokreće preko /bin/sh -c, pa tvoj
proces nije PID 1 i ne dobija signale. Posledica:
docker stop čeka deset sekundi pa nasilno ubija kontejner
umesto da se aplikacija uredno ugasi.
Slojevi i keš
Svaka instrukcija pravi novi sloj. Docker ih kešira i pri ponovnoj gradnji preskače sve do prve promene:
$ docker build -t moja-app:1.1 . => CACHED [2/4] RUN dnf install -y httpd => [3/4] COPY index.html /var/www/html/
Odavde sledi najvažnije pravilo pisanja Dockerfile-a: ono što se retko menja stavi gore, ono što se često menja stavi dole.
Loše:
FROM python:3.12-slim WORKDIR /app COPY . . RUN pip install -r requirements.txt
Svaka izmena bilo kog fajla poništava keš i pip install se
vrti ispočetka.
Dobro:
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . .
Sad se zavisnosti ponovo instaliraju samo kad se promeni
requirements.txt. Razlika je između gradnje od dve sekunde i
gradnje od dva minuta, svaki put.
Spajanje RUN instrukcija
Ovo ostavlja keš paketa u image-u zauvek:
RUN dnf install -y httpd RUN dnf clean all
Drugi sloj briše fajlove, ali oni i dalje postoje u prvom — slojevi se samo dodaju. Zato brisanje mora biti u istom sloju:
RUN dnf install -y httpd && \
dnf clean all && \
rm -rf /var/cache/dnf
.dockerignore
Docker pri gradnji šalje ceo kontekst — sve iz direktorijuma sa tačkom.
Ako je tu i .git i node_modules, to su stotine
megabajta pre nego što gradnja uopšte počne.
Napravi .dockerignore pored Dockerfile-a:
.git .gitignore node_modules __pycache__ *.log *.md .env Dockerfile
Sintaksa je ista kao kod .gitignore. Obavezno tu stavi i
.env i sve sa tajnama, da ne završe u image-u.
Višefazna gradnja
Za kompajlirane jezike ti alati za gradnju ne trebaju u gotovom image-u. Višefazna gradnja gradi u jednom image-u, a u finalni prenosi samo rezultat:
# Faza 1 — gradnja FROM golang:1.22 AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o /app/server ./cmd/server # Faza 2 — finalni image FROM alpine:3.19 RUN adduser -D -u 1001 app COPY --from=builder /app/server /usr/local/bin/server USER app EXPOSE 8080 ENTRYPOINT ["/usr/local/bin/server"]
Ključan je COPY --from=builder — uzima fajl iz prethodne
faze. Finalni image nema ni Go kompajler ni izvorni kod, pa umesto
800 MB ima 15 MB.
Rad sa image-ima
$ docker build -t moja-app:1.0 . $ docker build -t moja-app:1.0 -f Dockerfile.prod . $ docker build --no-cache -t moja-app:1.0 .
Više oznaka odjednom:
$ docker build -t moja-app:1.2 -t moja-app:latest .
Šta je nastalo:
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE moja-app 1.0 a3f8c2d91b04 2 minutes ago 271MB
Slojevi i komande od kojih je image nastao:
$ docker history moja-app:1.0
Ovo je i način da proveriš da nisi slučajno ugradio nešto osetljivo.
Comments
Post a Comment