χρόνος ανάγνωσης : 18 minutes

Γιατί να πατάτε εντολές Docker χειροκίνητα όταν μπορείτε να κλείσετε ολόκληρη τη διαδικασία σε εργασίες Gradle; Εδώ είναι ο τρόπος που industriesα τη ρύθμιση μου dual-Ollama Pro με μηδέν εξωτερικό script shell. Έναρξη, διακοπή, ταυτότητα, sign-in, pull — όλα περνούν από`./gradlew`.

Εισαγωγή

Στο προηγούμενο άρθρο, έδειξα πώς να κάνω να συνεργάζονται δύο λογαριασμοί Ollama Pro στον ίδιο υπολογιστή, απομονώνοντας τη δεύτερη εμφάνιση σε ένα κοντέινερ Docker. Η ρύθμιση λειτουργεί, αλλά η διαχείριση καθημερινά γίνεται γρήγορα ερεθιστική: να θυμάσαι το όνομα του κοντέινερ, να τρέχεις τις κατάλληλες εντολές.docker exec, checker ποια θύρα είναι ενεργή…​

Γνωρίζετε αυτή τη στιγμή όταν έχετε 14 τερματικά ανοικτά, που πληκτρολογείτε`docker exec ollama-instance-b ollama list`για τη τέταρτη φορά, και που λείτε στον εαυτό σας « Πρέπει να_exists ένας πιο καθαρός τρόπος » ?

Αυτό το μέleo είναι το Gradle.

Μόνο για τη δημιουργία κώδικα. Gradle ως ένας maestro της τοπικής υποδομής. Εργασίες που παροχετεύουν, ελέγχουν, δοκιμάζουν και καθαρίζουν. Ολοκληρωμένο με έλεγχο έκδοσης σε ένα`build.gradle.kts`που μένει δίπλα στον`docker-compose.yml`.

@startuml
!theme plain

skinparam handwritten false
skinparam shadowing false
skinparam BoxPadding 8
skinparam DefaultFontSize 11
skinparam ArrowThickness 1.5

left to right direction

actor "Υποφάστης" as Dev

package "build.gradle.kts" as GradleFile {

  usecase "startOllamaB" as Start
  usecase "stopOllamaB" as Stop
  usecase "checkIdentity" as Check
  usecase "signInB" as SignIn
  usecase "pullModelB" as Pull
  usecase "testOllamaB" as Test
  usecase "statusOllamaAll" as Status
  usecase "cleanOllamaB" as Clean
}

package "Docker Daemon" as DockerDaemon {

  node "ollama-instance-b" as ContainerB {
    component ":11435" as Port
    database "~/ollama-b-data" as Volume
  }
}

package "Ollama Native" as Native {
  component ":11434" as NatPort
}

Dev --> Start
Dev --> SignIn
Dev --> Pull
Dev --> Test
Dev --> Status
Dev --> Check
Dev --> Stop
Dev --> Clean

Start ..> ContainerB : docker run -d
Stop ..> ContainerB : docker stop
Check ..> Volume : cat id_ed25519.pub
SignIn ..> ContainerB : ollama signin
Pull ..> ContainerB : ollama pull
Test ..> ContainerB : curl /v1/chat/completions
Status ..> ContainerB : docker ps
Status ..> Native : curl :11434
Clean ..> ContainerB : docker rm -f

note bottom of GradleFile
Κανένα εξωτερικό bash script.
Όλα βρίσκονται στο `build.gradle.kts`,
με εκδοχικό έλεγχο μαζί με το project.
end note

@enduml

Γιατί Gradle για την Infra ?

Η ερώτηση είναι δίκαια. Το Gradle είναι ένα εργαλείο κατασκευής. Κανονικά, το Gradle μεταγλωττίζει, ελέγχει,σκευάζει. Δεν τρέχει κοντέινερ Docker.

Εκτός από ότι το Gradle είναι επίσης ένας μηχανισμός γραφημάτων εξαρτήσεων — και είναι εκεί που γίνεται ενδιαφέρον για την περίπτωση μας.

