Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get docker mapped ports from node.js application?

I would like to get mapped port from inside node.js application.

ex.

docker-compose:

my-app:
    build:
        context: ./my-app
    ports:
        - "80"     
    volumes:
        - /var/run/docker.sock:/tmp/docker.sock
    networks:
        - private

docker ps -a:

1234 my-app "/usr/local/bin/dock…"   About a minute ago   Up About a minute   0.0.0.0:33962->80/tcp

And I would like to get 33962 port from node.js app.

Any ideas?

like image 289
Niezborala Avatar asked Mar 26 '18 12:03

Niezborala


4 Answers

So the first thing is that currently, docker doesn't give the metadata information inside the container in any direct manner. There are workarounds to the same though

Now passing a docker socket inside your container is something that you can live with when it concerns a development environment but not prod environment

So what you want to do is build your own metadata service and then let it give the information back to your container without exposing the docker socket itself.

Now to get this metadata you can use two approaches

Metadata service based on container id

In this case, you create a simple endpoint like /metadata/container_id and then return the data you want.

This service can run on the main host itself or on the container which has the docker socket mapped.

Now in your NodeJS service, you will get the current container ID and then use the same to call this service. This can be done in different ways as listed in below thread

Docker, how to get container information from within the container?

The data returned you can restrict, in case I only want ports to be exposed, I will only return the detail of the ports only.

The problem with this approach is that you are still letting the service figure out its own container id and also at the same time trusting that it is sending the correct container id

Metadata service without passing any information

To get metadata information without passing information, we need to identify which pad made a call to the metadata server

This can be identified by using the source IP. Then we need to identify which container belongs to this IP. Once we have the container ID, we can return the metadata.

Below is a simple bash script that does the same

#!/bin/bash
CONTAINER_IP=$SOCAT_PEERADDR
CONTAINER_ID=$(docker ps -q | xargs docker inspect -f '{{.Id}}|{{range .NetworkSettings.Networks}}{{.IPAddress}}|{{end}}' | grep "|$CONTAINER_IP|" | cut -d '|' -f 1)
echo -e "HTTP/1.1 200 OK\r\nContent-Type: application/json;\r\nServer: $SOCAT_SOCKADDR:$SOCAT_SOCKPORT\r\nClient: $SOCAT_PEERADDR:$SOCAT_PEERPORT\r\nConnection: close\r\n";
METADATA=$(docker inspect -f '{{ json .NetworkSettings.Ports }}' $CONTAINER_ID)
echo $METADATA

Now to convert this to a web server we can use socat.

sudo socat -T 1 TCP-L:10081,pktinfo,reuseaddr,fork EXEC:./return_metadata.sh

Now inside the container when we call this metadata server

root@a9cf6dabdfb4:/# curl 192.168.33.100:10081
{"80/tcp":[{"HostIp":"0.0.0.0","HostPort":"8080"}]}

and the docker ps for the same

CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                  NAMES
a9cf6dabdfb4        nginx                 "nginx -g 'daemon of…"   3 hours ago         Up 3 hours          0.0.0.0:8080->80/tcp   nginx

Now how you create such a metadata server is upto you. You can use any approach

  • socat approach I showed
  • build one in nodejs
  • build one using https://github.com/avleen/bashttpd
  • build one using openresty

You can also add the IP of the service using extra_hosts in your docker-compose.yml and make the call like curl metaserver:10081

This should be a decently secure approach, without compromising on what you need

like image 71
Tarun Lalwani Avatar answered Nov 14 '22 20:11

Tarun Lalwani


You can use docker port for this.

docker port my-app 80

That will include the listener IP. If you need to strip that off, you can do that with the shell:

docker port my-app 80 | cut -f2 -d:

Unfortunately, this will only work with access to the docker socket. I wouldn't recommend mounting that socket for just this level of access inside your container.

Typically, most people solve this by passing a variable to their app and controlling what port is published in their docker-compose file. E.g.:

my-app:
    build:
        context: ./my-app
    ports:
        - "8080:80"     
    environment:
        - PUBLISHED_PORT=8080
    volumes:
        - /var/run/docker.sock:/tmp/docker.sock
    networks:
        - private

Then the app would look at the environment variable to reconfigure itself.

like image 33
BMitch Avatar answered Nov 14 '22 19:11

BMitch


I used a dockerode library for this.

Solution:

const Docker = require( 'dockerode' );
const os = require( 'os' );

const docker = new Docker( { socketPath: '/tmp/docker.sock' } );
const container = docker.getContainer( os.hostname() );

container.inspect( function( error, data ) {
    if ( error ) {
        return null;
    }

    const mappedPort = data.NetworkSettings.Ports[ '80/tcp' ][ 0 ].HostPort
} );
like image 25
Niezborala Avatar answered Nov 14 '22 20:11

Niezborala


What about using shelljs?

const shelljs = require('shelljs');

const output = shelljs.exec('docker ps --format {{.Ports}}', {silent: true}).stdout.trim();

console.log(output); // 80/tcp
                     // 19999/tcp
                     // 9000/tcp
                     // 8080/tcp, 50000/tcp

The output is a string, now let's map the response.

const shelljs = require('shelljs');

const output = shelljs.exec('docker ps --format {{.Ports}}', {silent: true}).stdout.trim();

const ports = output.split('\n').map(portList => portList.split(',').map(port => Number(port.split('/')[0].trim())));

This way ports return an array of arrays containing the ports:

[ [ 80 ], [ 19999 ], [ 9000 ], [ 8080, 50000 ] ]

In your case, you want the number between : and ->. So you can do this:

const shelljs = require('shelljs');

const output = shelljs
  .exec('docker ps --format {{.Ports}}', { silent: true })
  .stdout.trim();

const aoa = output.split('\n').map(portList => portList.split(','));

console.log(aoa); // [ [ '9000/tcp', ' 0.0.0.0:9000->123/tcp ' ], [ ' 80/tcp' ] ]

let ports = [];

aoa.forEach(arr => {
  arr.forEach(p => {
    // match strings which contains :PORT-> pattern;
    const match = p.match(/:(\d+)->/);
    if (match) {
      ports.push(Number(match[1]));
    }
  });
});

console.log(ports); // [ 9000 ]

Finally, you need to install docker inside your container and connect docker socks as explained here.

Update 29/06/2019:

As said by @Tarun Lalwani, if sharing docker socks is a problem, you may create an app which shares the network with your main app and has a GET method that returns ports.

like image 28
Pedro Arantes Avatar answered Nov 14 '22 21:11

Pedro Arantes