4

So I have a Next.js React component that is using getInitialProps to call the backend for some data before initial page load. Here is what that looks like:

 static async getInitialProps({req, query}){
    console.log('inside getInitialProps')
    if (req){
      var pinReturn = await axios.post(process.env.serverADD+'getPinData', {withCredentials: true})
      .then(response=>{
        console.log('value of response: ', response)
        return response.data.pins
      })
      .catch(error=>{
        console.log('value of error: ', error)
        return({})
      })

    }
    return {pinData: pinReturn}
  }

This is pretty self explanatory I think - and it works when my backend is a simple docker container. However, now I am using nginx and docker-compose to spin up my front and back end. Currently my process.env.serverADD is equal to http://localhost:80/back/.

My NGINX is :

server {
    listen 80;
    listen [::]:80;

    root /var/www/html;
    index index.html index.htm index.nginx-debian.html;

    server_name example.com www.example.com localhost;

    location /back {
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_set_header X-NginX-Proxy true;

      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";

      proxy_pass http://nodejs:8000;
    }

    location / {
      proxy_pass http://nextjs:3000;
    }

    location ~ /.well-known/acme-challenge {
      allow all;
      root /var/www/html;
    }
}

And here is my docker-compose (this is similar to what is here: https://www.digitalocean.com/community/tutorials/how-to-secure-a-containerized-node-js-application-with-nginx-let-s-encrypt-and-docker-compose#step-2-%E2%80%94-defining-the-web-server-configuration):

version: '3'

services:
  nodejs:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    container_name: nodejs
    restart: unless-stopped
    networks:
      - app-network
  nextjs:
    build:
      context: ../.
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    container_name: nextjs
    restart: unless-stopped
    networks:
      - app-network
  webserver:
    image: nginx:mainline-alpine
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
    depends_on:
      - nodejs
      - nextjs
    networks:
      - app-network

volumes:
  certbot-etc:
  certbot-var:
  web-root:
    driver: local
    driver_opts:
      type: none
      device: /
      o: bind

networks:
  app-network:
    driver: bridge 

If I call http://localhost:80/back/getPinData from curl then I get the data I'm expecting. It routes it through nginx, hits my docker-compose, and then routes to the correct location in my nodejs app. However, if I call that API from getInitialProps I get the following error:

inside getInitialProps
value of error:  { Error: connect ECONNREFUSED 127.0.0.1:80
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1104:14)
  errno: 'ECONNREFUSED',
  code: 'ECONNREFUSED',
  syscall: 'connect',
  address: '127.0.0.1',
  port: 80,
  config:
   { adapter: [Function: httpAdapter],
     transformRequest: { '0': [Function: transformRequest] },
     transformResponse: { '0': [Function: transformResponse] },
     timeout: 0,
     xsrfCookieName: 'XSRF-TOKEN',
     xsrfHeaderName: 'X-XSRF-TOKEN',
     maxContentLength: -1,
     validateStatus: [Function: validateStatus],
     headers:
      { Accept: 'application/json, text/plain, */*',
        'Content-Type': 'application/json;charset=utf-8',
        'User-Agent': 'axios/0.18.0',
        'Content-Length': 24 },
     method: 'post',
     url: 'http://localhost:80/back/getPinData',
     data: '{"withCredentials":true}' },
  request:
   Writable {
     _writableState:
      WritableState {
        objectMode: false,
        highWaterMark: 16384,
        finalCalled: false,
        needDrain: false,
        ending: false,
        ended: false,
        finished: false,
        destroyed: false,
        decodeStrings: true,
        defaultEncoding: 'utf8',
        length: 0,
        writing: false,
        corked: 0,
        sync: true,
        bufferProcessing: false,
        onwrite: [Function: bound onwrite],
        writecb: null,
        writelen: 0,
        bufferedRequest: null,
        lastBufferedRequest: null,
        pendingcb: 0,
        prefinished: false,
        errorEmitted: false,
        emitClose: true,
        bufferedRequestCount: 0,
        corkedRequestsFree: [Object] },
     writable: true,
     _events:
      [Object: null prototype] {
        response: [Function: handleResponse],
        error: [Function: handleRequestError] },
     _eventsCount: 2,
     _maxListeners: undefined,
     _options:
      { maxRedirects: 21,
        maxBodyLength: 10485760,
        protocol: 'http:',
        path: '/back/getPinData',
        method: 'post',
        headers: [Object],
        agent: undefined,
        auth: undefined,
        hostname: 'localhost',
        port: '80',
        nativeProtocols: [Object],
        pathname: '/back/getPinData' },
     _ended: false,
     _ending: true,
     _redirectCount: 0,
     _redirects: [],
     _requestBodyLength: 24,
     _requestBodyBuffers: [ [Object] ],
     _onNativeResponse: [Function],
     _currentRequest:
      ClientRequest {
        _events: [Object],
        _eventsCount: 6,
        _maxListeners: undefined,
        output: [],
        outputEncodings: [],
        outputCallbacks: [],
        outputSize: 0,
        writable: true,
        _last: true,
        chunkedEncoding: false,
        shouldKeepAlive: false,
        useChunkedEncodingByDefault: true,
        sendDate: false,
        _removedConnection: false,
        _removedContLen: false,
        _removedTE: false,
        _contentLength: null,
        _hasBody: true,
        _trailer: '',
        finished: false,
        _headerSent: true,
        socket: [Socket],
        connection: [Socket],
        _header:
         'POST /back/getPinData HTTP/1.1\r\nAccept: application/json, text/plain, */*\r\nContent-Type: application/json;charset=utf-8\r\nUser-Agent: axios/0.18.0\r\nContent-Length: 24\r\nHost: localhost\r\nConnection: close\r\n\r\n',
        _onPendingData: [Function: noopPendingOutput],
        agent: [Agent],
        socketPath: undefined,
        timeout: undefined,
        method: 'POST',
        path: '/back/getPinData',
        _ended: false,
        res: null,
        aborted: undefined,
        timeoutCb: null,
        upgradeOrConnect: false,
        parser: null,
        maxHeadersCount: null,
        _redirectable: [Circular],
        [Symbol(isCorked)]: false,
        [Symbol(outHeadersKey)]: [Object] },
     _currentUrl: 'http://localhost:80/back/getPinData' },
response: undefined }