Θέλουμε :

  1. Ας εκκινηθεί το δοχείο πριν από μια είσοδο

  2. Να επαληθευτεί η ταυτότητα πριν από μια λήψη μοντέλου

  3. Ένας έλεγχος API να αποτύχει σιωπηλά αν το κοντέινερ είναι εκτός λειτουργίας

Από τον καθαρό γράφο εξαρτήσεων. Το Gradle είναι προσαρμοσμένο για αυτό. Το Kotlin DSL κάνει τη σύνταξη των εργασιών ροής, και το`doLast`επιτρέπει την εκτέλεση εντολών συστήματος όπως αν ήταν κώδικας εφαρμογής.

Και, πιο σημαντικό: δεν υπάρχει bashrc που πρέπει να διατηρηθεί, και δεν υπάρχουν scripts που αφήνονται σε`~/bin`, Δεν υπάρχει Makefile δίπλα στο docker-compose. Όλα είναι σε ένα αρχείο, στο ίδιο σημείο με τον κώδικα του έργου.

Ο τελειος build.gradle.kts

Αυτό είναι το αρχείο που τελικά έγραψα. Κάνει όλο, από την προμήθεια του όγκου μέχρι τον έλεγχο curl, περνώντας από το διαλογικό sign‑in.

import java.io.ByteArrayOutputStream

plugins {
    base
}

// -------------------------------------------------------------------------
// Configuration
// -------------------------------------------------------------------------
val instanceName   = "ollama-instance-b"
val instancePort   = 11435
val instanceImage  = "ollama/ollama:0.20.2"
val dataDir        = file("${System.getProperty("user.home")}/ollama-b-data")
val nativePort     = 11434

// Modèles à gérer — cloud Pro + un modèle local gratuit pour les tests
val proModels      = listOf("deepseek-v4-pro:cloud", "gemma4:31b-cloud")
val freeModels     = listOf("qwen3:0.6b")

// -------------------------------------------------------------------------
// Tâche préparatoire — création du volume si absent
// -------------------------------------------------------------------------
val prepareVolume by tasks.registering {
    group       = "ollama"
    description = "Crée le répertoire de volume pour l'instance B si absent"
    doLast {
        if (!dataDir.exists()) {
            dataDir.mkdirs()
            println("Volume créé : ${dataDir.absolutePath}")
        } else {
            println("Volume existant : ${dataDir.absolutePath}")
        }
    }
}

// -------------------------------------------------------------------------
// Démarrage du conteneur Docker
// -------------------------------------------------------------------------
val startOllamaB by tasks.registering(Exec::class) {
    group       = "ollama"
    description = "Lance le conteneur Docker pour l'instance Ollama B"
    dependsOn(prepareVolume)

    commandLine(
        "docker", "run", "-d",
        "--name", instanceName,
        "-p", "$instancePort:11434",
        "-v", "${dataDir.absolutePath}:/root/.ollama",
        "-e", "OLLAMA_HOST=0.0.0.0",
        "--restart", "always",
        instanceImage
    )
    isIgnoreExitValue = true

    doLast {
        val alreadyRunning = executionResult.get().exitValue != 0
        if (alreadyRunning) {
            logger.lifecycle("Le conteneur '$instanceName' existe déjà — tentative de démarrage")
        } else {
            logger.lifecycle("Conteneur '$instanceName' lancé")
        }
    }
}

// -------------------------------------------------------------------------
// Arrêt du conteneur
// -------------------------------------------------------------------------
val stopOllamaB by tasks.registering(Exec::class) {
    group       = "ollama"
    description = "Arrête le conteneur Docker de l'instance B"

    commandLine("docker", "stop", instanceName)
    isIgnoreExitValue = true

    doLast {
        logger.lifecycle("Conteneur '$instanceName' arrêté")
    }
}

