Search code examples
node.jsnginxdocker-composeapollo-serverprisma

Secure websocket with Apollo Express, Nginx and docker-compose


Im trying to publish my first GraphQl project on a VPN, using docker-compose

It consists on a webapp, running on nodejs, and a GraphQl API, also running on nodejs, with Apollo Express and Prisma

The idea is to have the app and the API running on different containers and use a nginx container to proxy pass the requests to the right container (/ goes to the webapp, /api goes to the API)

I got it working, seems to be fine, but it needs to run on https. So Ive set up a letsencrypt certificate and set it on nginx and is working too, except for one thing: subscriptions

If I try to connect to the websocket using ws://mydomain/api, its refused cause the app is running on https. But if I try to connect on wss://mydomain/api, I get:

WebSocket connection to 'wss://mydomain/api' failed: Error during WebSocket handshake: Unexpected response code: 400

I read a lot of docs and tutorials and it seems to me Im doing right, but it just wont work and I dont know what to try anymore

Here is the relevant docker-compose.yml code:

version: "3"
services:
  api:
    build: 
      context: ./bin/api
    container_name: 'node10-api'
    restart: 'always'
    entrypoint: ["sh", "-c"]
    command: ["yarn && yarn prisma deploy && yarn prisma generate && yarn start"]
    restart: always
    ports:
      - "8383:8383"
    links: 
      - prisma
    volumes: 
      - /local/api:/api
  app:
    build: 
      context: ./bin/app
    container_name: 'node12-app'
    restart: 'always'
    entrypoint: ["sh", "-c"]
    command: ["yarn && yarn build && yarn express-start"]
    restart: always
    ports:
      - "3000:3000"
    links: 
      - api
    volumes: 
      - /local/app:/app
  nginx:
    container_name: 'nginx'
    restart: always
    image: nginx:1.15-alpine
    ports:
      - '80:80'  
      - '443:443'
    volumes:
      - ./data/nginx:/etc/nginx/conf.d
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot      
      - ./www:/etc/nginx/html

And here is the nginx conf:

upstream app {
  ip_hash;
  server app:3000;
}

upstream api {
  server api:8383;
}

server {
    listen 80;
}

map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

server {
    listen 443 ssl;
    server_name mydomain;

    ssl_certificate /etc/letsencrypt/live/mydomain/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mydomain/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
      proxy_pass http://app;
    } 

    location /api {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $host;

      proxy_pass http://api;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "Upgrade";      
    }   
}

And finally, the server initialization:

const app = express();
app.use(cookieParser());
app.use(process.env.URL_BASE_PATH + '/' + process.env.UPLOAD_URL_DIR, express.static(process.env.UPLOAD_PATH));
app.use(process.env.URL_BASE_PATH + '/assets', express.static('assets'));
app.use(process.env.URL_BASE_PATH + '/etc', router);
app.use(createLocaleMiddleware());

app.use(helmet());
app.disable('x-powered-by');

console.log(process.env.URL_BASE_PATH);
if(process.env.URL_BASE_PATH === '')server.applyMiddleware({app, cors:corsOptions});
    else server.applyMiddleware({app, cors:corsOptions, path:process.env.URL_BASE_PATH});

const httpServer = http.createServer(app);
server.installSubscriptionHandlers(httpServer);

//STARTING
httpServer.listen({port: process.env.SERVER_PORT}, () => {
        console.log(`🚀 Server ready`)
    }
);

Where server is an ApolloServer

Everything works but the wss connection: the app can connect to the api using https://mydomain/api normally, and regular ws connection works too, if I run the app on http

Is just wss that I cant get to work

Any clues? What am I doing wrong here?


Solution

  • I found my own solution: the docker/nginx configs were right, but Apollo was expecting the websocket connection on wss://mydomain/graphql, even though the graphql server is running on https://mydomain/api

    I failed to find a way to change that, so I added this to the nginx conf:

      location ^~/graphql {
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Frame-Options SAMEORIGIN;
    
        proxy_pass http://api; 
      }
    

    And it finally worked