<p>I want to connect a React Native application using Socket.io to a server that is inside a Kubernetes Cluster hosted on Google Cloud Platform (GKE).</p>
<p>There seems to be an issue with the Nginx Ingress Controller declaration but I cannot find it.</p>
<p>I have tried adding <code>nginx.org/websocket-services</code>; rewriting my backend code so that it uses a separate NodeJS server (a simple HTTP server) on port 3004, then exposing it via the Ingress Controller under a different path than the one on port 3003; and multiple other suggestions from other SO questions and Github issues.</p>
<p>Information that might be useful:</p>
<ol>
<li>Cluster master version: <code>1.15.11-gke.15</code></li>
<li>I use a Load Balancer managed with Helm (<code>stable/nginx-ingress</code>) with RBAC enabled</li>
<li>All deployments and services are within the namespace <code>gitlab-managed-apps</code></li>
<li>The error I receive when trying to connect to socket.io is: <code>Error: websocket error</code></li>
</ol>
<p>For the front-end part, the code is as follows:</p>
<p><code>App.js</code></p>
<pre class="lang-js prettyprint-override"><code>const socket = io('https://example.com/app-sockets/socketns', {
    reconnect: true,
    secure: true,
    transports: ['websocket', 'polling']
});
</code></pre>
<p>I expect the above to connect me to a socket.io namespace called <code>socketdns</code>.</p>
<p>The backend code is:</p>
<p><code>app.js</code></p>
<pre class="lang-js prettyprint-override"><code>const express = require('express');
const app = express();
const server = require('http').createServer(app);
const io = require('socket.io')(server);
const redis = require('socket.io-redis');

io.set('transports', ['websocket', 'polling']);
io.adapter(redis({
    host: process.env.NODE_ENV === 'development' ? 'localhost' : 'redis-cluster-ip-service.gitlab-managed-apps.svc.cluster.local',
    port: 6379
}));
io.of('/').adapter.on('error', function(err) { console.log('Redis Adapter error! ', err); });

const nsp = io.of('/socketns');

nsp.on('connection', function(socket) {
    console.log('connected!');
});

server.listen(3003, () =&gt; {
    console.log('App listening to 3003');
});
</code></pre>
<p>The ingress service is:</p>
<p><code>ingress-service.yaml</code></p>
<pre class="lang-yaml prettyprint-override"><code>apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /$1
    nginx.ingress.kubernetes.io/proxy-body-size: &quot;100m&quot;
    certmanager.k8s.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/proxy-connect-timeout: &quot;7200&quot;
    nginx.ingress.kubernetes.io/proxy-read-timeout: &quot;7200&quot;
    nginx.ingress.kubernetes.io/proxy-send-timeout: &quot;7200&quot;
    nginx.org/websocket-services: &quot;app-sockets-cluster-ip-service&quot;
  name: ingress-service
  namespace: gitlab-managed-apps
spec:
  tls:
  - hosts:
    - example.com
    secretName: letsencrypt-prod
  rules:
  - host: example.com
    http:
      paths:
      - backend:
          serviceName: app-cms-cluster-ip-service
          servicePort: 3000
        path: /?(.*)
      - backend:
          serviceName: app-users-cluster-ip-service
          servicePort: 3001
        path: /app-users/?(.*)
      - backend:
          serviceName: app-sockets-cluster-ip-service
          servicePort: 3003
        path: /app-sockets/?(.*)
      - backend:
          serviceName: app-sockets-cluster-ip-service
          servicePort: 3003
        path: /app-sockets/socketns/?(.*)
</code></pre>

<p>The solution is to remove the <code>nginx.ingress.kubernetes.io/rewrite-target: /$1</code> annotation.</p>
<p>Here is a working configuration: (please note that apiVersion has changed since the question has been asked)</p>
<p><strong>Ingress configuration</strong></p>
<p><code>ingress-service.yaml</code></p>
<pre class="lang-yaml prettyprint-override"><code>apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/ssl-redirect: &quot;true&quot;
    nginx.ingress.kubernetes.io/force-ssl-redirect: &quot;true&quot;
    nginx.ingress.kubernetes.io/use-regex: &quot;true&quot;
    nginx.ingress.kubernetes.io/proxy-body-size: &quot;64m&quot;
    cert-manager.io/cluster-issuer: &quot;letsencrypt-prod&quot;
  name: ingress-service
  namespace: default
spec:
  tls:
  - hosts:
    - example.com
    secretName: letsencrypt-prod
  rules:
  - host: example.com
    http:
      paths:
      - backend:
          service:
            name: app-sockets-cluster-ip-service
            port:
              number: 3003
        path: /app-sockets/?(.*)
        pathType: Prefix
</code></pre>
<p><strong>On the service (Express.js):</strong></p>
<p><code>app.js</code></p>
<pre class="lang-js prettyprint-override"><code>const redisAdapter = require('socket.io-redis');

const io = require('socket.io')(server, {
    path: `${ global.NODE_ENV === 'development' ? '' : '/app-sockets' }/sockets/`,
    cors: {
        origin: '*',
        methods: ['GET', 'POST'],
    },
});

io.adapter(redisAdapter({
    host: global.REDIS_HOST,
    port: 6379,
}));

io.of('/').adapter.on('error', err =&gt; console.log('Redis Adapter error! ', err));

io.on('connection', () =&gt; { 
//...
});
</code></pre>
<p>The <code>global.NODE_ENV === 'development' ? '' : '/app-sockets'</code> bit is related to an issue in development. If you change it here, you must also change it in the snippet below.</p>
<p>In development the service is under <code>http://localhost:3003</code> (sockets endpoint is <code>http://localhost:3003/sockets</code>).</p>
<p>In production the service is under <code>https://example.com/app-sockets</code> (sockets endpoint is <code>https://example.com/app-sockets/sockets</code>).</p>
<p><strong>On frontend</strong></p>
<p><code>connectToWebsocketsService.js</code></p>
<pre class="lang-js prettyprint-override"><code>/**
 * Connect to a websockets service
 * @param tokens {Object}
 * @param successCallback {Function}
 * @param failureCallback {Function}
 */
export const connectToWebsocketsService = (tokens, successCallback, failureCallback) =&gt; {
    //SOCKETS_URL = NODE_ENV === 'development' ? 'http://localhost:3003' : 'https://example.com/app-sockets'
    const socket = io(`${ SOCKETS_URL.replace('/app-sockets', '') }`, {
        path: `${ NODE_ENV === 'development' ? '' : '/app-sockets' }/sockets/`,
        reconnect: true,
        secure: true,
        transports: ['polling', 'websocket'], //required
        query: {
            // optional
        },
        auth: {
            ...generateAuthorizationHeaders(tokens), //optional
        },
    });

    socket.on('connect', successCallback(socket));
    socket.on('reconnect', successCallback(socket));
    socket.on('connect_error', failureCallback);
};
</code></pre>
<p><em>Note: I wasn't able to do it on the project mentioned in the question, but I have on another project which is hosted <strong>on EKS, not GKE</strong>. Feel free to confirm if this works for you on GKE as well.</em></p>