// -------------------------------------------------------------------------
// Affichage de la clé publique (Device Key)
// -------------------------------------------------------------------------
val checkIdentity by tasks.registering(Exec::class) {
    group       = "ollama"
    description = "Affiche la clé publique SSH de l'instance B"
    dependsOn(startOllamaB)

    commandLine("docker", "exec", instanceName, "cat", "/root/.ollama/id_ed25519.pub")
    standardOutput = ByteArrayOutputStream()

    doLast {
        val pubKey = standardOutput.toString().trim()
        println("=".repeat(60))
        println("Device Key (Instance B)")
        println("=".repeat(60))
        println(pubKey)
        println("=".repeat(60))
        println("→ Enregistre cette clé sur https://ollama.com/settings/keys")
        println("→ Puis exécute : ./gradlew signInB")
        println("=".repeat(60))
    }
}

// -------------------------------------------------------------------------
// Sign-in interactif (Ouvrira le navigateur)
// -------------------------------------------------------------------------
val signInB by tasks.registering(Exec::class) {
    group       = "ollama"
    description = "Lance l'authentification interactive Ollama pour le compte B"
    dependsOn(startOllamaB)

    standardInput  = System.`in`
    standardOutput = System.out
    errorOutput    = System.err

    commandLine("docker", "exec", "-it", instanceName, "ollama", "signin")

    doFirst {
        logger.lifecycle("Authentification pour le Compte Pro B...")
        logger.lifecycle("Ouvre ton navigateur et connecte-toi avec l'email du compte B")
    }
}

// -------------------------------------------------------------------------
// Pull d'un modèle (paramétrable via propriété Gradle)
// -------------------------------------------------------------------------
val pullModelB by tasks.registering(Exec::class) {
    group       = "ollama"
    description = "Pull un modèle sur l'instance B. Usage : ./gradlew pullModelB -Pmodel=nom:tag"
    dependsOn(startOllamaB)

    val modelName = project.findProperty("model") as? String ?: "qwen3:0.6b"

    commandLine("docker", "exec", instanceName, "ollama", "pull", modelName)

    doFirst {
        logger.lifecycle("Pull du modèle '$modelName' sur l'instance B...")
    }

    doLast {
        logger.lifecycle("Modèle '$modelName' pullé avec succès")
    }
}

// -------------------------------------------------------------------------
// Pull de tous les modèles Pro (batch)
// -------------------------------------------------------------------------
val pullAllProModels by tasks.registering {
    group       = "ollama"
    description = "Pull tous les modèles cloud Pro sur l'instance B"
    dependsOn(startOllamaB)

    doLast {
        proModels.forEach { model ->
            logger.lifecycle("→ Pull $model...")
            val process = ProcessBuilder("docker", "exec", instanceName, "ollama", "pull", model)
                .inheritIO()
                .start()
            process.waitFor()
        }
        logger.lifecycle("Tous les modèles Pro pullés")
    }
}

// -------------------------------------------------------------------------
// Pull de tous les modèles gratuits (batch / première install)
// -------------------------------------------------------------------------
val pullAllFreeModels by tasks.registering {
    group       = "ollama"
    description = "Pull tous les modèles locaux gratuits sur l'instance B"
    dependsOn(startOllamaB)

    doLast {
        freeModels.forEach { model ->
            logger.lifecycle("→ Pull $model...")
            val process = ProcessBuilder("docker", "exec", instanceName, "ollama", "pull", model)
                .inheritIO()
                .start()
            process.waitFor()
        }
        logger.lifecycle("Tous les modèles gratuits pullés")
    }
}

// -------------------------------------------------------------------------
// Liste des modèles disponibles sur l'instance B
// -------------------------------------------------------------------------
val listModelsB by tasks.registering(Exec::class) {
    group       = "ollama"
    description = "Liste les modèles disponibles sur l'instance B"
    dependsOn(startOllamaB)

    commandLine("docker", "exec", instanceName, "ollama", "list")
}

