J’ai toujours été fasciné par le service Cloud RUN de Google qui réussit à réduire le nombre de conteneurs qui tourne à 0 et tout de même servir 100% des requêtes HTTP qu’il reçoit. Ce service repose enfaite sur une technologie qui s’appelle KNative , et qui permet de reproduire ce comportement sur un cluster Kubernetes.
Dans cet article, je vous propose de créer ensemble un petit script NodeJS, capable de recevoir des requêtes HTTP, et de démarrer un conteneur si nécessaire.
Commençons donc par créer un simple serveur http qui répond Hello world
et construisons une image Docker à partir de ça:
import http from "http";
const hostname = "0.0.0.0";
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader("Content-Type", "text/plain");
res.end("Hello world\n");
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
FROM node:22
WORKDIR /app
COPY ./server.js ./server.js
CMD ["node", "server.js"]
docker build . -t simple-server:local
Pour notre petit script, notre image s’appelle donc simple-server:local
.
Voici donc le petit script NodeJS très basique qui répondra à notre besoin:
import http from "node:http";
import { proxyRequest, isPortOpen, startContainer } from "./utils.js";
const CONTAINER_PORT = 8080;
const httpServer = http.createServer(async (req, res) => {
console.log(
"A new request comes in. We need to check if the docker container is running"
);
const isContainerRunning = await isPortOpen(CONTAINER_PORT);
if (!isContainerRunning) {
console.log("Container is not running. Starting it now...");
await startContainer();
}
console.log("Proxying the request to the container");
return proxyRequest({
req,
res,
port: CONTAINER_PORT,
host: "localhost",
});
});
httpServer.listen(3000, "127.0.0.1");
La fonction la plus intéressante ici est startContainer
.
import http from "node:http";
import { spawn } from "node:child_process";
export async function startContainer({ port, image }) {
// We first start our container
spawn("docker", ["run", "--rm", "-p", `${port}:3000`, "simple-server:local"], {
stdio: "inherit",
});
while (true) {
console.log(`checking if port ${port} is open`);
if (await isPortOpen(port)) {
console.log(`port ${port} is open now`);
break;
}
console.log(
`port ${port} is not open yet. Waiting for 100ms before retrying`
);
await wait(100); // We wait 100ms before retrying
}
// We should handle container startup errors
// We should add a container startup timeout
}
La fonction telle qu’elle est écrite ici fonctionne.
Et voilà! On a attendu de lancer notre conteneur avant de servir la requête!
Lorsque l’on lance la commande:
curl http://localhost:3000
On reçoit bien Hello World
. La première fois en constatant un “cold start”, et la seconde fois beaucoup plus rapidement.
Si ça vous intéresse d’aller plus loin, dîtes moi en commentaire et j’écrirai la logique pour couper le conteneur au bout d’un certain temps, ou encore pour bien gérer les requêtes lorsque le conteneur rencontre une erreur!
En supplément, je vous laisse également la fonction permettant de savoir si un port est ouvert:
export function isPortOpen(port, host = "127.0.0.1") {
return new Promise((resolve) => {
const socket = new net.Socket();
socket.setTimeout(1000); // Set timeout to avoid long waiting times
socket.once("connect", () => {
socket.destroy();
resolve(true);
});
socket.once("timeout", () => {
socket.destroy();
resolve(false);
});
socket.once("error", (err) => {
socket.destroy();
resolve(false);
});
socket.connect(port, host);
});
}