Βέλτιστες πρακτικές


Ακολούθησε τις επίσημες βέλτιστες πρακτικές

Οι επίσημες βέλτιστες πρακτικές για το Dockerfile έχουν πολύ χρήσιμο υλικό για το πώς να βελτιώσεις τα Dockerfiles σου.

Απόδοση

Θα πρέπει να βελτιστοποιείς κυρίως ως προς την απόδοση (ειδικά για τους test runners). Έτσι θα διασφαλίσεις ότι τα εργαλεία σου τρέχουν όσο το δυνατόν γρηγορότερα και δεν ξεπερνούν το χρονικό όριο.

Μέτρησε

Το να μετράς συχνά τον χρόνο εκτέλεσης είναι ένας εξαιρετικός τρόπος να αποκτήσεις αίσθηση της απόδοσης των εργαλείων. Κάνε συνήθεια να μετράς τον χρόνο εκτέλεσης και μετά και πριν από μια αλλαγή. Ακόμα κι όταν νιώθεις "σίγουρος" ότι μια αλλαγή θα βελτιώσει την απόδοση, θα πρέπει και πάλι να μετράς τον χρόνο εκτέλεσης.

Σενάρια

Όταν είναι δυνατόν, δημιούργησε σενάρια που μετρούν αυτόματα την απόδοση (αυτό είναι γνωστό και ως benchmarking). Ένα πολύ χρήσιμο εργαλείο γραμμής εντολών είναι το hyperfine, αλλά μη διστάσεις να χρησιμοποιήσεις ό,τι βγάζει περισσότερο νόημα για τα εργαλεία σου.

Τα νεότερα αποθετήρια εργαλείων track έχουν πρόσβαση στα εξής δύο σενάρια:

  1. ./bin/benchmark.sh: μετρά την απόδοση του κώδικα των εργαλείων του track (πηγαίος κώδικας)
  2. ./bin/benchmark-in-docker.sh: μετρά την απόδοση του Docker image των εργαλείων του track (πηγαίος κώδικας)
Note

Αν δουλεύεις σε ένα αποθετήριο εργαλείων track χωρίς αυτά τα αρχεία, μη διστάσεις να τα αντιγράψεις στο αποθετήριό σου χρησιμοποιώντας τους παραπάνω συνδέσμους με τον πηγαίο κώδικα.

Caution

Τα σενάρια μέτρησης απόδοσης μπορούν να βοηθήσουν στην εκτίμηση της απόδοσης των εργαλείων. Να έχεις όμως υπόψη σου ότι η απόδοση στους παραγωγικούς διακομιστές του Exercism είναι συχνά χαμηλότερη.

Πειραματίσου με διαφορετικά βασικά images

Δοκίμασε να πειραματιστείς με διαφορετικά βασικά images (π.χ. Alpine αντί για Ubuntu), για να δεις αν το ένα αποδίδει (σημαντικά) καλύτερα από το άλλο. Αν η απόδοση είναι περίπου ίδια, διάλεξε το image που είναι το μικρότερο.

Δοκίμασε το δίκτυο internal

Έλεγξε αν η χρήση του δικτύου internal αντί για none βελτιώνει την απόδοση. Δες τα έγγραφα για το δίκτυο για περισσότερες πληροφορίες.

Προτίμησε εντολές χρόνου κατασκευής αντί για εντολές χρόνου εκτέλεσης

Τα εργαλεία του track τρέχουν ένα μεμονωμένο, βραχύβιο Docker container που εκτελεί τα παρακάτω βήματα.

  1. Δημιουργείται ένα Docker container.
  2. Το Docker container εκτελείται με τα σωστά ορίσματα.
  3. Το Docker container καταστρέφεται.

Επομένως, ο κώδικας που εκτελείται στο βήμα 2 εκτελείται σε κάθε εκτέλεση των εργαλείων. Για αυτόν τον λόγο, το να μειώσεις την ποσότητα κώδικα που εκτελείται στο βήμα 2 είναι ένας εξαιρετικός τρόπος να βελτιώσεις την απόδοση. Ένας τρόπος να το κάνεις αυτό είναι να μετακινήσεις κώδικα από τον χρόνο εκτέλεσης στον χρόνο κατασκευής. Ενώ ο κώδικας χρόνου εκτέλεσης τρέχει σε κάθε εκτέλεση των εργαλείων, ο κώδικας χρόνου κατασκευής τρέχει μόνο μία φορά (όταν κατασκευάζεται το Docker image).