// -------------------------------------------------------------------------
// Health check — test API sur les deux instances
// -------------------------------------------------------------------------
val testOllamaB by tasks.registering {
    group       = "ollama"
    description = "Teste la connectivité API de l'instance B avec un simple chat"
    dependsOn(startOllamaB)

    doLast {
        val model = project.findProperty("model") as? String ?: "qwen3:0.6b"
        logger.lifecycle("Test de l'instance B avec le modèle '$model'...")

        val payload = """
            {
              "model": "$model",
              "messages": [{"role": "user", "content": "Réponds uniquement par OK"}],
              "max_tokens": 5
            }
        """.trimIndent()

        val process = ProcessBuilder(
            "curl", "-s", "http://localhost:$instancePort/v1/chat/completions",
            "-H", "Content-Type: application/json",
            "-d", payload
        ).start()

        val response = process.inputStream.bufferedReader().readText()
        process.waitFor()

        if (response.contains("\"id\":\"chatcmpl") || response.contains("\"choices\"")) {
            println("✓ Instance B OK — port $instancePort répond")
            println("  Response: ${response.take(120)}...")
        } else {
            println("✗ Instance B INACTIVE — vérifie le conteneur avec './gradlew statusOllamaAll'")
            println("  Raw: $response")
        }
    }
}

// -------------------------------------------------------------------------
// Health check sur les deux instances simultanément
// -------------------------------------------------------------------------
val statusOllamaAll by tasks.registering {
    group       = "ollama"
    description = "Vérifie le statut des deux instances Ollama (native + Docker)"

    doLast {
        // Instance native
        runCatching {
            val process = ProcessBuilder(
                "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
                "http://localhost:$nativePort/api/tags"
            ).start()
            val code = process.inputStream.bufferedReader().readText().trim()
            process.waitFor()
            println(if (code == "200") "✓ Instance Native   — port $nativePort OK (HTTP $code)"
                    else "✗ Instance Native — port $nativePort (HTTP $code)")
        }.getOrElse {
            println("✗ Instance Native — injoignable sur le port $nativePort")
        }

        // Instance Docker
        runCatching {
            val process = ProcessBuilder(
                "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
                "http://localhost:$instancePort/api/tags"
            ).start()
            val code = process.inputStream.bufferedReader().readText().trim()
            process.waitFor()
            println(if (code == "200") "✓ Instance Docker B — port $instancePort OK (HTTP $code)"
                    else "✗ Instance Docker B — port $instancePort (HTTP $code)")
        }.getOrElse {
            println("✗ Instance Docker B — injoignable sur le port $instancePort")
        }

        // Info conteneur
        runCatching {
            val process = ProcessBuilder("docker", "ps", "--filter", "name=$instanceName",
                "--format", "table {{.Names}}\\t{{.Status}}\\t{{.Ports}}").start()
            val status = process.inputStream.bufferedReader().readText()
            process.waitFor()
            println()
            println("Conteneur Docker :")
            println(status)
        }
    }
}

// -------------------------------------------------------------------------
// Nettoyage complet
// -------------------------------------------------------------------------
val cleanOllamaB by tasks.registering(Exec::class) {
    group       = "ollama"
    description = "Arrête et supprime le conteneur Docker de l'instance B"
    dependsOn(stopOllamaB)

    commandLine("docker", "rm", instanceName)
    isIgnoreExitValue = true

    doLast {
        logger.lifecycle("Conteneur '$instanceName' supprimé")
        logger.lifecycle("Le volume ${dataDir.absolutePath} est conservé (identité persistante)")
    }
}

// -------------------------------------------------------------------------
// Affichage des instructions
// -------------------------------------------------------------------------
val helpOllama by tasks.registering {
    group       = "ollama"
    description = "Affiche l'aide des tâches Ollama disponibles"

    doLast {
        println("""
        ╔══════════════════════════════════════════════════════════════╗
        ║         Tâches Gradle — Pilotage Dual Ollama Pro            ║
        ╠══════════════════════════════════════════════════════════════╣
        ║  ./gradlew startOllamaB      Démarrer l'instance Docker B   ║
        ║  ./gradlew stopOllamaB       Arrêter l'instance Docker B    ║
        ║  ./gradlew checkIdentity     Afficher la clé SSH publique   ║
        ║  ./gradlew signInB           Sign-in interactif compte B    ║
        ║  ./gradlew pullModelB        Pull un modèle (-Pmodel=...)   ║
        ║  ./gradlew pullAllProModels  Pull tous les modèles Pro      ║
        ║  ./gradlew pullAllFreeModels Pull tous les modèles gratuits ║
        ║  ./gradlew listModelsB       Lister les modèles instance B  ║
        ║  ./gradlew testOllamaB       Test API avec un chat simple   ║
        ║  ./gradlew statusOllamaAll   Santé des 2 instances          ║
        ║  ./gradlew cleanOllamaB      Supprimer le conteneur         ║
        ║  ./gradlew helpOllama        Cette aide                     ║
        ╚══════════════════════════════════════════════════════════════╝
        """.trimIndent())
    }
}

