node.jsdockerexpressdocker-api

How can I stream a docker container log to Webbrowser using express without reloading the page?


What is the best way to stream my docker container log to my Webbrowser without refreshing using express and node-docker-api.

This is my current script.

app.get('/container/:id/log', async function(req, res){
    await checkTokenHeader(req, res);
    //todo: Check if Container exist
    let container = await getDockerContainer(req.params.id);

    await container.logs({
        follow: true,
        stdout: true,
        stderr: true
    }).then(async function(stream){
        res.setHeader('Content-Type', 'text/html; charset=utf-8');
        res.setHeader('Transfer-Encoding', 'chunked');
        console.log(stream)
        //stream.on('data', info => res.write(info.toString('utf-8').slice(8)))
        //stream.on('data', info => console.log(info.toString('utf-8').slice(8)))
        
        //I WANT TO STREAM THE OUTPUT TO Webbrowser or inside a div

    })
});

I tried using res.write or res.send, but nothing works


Solution

  • One possible way to achieve what you described is:

    1. Construct an HTML document with some client-side JavaScript to get the logs
    2. Establish a WebSocket connection to stream the logs to the client

    Here's a very simple implementation:

    // save this to "index.mjs" and run with "node ./index.mjs"
    
    import http from "http";
    
    import express from "express"; // npm i express
    import {
      Docker
    } from "node-docker-api"; // npm i node-docker-api
    import {
      WebSocketServer
    } from "ws"; // npm i ws
    
    const app = express();
    const docker = new Docker({
      socketPath: "/var/run/docker.sock"
    });
    
    app.get("/container/:id/log", async function(req, res) {
      res.setHeader("Content-Type", "text/html; charset=utf-8");
    
      // This is a naïve way of building an HTML document. 
      // You most likely want to to build this from a template
      res.end(`
        <!DOCTYPE html>
        <html>
          <head>
            <title>Container Logs</title>
          </head>
          <body>
            <h1>Container Logs</h1>
            <div id="logs"></div>
            <script>
              const ws = new WebSocket(\`ws://\${window.location.host}/${req.params.id}\`);
              ws.onmessage = (event) => {
                const log = document.createElement("p");
                log.innerText = event.data;
                document.getElementById("logs").appendChild(log);
                window.scrollTo(0, document.body.scrollHeight);
              };
            </script>
          </body>
        </html>
      `);
    });
    
    const server = http.createServer(app);
    const wss = new WebSocketServer({
      server
    });
    
    wss.on("connection", async(ws, req) => {
      const containerId = req.url.split("/")[1];
    
      // TODO: validate containerId
    
      ws.on("error", console.error);
    
      let container = docker.container.get(containerId);
      const stream = await container.logs({
        follow: true,
        stdout: true,
        stderr: true,
      });
    
      ws.once("close", () => {
        stream.removeAllListeners();
        stream.destroy();
      });
    
      stream.on("data", (info) => ws.send(info.toString("utf-8")));
    });
    
    server.listen(3000, () => {
      console.log("Server running on port 3000");
    });

    Running the code above with the following docker-compose.yml:

    version: "3.8"
    services:
      counter:
        image: busybox:latest
        init: true
        command: '/bin/sh -c "while true; do date; sleep 1; done"'
    

    Appends the log lines to the page without refreshing:

    enter image description here