Οι επίσημες βέλτιστες πρακτικές για το Dockerfile έχουν πολύ χρήσιμο υλικό για το πώς να βελτιώσεις τα Dockerfiles σου.
Θα πρέπει να βελτιστοποιείς κυρίως ως προς την απόδοση (ειδικά για τους test runners). Έτσι θα διασφαλίσεις ότι τα εργαλεία σου τρέχουν όσο το δυνατόν γρηγορότερα και δεν ξεπερνούν το χρονικό όριο.
Το να μετράς συχνά τον χρόνο εκτέλεσης είναι ένας εξαιρετικός τρόπος να αποκτήσεις αίσθηση της απόδοσης των εργαλείων. Κάνε συνήθεια να μετράς τον χρόνο εκτέλεσης και μετά και πριν από μια αλλαγή. Ακόμα κι όταν νιώθεις "σίγουρος" ότι μια αλλαγή θα βελτιώσει την απόδοση, θα πρέπει και πάλι να μετράς τον χρόνο εκτέλεσης.
Όταν είναι δυνατόν, δημιούργησε σενάρια που μετρούν αυτόματα την απόδοση (αυτό είναι γνωστό και ως benchmarking). Ένα πολύ χρήσιμο εργαλείο γραμμής εντολών είναι το hyperfine, αλλά μη διστάσεις να χρησιμοποιήσεις ό,τι βγάζει περισσότερο νόημα για τα εργαλεία σου.
Τα νεότερα αποθετήρια εργαλείων track έχουν πρόσβαση στα εξής δύο σενάρια:
./bin/benchmark.sh: μετρά την απόδοση του κώδικα των εργαλείων του track (πηγαίος κώδικας)./bin/benchmark-in-docker.sh: μετρά την απόδοση του Docker image των εργαλείων του track (πηγαίος κώδικας)Αν δουλεύεις σε ένα αποθετήριο εργαλείων track χωρίς αυτά τα αρχεία, μη διστάσεις να τα αντιγράψεις στο αποθετήριό σου χρησιμοποιώντας τους παραπάνω συνδέσμους με τον πηγαίο κώδικα.
Τα σενάρια μέτρησης απόδοσης μπορούν να βοηθήσουν στην εκτίμηση της απόδοσης των εργαλείων. Να έχεις όμως υπόψη σου ότι η απόδοση στους παραγωγικούς διακομιστές του Exercism είναι συχνά χαμηλότερη.
Δοκίμασε να πειραματιστείς με διαφορετικά βασικά images (π.χ. Alpine αντί για Ubuntu), για να δεις αν το ένα αποδίδει (σημαντικά) καλύτερα από το άλλο. Αν η απόδοση είναι περίπου ίδια, διάλεξε το image που είναι το μικρότερο.
Έλεγξε αν η χρήση του δικτύου internal αντί για none βελτιώνει την απόδοση.
Δες τα έγγραφα για το δίκτυο για περισσότερες πληροφορίες.
Τα εργαλεία του track τρέχουν ένα μεμονωμένο, βραχύβιο 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, κάτι που σημαίνει ότι αυτό θα:
Οι διαφορετικές διανομές 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 έχουν ειδικές παραλλαγές "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 images χρειάζεται να εγκαταστήσουν επιπλέον πακέτα, κάτι που συνήθως γίνεται μέσω ενός διαχειριστή πακέτων. Αυτά τα πακέτα πρέπει να εγκαθίστανται στον χρόνο κατασκευής (καθώς δεν υπάρχει σύνδεση στο διαδίκτυο στον χρόνο εκτέλεσης). Επομένως, τυχόν αρχεία προσωρινής αποθήκευσης ή τήρησης στοιχείων του διαχειριστή πακέτων θα πρέπει να αφαιρούνται μετά την εγκατάσταση των επιπλέον πακέτων.
Οι διανομές που χρησιμοποιούν τον διαχειριστή πακέτων apk (όπως το Alpine) θα πρέπει να χρησιμοποιούν τη σημαία --no-cache όταν χρησιμοποιούν το apk add για να εγκαταστήσουν πακέτα:
RUN apk add --no-cache curl
Οι διανομές που χρησιμοποιούν τον διαχειριστή πακέτων 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 σου μοιάζει με αυτή:
Με αυτήν τη ρύθμιση, τα επιπλέον πακέτα εγκαθίστανται μόνο στο στάδιο "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" ]
Το Dockerfile του test runner της C# κάνει κάτι παρόμοιο, μόνο που σε αυτήν την περίπτωση το στάδιο build μπορεί να χρησιμοποιήσει ένα υπάρχον Docker image που έχει προεγκατεστημένα τα επιπλέον πακέτα που απαιτούνται για την εγκατάσταση βιβλιοθηκών.
Οι δοκιμές μονάδας μπορεί να είναι πολύ χρήσιμες, αλλά προτείνουμε να επικεντρωθείς στη συγγραφή δοκιμών ολοκλήρωσης. Το κύριο πλεονέκτημά τους είναι ότι δοκιμάζουν καλύτερα πώς τρέχουν τα εργαλεία στην παραγωγή, και έτσι βοηθούν να αυξηθεί η εμπιστοσύνη σου στην υλοποίηση των εργαλείων σου.
Για να μιμηθούν όσο καλύτερα γίνεται το παραγωγικό περιβάλλον, οι δοκιμές ολοκλήρωσης θα πρέπει να τρέχουν τα εργαλεία όπως το παραγωγικό περιβάλλον. Αυτό σημαίνει να κατασκευάσεις το Docker image και μετά να τρέξεις το image που κατασκευάστηκε σε μια λύση για να επαληθεύσεις την έξοδό του.
Οι δοκιμές ολοκλήρωσης θα πρέπει να ορίζονται ως δοκιμές golden, δηλαδή δοκιμές όπου η αναμενόμενη έξοδος αποθηκεύεται σε ένα αρχείο. Αυτό είναι ιδανικό για δοκιμές ολοκλήρωσης εργαλείων track, καθώς η έξοδος των εργαλείων είναι επίσης αρχεία.
Όταν τρέχεις τον test runner σε μια λύση, η έξοδός του είναι ένα αρχείο results.json.
Μπορούμε τότε να συγκρίνουμε αυτό το αρχείο με ένα "γνωστά σωστό" (δηλαδή "αναμενόμενο") αρχείο εξόδου (με το όνομα expected_results.json) για να ελέγξουμε αν ο test runner λειτουργεί όπως πρέπει.
Η ασφάλεια είναι ένας βασικός λόγος για τον οποίο χρησιμοποιούμε Docker containers για να τρέχουμε τα εργαλεία μας.
Υπάρχουν πολλά 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
Το παραγωγικό μας περιβάλλον αυτή τη στιγμή δεν επιβάλλει σύστημα αρχείων μόνο για ανάγνωση, αλλά μπορεί να το κάνει στο μέλλον. Για αυτόν τον λόγο, το βασικό πρότυπο για έναν νέο test runner/analyzer/representer ξεκινά με σύστημα αρχείων μόνο για ανάγνωση. Αν δεν μπορείς να κάνεις τα πράγματα να δουλέψουν σε ένα αρχείο μόνο για ανάγνωση, μη διστάσεις (για την ώρα) να υποθέσεις ένα εγγράψιμο σύστημα αρχείων.