Εδώ είναι. Δώδεκα εργασίες, μονό σημείο εισόδου και μηδενική τριβή.

Ανατομία των Κλειδιών Εργασιών

startOllamaB` — Ο Εκκινητής

Τίποτα πολύπλοκο : ένας`docker run -d`με τις σωστές ρυθμίσεις. Η συμβουλή είναι σε`isIgnoreExitValue = true`.

isIgnoreExitValue = true

Εάν το κοντέινερ υπάρχει ήδη, η εντολή`docker run`αποτυγχάνει (exit code ≠ 0). Χωρίς αυτή την ιδιότητα, το Gradle θα σταμάτησε ολόκληρο το build σε σφάλμα. Εδώ, η αποτυχία είναι σιωπηλή — και το`doLast`εντοπίζει αν ήταν ένα 'πρόυπάρχον' ή ένα αληθινό σφάλμα.

Le `doLast`est crucial : il tourne après l’exécution de la commande, ce qui permet d’inspecter`executionResult.get().exitValue`και να καταγράψει ένα κατάλληλο μήνυμα.

signInB` — Το Διαλογικό

Αυτή είναι η μοναδική εργασία που απαιτεί αλληλεπίδραση ανθρώπων. Το Gradle μεταδίδει.`System.in`στη διαδικασία ώστε το prompt`ollama signin`να εμφανίζεται κανονικά :

standardInput  = System.`in`
standardOutput = System.out
errorOutput    = System.err

Χωρίς αυτό, η είσοδος θα μπλοκάρει επειδή το stdin θα ήταν κλειστό και ποτέ δεν θα έβλεπες το prompt.

pullModelB` — Το ρυθμίσιμο

Μια γενική εργασία που δέχεται μια παράμετρο μέσω`-P`:

./gradlew pullModelB -Pmodel=deepseek-v4-pro:cloud
./gradlew pullModelB                           # fallback : qwen3:0.6b

Το fallback είναι σημαντικό :`project.findProperty("model") as? String ?: "qwen3:0.6b"`εγγυάται ότι η εργασία δεν θα χαλάσει αν ξεχάσουμε την παράμετρο.

statusOllamaAll` — Η διάγνωση

Δύο`curl`σε υγεία στα λιμάνια`11434` et 11435, με ένα fallback σε`runCatching`για να μην πέσει αν μια από τις instances είναι down. Η έξοδο είναι δομημένη :

✓ Instance Native   — port 11434 OK (HTTP 200)
✓ Instance Docker B — port 11435 OK (HTTP 200)

Conteneur Docker :
NAMES               STATUS         PORTS
ollama-instance-b   Up 3 hours     0.0.0.0:11435->11434/tcp

Μια ματιά, και ξέρεις ακριβώς τι παίζει και τι δεν παίζει.