Ο κώδικας χρόνου κατασκευής τρέχει μία φορά, ως μέρος μιας ροής εργασίας του GitHub Actions. Επομένως, δεν πειράζει αν ο κώδικας που τρέχει στον χρόνο κατασκευής είναι (σχετικά) αργός.

Παράδειγμα: προμεταγλώττιση βιβλιοθηκών

Όταν τρέχουν οι δοκιμές στο test runner της Haskell, απαιτείται η μεταγλώττιση κάποιων βασικών βιβλιοθηκών. Επειδή κάθε εκτέλεση δοκιμών γίνεται σε ένα καινούργιο container, αυτό σημαίνει ότι η μεταγλώττιση γινόταν σε κάθε εκτέλεση δοκιμών! Για να το αποφύγει αυτό, το Dockerfile του test runner της Haskell έχει τις εξής δύο εντολές:

COPY pre-compiled/ .
RUN stack build --resolver lts-20.18 --no-terminal --test --no-run-tests

Πρώτα, ο κατάλογος pre-compiled αντιγράφεται στο image. Ο κατάλογος αυτός έχει στηθεί ως άσκηση δοκιμών και εξαρτάται από τις ίδιες βασικές βιβλιοθήκες από τις οποίες εξαρτάται και η πραγματική άσκηση. Στη συνέχεια τρέχουμε τις δοκιμές σε αυτόν τον κατάλογο, κάτι που μοιάζει με τον τρόπο που τρέχουν οι δοκιμές για μια πραγματική άσκηση. Το τρέξιμο των δοκιμών έχει ως αποτέλεσμα να μεταγλωττιστεί η βάση, με τη διαφορά ότι αυτό συμβαίνει στον χρόνο κατασκευής. Έτσι, το Docker image που προκύπτει θα έχει τις βασικές βιβλιοθήκες του ήδη μεταγλωττισμένες. Αυτό σημαίνει ότι δεν χρειάζεται μεταγλώττιση στον χρόνο εκτέλεσης, με αποτέλεσμα (πολύ) ταχύτερη εκτέλεση.

Παράδειγμα: προμεταγλώττιση δυαδικών αρχείων

Κάποιες γλώσσες επιτρέπουν τη μεταγλώττιση κώδικα εκ των προτέρων (ahead-of-time) ή τη στιγμή της εκτέλεσης (just-in-time). Πρόκειται για έναν συμβιβασμό χρόνου κατασκευής εναντίον χρόνου εκτέλεσης, και, όπως και πριν, προτιμάμε την εκτέλεση στον χρόνο κατασκευής για λόγους απόδοσης.

Το Dockerfile του test runner της C# χρησιμοποιεί αυτήν την προσέγγιση: ο test runner μεταγλωττίζεται σε δυαδικό αρχείο εκ των προτέρων (στον χρόνο κατασκευής) αντί να μεταγλωττίζεται ο κώδικας τη στιγμή της εκτέλεσης (στον χρόνο εκτέλεσης). Αυτό σημαίνει ότι μένει λιγότερη δουλειά για τον χρόνο εκτέλεσης, κάτι που βοηθά να αυξηθεί η απόδοση.

Μέγεθος

Θα πρέπει να προσπαθήσεις να μειώσεις το μέγεθος του image, κάτι που σημαίνει ότι αυτό θα:

  • Το deploy γίνεται πιο γρήγορο
  • Μειώνει το κόστος για εμάς
  • Βελτιώνει τον χρόνο εκκίνησης κάθε container

Δοκίμασε διαφορετικές διανομές

Οι διαφορετικές διανομές images έχουν διαφορετικά μεγέθη. Για παράδειγμα, το image alpine:3.20.2 είναι δέκα φορές μικρότερο από το image ubuntu:24.10:

REPOSITORY   TAG       SIZE
alpine       3.20.2    8.83MB
ubuntu       24.10     101MB

Γενικά, τα images που βασίζονται στο Alpine είναι από τα μικρότερα, γι' αυτό πολλά images εργαλείων βασίζονται στο Alpine.

Δοκίμασε ελαφρύτερα images

Κάποια images έχουν ειδικές παραλλαγές "slim", στις οποίες έχουν αφαιρεθεί κάποιες λειτουργίες, με αποτέλεσμα μικρότερα μεγέθη image. Για παράδειγμα, το image node:20.16.0-slim είναι πέντε φορές μικρότερο από το image node:20.16.0:

REPOSITORY   TAG            SIZE
node         20.16.0        1.09GB
node         20.16.0-slim   219MB

Ο λόγος που οι παραλλαγές "slim" είναι μικρότερες είναι ότι έχουν λιγότερες λειτουργίες. Το δικό σου image μπορεί να μη χρειάζεται τις επιπλέον λειτουργίες, και αν όχι, σκέψου να χρησιμοποιήσεις την παραλλαγή "slim".

Αφαίρεσε τα περιττά κομμάτια

Ένας προφανής, αλλά εξαιρετικός, τρόπος να μειώσεις το μέγεθος του image σου είναι να αφαιρέσεις ό,τι δεν χρειάζεσαι. Τέτοια πράγματα μπορεί να είναι:

  • Αρχεία πηγαίου κώδικα που δεν χρειάζονται πια αφού κατασκευαστεί από αυτά ένα δυαδικό αρχείο
  • Αρχεία που αφορούν διαφορετικές αρχιτεκτονικές από αυτήν του Docker image
  • Τεκμηρίωση

Αφαίρεσε τα αρχεία του διαχειριστή πακέτων

Τα περισσότερα Docker images χρειάζεται να εγκαταστήσουν επιπλέον πακέτα, κάτι που συνήθως γίνεται μέσω ενός διαχειριστή πακέτων. Αυτά τα πακέτα πρέπει να εγκαθίστανται στον χρόνο κατασκευής (καθώς δεν υπάρχει σύνδεση στο διαδίκτυο στον χρόνο εκτέλεσης). Επομένως, τυχόν αρχεία προσωρινής αποθήκευσης ή τήρησης στοιχείων του διαχειριστή πακέτων θα πρέπει να αφαιρούνται μετά την εγκατάσταση των επιπλέον πακέτων.

apk

Οι διανομές που χρησιμοποιούν τον διαχειριστή πακέτων apk (όπως το Alpine) θα πρέπει να χρησιμοποιούν τη σημαία --no-cache όταν χρησιμοποιούν το apk add για να εγκαταστήσουν πακέτα:

RUN apk add --no-cache curl
apt-get/apt

