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

Popular posts from this blog

Početak u Linuxu — šta je i zašto se uči

Konverzija tipova podataka u Pythonu

groupadd