What is going on here? Why does getInitialProps appear to work when not using NGINX, but fails when using the reverse-proxy?

Peter Weyand
  • 2,159
  • 9
  • 40
  • 72
  • It is not related to Next, if you will do the same thing anywhere in the node you probably will get the same result. I'm not an expert but sounds like localhost inside the docker is not nginx, if I'm not wrong, there is an option to expose other dockerized service inside the node one. – felixmosh Apr 10 '19 at 17:01
  • I'm sorry, I'm not sure if I follow - all other calls to localhost work, only failing when called from getInitialProps. "If I do the same thing anywhere in node, you will get the same result" doesn't appear to be correct. What do you think I should change to get the above to work (concretely if possible)? – Peter Weyand Apr 10 '19 at 17:03
  • Check this out: https://stackoverflow.com/questions/30545023/how-to-communicate-between-docker-containers-via-hostname – felixmosh Apr 10 '19 at 17:04
  • There's a lot there - I'm not sure this addresses my question. – Peter Weyand Apr 10 '19 at 17:05

1 Answers1

7

When resolving URLs from within your container, localhost is no longer your actual host machine. Instead, it's the loopback adapter of the container itself.

If you're trying to call the Nginx server from within the Docker container, you need to use http://webserver:80/back/getPinData.

You've already set up your docker-compose.yml to use a bridge network, so this should work right away.

sjagr
  • 15,983
  • 5
  • 40
  • 67
  • Absolutely brilliant thank you. Not sure why I didn't see that. – Peter Weyand Apr 10 '19 at 17:12
  • @sjagr is there a way to call using the public URL from within the container? Since getInitialProps may be called on client when acting as single page application, http://webserver:80/back/getPinData will definitely not work from client browser. Any solutions? – atulmy Dec 30 '19 at 16:54
  • @AtulYadav you would need to expose the port to the host machine and fetch using the host IP. There's various blog posts and SO questions that address "fetching host machine IP from docker container" - so I won't go into this. However, to solve the Next.js specific problem, there's likely a flag to detect when you're in server-side or client-side code to switch out the hostname. Perhaps [this will help](https://stackoverflow.com/questions/49411796/how-do-i-detect-i-am-on-server-vs-client-in-next-js). – sjagr Jan 02 '20 at 20:20
  • @sjagr hey thanks for the reply. I was facing that issue on DigitalOcean Ubuntu + Docker distribution container where the Docker container was not able to call any URL pointing IP address of HOST machine. However I tried with CentOS container and manually installed Docker + Docker Compose and did not find any issue. SSR works just fine. I am using github.com/jaredpalmer/razzle though. – atulmy Jan 02 '20 at 21:21