Οι διανομές που χρησιμοποιούν τον διαχειριστή πακέτων apt-get/apk (όπως το Ubuntu) θα πρέπει να εκτελούν τις εντολές apt-get autoremove -y και rm -rf /var/lib/apt/lists/* αφού εγκαταστήσουν τα πακέτα, μέσα στην ίδια εντολή RUN:

RUN apt-get update && \
    apt-get install curl -y && \
    apt-get autoremove -y && \
    rm -rf /var/lib/apt/lists/*

Χρησιμοποίησε κατασκευές πολλαπλών σταδίων

Το Docker έχει μια λειτουργία που ονομάζεται κατασκευές πολλαπλών σταδίων. Σου επιτρέπουν να χωρίσεις το Dockerfile σου σε ξεχωριστά στάδια, με μόνο το τελευταίο στάδιο να καταλήγει στο Docker image που παράγεται (τα υπόλοιπα υπάρχουν μόνο για να υποστηρίξουν την κατασκευή του τελευταίου σταδίου). Μπορείς να σκεφτείς κάθε στάδιο ως ένα μικρό Dockerfile από μόνο του· τα στάδια μπορούν να χρησιμοποιούν διαφορετικά βασικά images.

Οι κατασκευές πολλαπλών σταδίων είναι ιδιαίτερα χρήσιμες όταν το Dockerfile σου απαιτεί την εγκατάσταση πακέτων που χρειάζονται μόνο στον χρόνο κατασκευής. Σε αυτήν την περίπτωση, η γενική δομή του Dockerfile σου μοιάζει με αυτή:

  1. Όρισε ένα νέο στάδιο (θα το ονομάσουμε "build"). Αυτό το στάδιο θα χρησιμοποιηθεί μόνο στον χρόνο κατασκευής.
  2. Εγκατέστησε τα απαιτούμενα επιπλέον πακέτα (στο στάδιο "build").
  3. Εκτέλεσε τις εντολές που χρειάζονται τα επιπλέον πακέτα (μέσα στο στάδιο "build").
  4. Όρισε ένα νέο στάδιο (θα το ονομάσουμε "runtime"). Αυτό το στάδιο θα αποτελέσει το Docker image που προκύπτει και εκτελείται στον χρόνο εκτέλεσης.
  5. Αντίγραψε το/τα αποτελέσματα από τις εντολές που εκτελέστηκαν στο βήμα 3 (στο στάδιο "build") σε αυτό το στάδιο (το στάδιο "runtime").

Με αυτήν τη ρύθμιση, τα επιπλέον πακέτα εγκαθίστανται μόνο στο στάδιο "build" και όχι στο στάδιο "runtime", πράγμα που σημαίνει ότι δεν θα καταλήξουν στο Docker image που παράγεται.

Παράδειγμα: λήψη αρχείων

Ο test runner της Fortran χρειάζεται το curl για να κατεβάσει κάποια αρχεία. Ωστόσο, το image του χρόνου εκτέλεσης δεν χρειάζεται το curl, γεγονός που το κάνει ιδανική περίπτωση χρήσης για μια κατασκευή πολλαπλών σταδίων.

Πρώτα, το Dockerfile του ορίζει ένα στάδιο (με το όνομα "build") στο οποίο εγκαθίσταται το πακέτο curl. Στη συνέχεια χρησιμοποιεί το curl για να κατεβάσει αρχεία σε αυτό το στάδιο.

FROM alpine:3.15 AS build

RUN apk add --no-cache curl

WORKDIR /opt/test-runner
COPY bust_cache .

WORKDIR /opt/test-runner/testlib
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/testlib/CMakeLists.txt
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/testlib/TesterMain.f90

WORKDIR /opt/test-runner
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/config/CMakeLists.txt

Το δεύτερο μέρος του Dockerfile ορίζει ένα νέο στάδιο και αντιγράφει τα αρχεία που κατέβηκαν από το στάδιο "build" στο δικό του στάδιο χρησιμοποιώντας την εντολή COPY:

FROM alpine:3.15

RUN apk add --no-cache coreutils jq gfortran libc-dev cmake make

WORKDIR /opt/test-runner
COPY --from=build /opt/test-runner/ .

COPY . .
ENTRYPOINT ["/opt/test-runner/bin/run.sh"]
Παράδειγμα: εγκατάσταση βιβλιοθηκών

Ο test runner της Ruby χρειάζεται να εγκατασταθούν τα πακέτα git, openssh, build-base, gcc και wget πριν μπορέσουν να εγκατασταθούν οι απαιτούμενες βιβλιοθήκες του (gems). Το Dockerfile του ξεκινά με ένα στάδιο (με το όνομα build) που εγκαθιστά αυτά τα πακέτα (μέσω apk add) και στη συνέχεια εγκαθιστά τις εξαρτήσεις (μέσω bundle install):

FROM ruby:3.2.2-alpine3.18 AS build

RUN apk update && apk upgrade && \
    apk add --no-cache git openssh build-base gcc wget git

COPY Gemfile Gemfile.lock .

RUN gem install bundler:2.4.18 && \
    bundle config set without 'development test' && \
    bundle install

Στη συνέχεια ορίζει το στάδιο που θα αποτελέσει το Docker image που προκύπτει. Αυτό το στάδιο δεν εγκαθιστά τις εξαρτήσεις που εγκατέστησε το προηγούμενο στάδιο· αντ' αυτού χρησιμοποιεί την εντολή COPY για να αντιγράψει τις εγκατεστημένες βιβλιοθήκες από το στάδιο build στο δικό του στάδιο:

FROM ruby:3.2.2-alpine3.18

RUN apk add --no-cache bash

WORKDIR /opt/test-runner

COPY --from=build /usr/local/bundle /usr/local/bundle

COPY . .

ENTRYPOINT [ "sh", "/opt/test-runner/bin/run.sh" ]
Note

Το Dockerfile του test runner της C# κάνει κάτι παρόμοιο, μόνο που σε αυτήν την περίπτωση το στάδιο build μπορεί να χρησιμοποιήσει ένα υπάρχον Docker image που έχει προεγκατεστημένα τα επιπλέον πακέτα που απαιτούνται για την εγκατάσταση βιβλιοθηκών.

Δοκιμές

Χρησιμοποίησε δοκιμές ολοκλήρωσης

Οι δοκιμές μονάδας μπορεί να είναι πολύ χρήσιμες, αλλά προτείνουμε να επικεντρωθείς στη συγγραφή δοκιμών ολοκλήρωσης. Το κύριο πλεονέκτημά τους είναι ότι δοκιμάζουν καλύτερα πώς τρέχουν τα εργαλεία στην παραγωγή, και έτσι βοηθούν να αυξηθεί η εμπιστοσύνη σου στην υλοποίηση των εργαλείων σου.

Χρησιμοποίησε το Docker

Για να μιμηθούν όσο καλύτερα γίνεται το παραγωγικό περιβάλλον, οι δοκιμές ολοκλήρωσης θα πρέπει να τρέχουν τα εργαλεία όπως το παραγωγικό περιβάλλον. Αυτό σημαίνει να κατασκευάσεις το Docker image και μετά να τρέξεις το image που κατασκευάστηκε σε μια λύση για να επαληθεύσεις την έξοδό του.

Χρησιμοποίησε δοκιμές golden

Οι δοκιμές ολοκλήρωσης θα πρέπει να ορίζονται ως δοκιμές golden, δηλαδή δοκιμές όπου η αναμενόμενη έξοδος αποθηκεύεται σε ένα αρχείο. Αυτό είναι ιδανικό για δοκιμές ολοκλήρωσης εργαλείων track, καθώς η έξοδος των εργαλείων είναι επίσης αρχεία.

Παράδειγμα: test runner

Όταν τρέχεις τον test runner σε μια λύση, η έξοδός του είναι ένα αρχείο results.json. Μπορούμε τότε να συγκρίνουμε αυτό το αρχείο με ένα "γνωστά σωστό" (δηλαδή "αναμενόμενο") αρχείο εξόδου (με το όνομα expected_results.json) για να ελέγξουμε αν ο test runner λειτουργεί όπως πρέπει.

Ασφάλεια

Η ασφάλεια είναι ένας βασικός λόγος για τον οποίο χρησιμοποιούμε Docker containers για να τρέχουμε τα εργαλεία μας.

Προτίμησε επίσημα images

Υπάρχουν πολλά Docker images στο Docker Hub, αλλά προσπάθησε να χρησιμοποιείς επίσημα. Αυτά τα images είναι επιμελημένα και έχουν (πολύ) λιγότερες πιθανότητες να είναι ανασφαλή.

Κλείδωσε τις εκδόσεις

Για να διασφαλίσεις ότι οι κατασκευές είναι σταθερές (δηλαδή δεν σπάνε ξαφνικά), θα πρέπει πάντα να κλειδώνεις τα βασικά images σου σε συγκεκριμένες ετικέτες. Αυτό σημαίνει ότι αντί για:

FROM alpine:latest

θα πρέπει να χρησιμοποιείς:

FROM alpine:3.20.2

Με το δεύτερο, οι κατασκευές θα χρησιμοποιούν πάντα την ίδια έκδοση.

Τρέξε ως μη προνομιούχος χρήστης

Από προεπιλογή, πολλά images τρέχουν με έναν χρήστη που έχει δικαιώματα root. Θα πρέπει να εξετάσεις το ενδεχόμενο να τρέχεις ως μη προνομιούχος χρήστης.

FROM alpine

RUN groupadd -r myuser && useradd -r -g myuser myuser

# RUN <COMMANDS THAT REQUIRE ROOT USER, E.G. INSTALLING PACKAGES>

USER myuser

Ενημέρωσε τα αποθετήρια πακέτων στην τελευταία έκδοση

Είναι (σχεδόν) πάντα καλή ιδέα να εγκαθιστάς τις τελευταίες εκδόσεις

RUN apt-get update && \
    apt-get install curl

Υποστήριξε σύστημα αρχείων μόνο για ανάγνωση

Ενθαρρύνουμε τα Dockerfiles να γράφονται με χρήση συστήματος αρχείων μόνο για ανάγνωση. Οι μόνες διαδρομές που θα πρέπει να θεωρείς ότι είναι εγγράψιμες είναι:

  • Η διαδρομή της λύσης (που περνιέται ως δεύτερο όρισμα)
  • Η διαδρομή εξόδου (που περνιέται ως τρίτο όρισμα)
  • Η διαδρομή /tmp
Caution

Το παραγωγικό μας περιβάλλον αυτή τη στιγμή δεν επιβάλλει σύστημα αρχείων μόνο για ανάγνωση, αλλά μπορεί να το κάνει στο μέλλον. Για αυτόν τον λόγο, το βασικό πρότυπο για έναν νέο test runner/analyzer/representer ξεκινά με σύστημα αρχείων μόνο για ανάγνωση. Αν δεν μπορείς να κάνεις τα πράγματα να δουλέψουν σε ένα αρχείο μόνο για ανάγνωση, μη διστάσεις (για την ώρα) να υποθέσεις ένα εγγράψιμο σύστημα αρχείων.