dockernginxnext.jsnestjs

Getting real client IP Address instead of Nginx Container's IP Address on Dockerized Next.js and Nestjs app | X-Forwarded-For


I have Next.js 13 (as frontend) and Nestjs (as API) both Dockerized and reverse proxied using nginx-proxy image, here is my docker-compose.yml:

 api:
    image: gidgud/my:api
    container_name: api
    ports:
      - '6900:6999'
    networks:
      - mynetwork
    environment:
      - VIRTUAL_HOST=api.mywebsite.com
      - VIRTUAL_PORT=6999

  web:
    image: gidgud/my:web
    container_name: web
    expose:
      - '3000'
    networks:
      - mynetwork
    depends_on:
      - api
    environment:
      - VIRTUAL_HOST=www.mywebsite.com,mywebsite.com
      - VIRTUAL_PORT=3000

  nginx-proxy:
    image: 'nginxproxy/nginx-proxy:latest'
    container_name: 'nginx-proxy'
    volumes:
      - 'html:/usr/share/nginx/html'
      - 'dhparam:/etc/nginx/dhparam'
      - 'vhost:/etc/nginx/vhost.d'
      - 'certs:/etc/nginx/certs'
      - 'conf:/etc/nginx/conf.d'
      - '/var/run/docker.sock:/tmp/docker.sock:ro'
      - './custom_proxy.conf:/etc/nginx/conf.d/custom_proxy.conf'
    depends_on:
      - web
      - api
    networks:
      - mynetwork
    ports:
      - '80:80'
      - '443:443'

networks:
  mynetwork:
    name: my_network
    external: true

My web request to api using the VM IP Address with the api's exposed port (I'm trying to point it to api.mywebsite.com instead, but Next.js giving TIMEOUT error when doing server request to it, that's why for now I point it to the VM IP Address).

Need to know: my web is not directly requesting to api's IP Address but it goes through Next.js rewrites first. So I 'wrap' the api's IP Address with Next.js rewrites feature.

Okay above is my website setup and here comes the main problem.

My api cannot receive the real client IP Address that comes from web request. Instead, it always receives nginx-proxy container's IP Address. Moreover, when web doing server request to the api (Next.js server component), the api receives the Docker network default gateway IP Address instead.

Beside that, I've tried to deploy a different website on Vercel without Docker and try to send request to the api (api.mywebsite.com) and my api can correctly receive the real client IP Address. According to this findings, I suspect the problem is either from the Docker or Next.js setup.

Question: How do I setup Next.js/Docker so that when it request to api it will also send at least the X-Forwarded-For header? (for real client IP Address)

Thank you and for your answers ^^


Solution

  • After surfing and testing many times, here I come up with this solution.

    Nestjs

    In order to receive end user real IP, need to activate trust proxy setting like this:

    // main.ts
    async function bootstrap() {
      const app = await NestFactory.create<NestExpressApplication>(AppModule);
      // If your Next.js (client) on the same server/IP with the Nest.js use this
      app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
      // If your Next.js (client) on different server/IP from the Nest.js use this
      // app.set('trust proxy', true);
      ...
    }
    

    Got the Reference when trying to rate limit my app: https://docs.nestjs.com/security/rate-limiting#proxies

    Next.js (v13.5.4)

    1. Next.js rewrites by default doesn't forward end user headers, so I need to append them manually like this:
    // middleware.ts
    export const config = {
      matcher: '/my-api/:path*',
    };
    
    export function middleware(req: NextRequest) {
      const baseUrl = process.env.API_URL;
    
      const requestHeaders = new Headers(req.headers);
    
      const url = req.nextUrl.clone();
      url.protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
      url.pathname = url.pathname.replace(/^\/my-api/, '');
      url.href = baseUrl + url.pathname + '?' + url.searchParams;
    
      return NextResponse.rewrite(url, {
        request: {
          headers: requestHeaders,
        },
      });
    }
    

    Other options beside using Next.js rewrites would be using http-proxy-middleware package to create custom proxy to api.

    1. For server request, Next.js fetch by default also doesn't forward/include end user headers. So I need to append them in every fetch request like this:
    import { headers } from "next/headers";
    
    ...
    
    const response = await fetch(`${baseUrl}/api${path}`, {
      ...options,
      headers: Object.fromEntries(headers())
    });
    

    Reference about this server request headers: https://github.com/vercel/next.js/issues/47793

    Nginx

    As reverse proxy, to make sure it forwards end user real IP to your web app, need to add these lines in Nginx config:

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
    proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
    proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl;
    proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
    

    In my case, I use nginx-proxy docker image that already have above config by default so I don't need to add extra configuration to my Nginx container.

    Hopefully this helps! And I still open for other solution, suggestion, and answer, Thanks!