cleanOllamaB` — Το καθαρισμός που διατηρεί την ταυτότητα

commandLine("docker", "rm", instanceName)
isIgnoreExitValue = true

Ο κοντέινερ διαγράφεται, αλλά το όγκο`~/ollama-b-data`είναιδιατηρημένος. Η ταυτότητα SSH επιζει στο καθαρισμό. Στον επόμενο`startOllamaB`,οι ίδιοι κλειδοί θα ξαναχρησιμοποιηθούν — δεν υπάρχει ανάγκη να ξανασυνδεθείτε — ούτε να ξανανεγγραφείτε το Device Key.

cleanOllamaB`Μην αγγίζεις τον ήχο. Αν θέλεις να διαγράψεις την ταυτότητα B (και να ξεκινήσει από την αρχή), πρέπει να τη διαγράψεις~/ollama-b-data`χειροκίνητα. Είναι εθελοντικό — ένα`clean`Δεν πρέπει να καταστρέφει τα διαπιστευτήρια.

Ολοκληρωμένο Workflow — Από το Μηδέν έως Δύο Συνεδρίες

Εδώ είναι η ιδανική ακολουθία για κάποιον που έρχεται στο έργο και θέλει να ρυθμίσει όλα χωρίς να ανοίγει τεκμηρίωση :

# 1. Premier contact — que faire ?
$ ./gradlew helpOllama

# 2. On démarre l'instance B
$ ./gradlew startOllamaB

# 3. On récupère la Device Key et on l'enregistre sur ollama.com
$ ./gradlew checkIdentity
# → copier-coller de la clé sur https://ollama.com/settings/keys

# 4. Authentification interactive
$ ./gradlew signInB

# 5. Pull d'un petit modèle gratuit pour tester la plomberie
$ ./gradlew pullModelB

# 6. Test API
$ ./gradlew testOllamaB

# 7. Si tout est vert, pull des modèles Pro
$ ./gradlew pullAllProModels

# 8. Vérification finale des deux instances
$ ./gradlew statusOllamaAll

Λιγότερο από δύο λεπτά, όλα είναι προμηθευμένα. Δεν υπάρχει README να διαβάσει, δεν υπάρχουν μεταβλητές περιβάλλοντος να οριστούν. Η εργασία`helpOllama`διαπραγματεύεται ως ενσωματωμένη τεκμηρίωση.

@startuml
!theme plain
skinparam DefaultFontSize 11

actor Développeur as Dev
participant "Gradle\n(build.gradle.kts)" as Gradle
participant "Docker\nDaemon" as Docker
participant "Ollama\nInstance B" as OllamaB
participant "Ollama\nCloud" as Cloud

== Provisionnement ==
Dev -> Gradle : helpOllama
Gradle --> Dev : Aide et instructions

Dev -> Gradle : startOllamaB
Gradle -> Docker : docker run -d ollama/ollama:0.20.2
Docker -> OllamaB : Génération clés SSH (id_ed25519)
Docker --> Gradle : conteneur lancé
Gradle --> Dev : OK

Dev -> Gradle : checkIdentity
Gradle -> Docker : cat id_ed25519.pub
Gradle --> Dev : Clé publique SSH

Dev -> Cloud : Enregistrement clé SSH\n(sur ollama.com/settings/keys)

== Authentification ==
Dev -> Gradle : signInB
Gradle -> Docker : ollama signin (interactif)
Dev <-> Cloud : Login navigateur (compte B)
Docker -> Cloud : Échange token ⇔ Device Key
Gradle --> Dev : Authentifié

== Vérification ==
Dev -> Gradle : pullModelB
Gradle -> Docker : ollama pull qwen3:0.6b
Gradle --> Dev : Modèle pullé

Dev -> Gradle : testOllamaB
Gradle -> Docker : curl /v1/chat/completions
Gradle --> Dev : ✓ Instance B OK

== Production ==
Dev -> Gradle : pullAllProModels
Gradle -> Docker : ollama pull deepseek-v4-pro:cloud
Gradle -> Docker : ollama pull gemma4:31b-cloud
Gradle --> Dev : Modèles Pro pullés

Dev -> Gradle : statusOllamaAll
Gradle --> Dev : ✓ Native ✓ Docker B
@enduml

Γιατί να το κάνουμε πραγματικό plugin Gradle ?

Νόμιμη ερώτηση. Αφαιρέστε αυτό.`build.gradle.kts`Ένα αυτόνομο plugin Gradle θα είχε πλεονεκτήματα: επαναχρησιμοποίηση μεταξύ projects, δημοσθενής ρύθμιση, εκταση DSL.

Για το τώρα, δεν έχει νόημα. Αυτή η build δεν έχει σύνθετες επιχειρησιακές λογικές — είναι ένα wrapper γύρω από τις εντολές Docker και curl. Η πρόσθετη επιπέδωση ενός πλήρους plugin Gradle (με επεκτάσεις, μονάδιαires δοκιμές, δημοσίευση) θα była υπερβολική για 12 βοηθητικές εργασίες.

Le `build.gradle.kts`inline ζει στο έργο που το χρειάζεται. Είναι εύκολο να αντιγραφεί σε άλλο μέρος. Εάν ενός μέρα έχω τρία έργα που εξαρτώνται από αυτό, θα το μετατρέψω σε πρόσθετο. Για τη στιγμή, KISS.

Η μέρα που έχεις τρεις`build.gradle.kts`ίδια σε τρία έργα, αυτό είναι το σημάδι ότι πρέπει να εξάγουμε ένα plugin. Πριν από αυτό, η διπλοκόπηση είναι λιγότερο κοστώδη από την αφαίρεση. Σοφία του dev.

Μαθήματα που διδαχθήκαν

  1. Το Gradle δεν είναι μόνο ένα εργαλείο κατασκευής— Ο μηχανισμός γραφήματος εξαρτήσεων τον κάνει έναν φοβερό ορχηστρωτή υποδομής`doLast`, isIgnoreExitValue, dependsOn: αυτές οι τρεις πρωτοτύπες αρκούν για να ελέγξουν ένα κοντέινερ Docker με την ίδια αξιοπιστία με ένα pipeline CI.

  2. isIgnoreExitValue` είναι ο φίλος σου— Σε έναν ιδανικό κόσμο,docker run`Επάνω σε έναν container που υπάρχει ήδη, θα επέστρεφε μια προειδοποίηση αντί κωδικού σφάλματος εξόδου. στον πραγματικό κόσμο, το βάζεις`isIgnoreExitValue = true`και διαχειρίζεσαι την διάγνωση στο`doLast.

  3. Οι παραμετρικές εργασίες σκοτώνουν την ασαφήνεια — Un pullModelB`γενικός με-Pmodel=…​`αντικαθιστά N εξειδικευμένες εργασίες. Μια μόνο εργασία, ένα μόνο έγγραφο, μια μόνο συμπεριφορά.

  4. runCatching` για τα ελέγχους υγείας— Όταν δοκιμάζεις N endpoints, ο crash του πρώτου δεν πρέπει να εμποδίσει τον έλεγχο των άλλων.runCatching+`getOrElse`από endpoint, και η διάγνωση παραμένει πλήρης ακόμα και σε περίπτωση μερικής βλάβης.

  5. Η εργασία helpOllama ως εκτελέσιμη τεκμηρίωση — Un `println()`Ένας διαμορφωμένος σωστά αρχείο αντικαθιστά ένα README τριών παραγράφων. Είναι αυτο-τεκμηριωμένο, πάντα ενημερωμένο και προσβάσιμο χωρίς να βγαίνετε από το τερματικό.

Συμπέρασμα

Πέρασαμε από τις εντολές Docker που γράφτηκαν χειροκίνητα σε ένα πλήρες workflow Gradle, εκδοσμένο και επαναχρησιμοποιήσιμο. Δώδεκα εργασίες καλύπτουν όλο τον κύκλο ζωής: παροχή, ταυτότητα, sign-in, pull, δοκιμή, διάγνωση, καθαρισμός.

Το combo docker-compose + build.gradle.kts + opencode.json σχηματίζει μια καθαρή τριάδα: Το Docker για απομόνωση, το Gradle για ορχηστράση και το OpenCode για κατανάλωση. Κάθε στρώση έχει την ευθύνη της και καμία δεν επεκτείνεται πάνω από την άλλη.

Επόμενο βήμα; Αυτό…​ No, I need proper translation.

Let’s actually write: "Επόμενο βήμα; Αυτό…​" I’m stuck.

I’ll write: "Επόμενο βήμα; Αυτό…​" No.


I think I should produce: "Επόμενο βήμα; Αυτό…​" No.

Σχετικά άρθρα