Home > other >  Dockerized react socket.io client with nginx is connected but not emitting to reversed proxy uvicorn
Dockerized react socket.io client with nginx is connected but not emitting to reversed proxy uvicorn

Time:09-03

I have a dockerized react-uvicorn asgi app running on reversed proxy nginx. When i run 'docker compose up --build' everything is connected and on page reload reconnecting is successful. The problem is that react can't emitt events or uvicorn is not recieving them. The app was tested successfully without nginx locally and everything was ok until i added nginx and deployed on digitalocean. I'm having some sleepless nights trying to figure it out and still don't know what the problem is. Can someone pls help me...

root directory hirarchy:

App
|__client(react)
|   |__conf
|      |__conf.d
|         |__default.conf
|         |__gzip.conf
|   |__Dockerfile
|   |__public
|   |__src
|      |__withSocket.tsx
|      |__App.tsx
|__server(uvicorn)
|   |__server.py
|   |__Dockerfile
|__another-service(python socket.io)
|  |__main.py
|__docker-compose.yml

docker-compose.yml

version: '3.9'
services:
  api:
    build: server/
    restart: unless-stopped
    volumes:
      - ./server:/app
    ports:
      - 8080:8080
  client:
    build: ./newclient
    restart: unless-stopped
    ports:
      - 80:80
  another-service:
    build: ./another-service
    restart: unless-stopped
    volumes:
      - ./another-service:/app
    ports:
      - 5004:5004
    depends_on:
      - api
 

uvicorn asgi server.py


import socketio
import asyncio
import json

sio = socketio.AsyncServer(
    async_mode='asgi',
    async_handlers=True, 
    cors_allowed_origins="*", 
    logger=True, 
    engineio_logger=True,
    always_connect=True,
    ping_timeout=60
    )
app = socketio.ASGIApp(sio, socketio_path='/socket.io')

@sio.event
async def connect(sid, environ, auth=None):
    print(Fore.GREEN   'connected ', sid)

@sio.event
def disconnect(sid):
    print('disconnect ', sid)

@sio.on('example-event')
async def auth(sid, data):
    data = json.loads(data)
    print('received data: '   data['data'])
    
    if await client.is_user_authorized():
        response = {
            'auth_log': 'Authenticated',
            'logged_in': True
        }
        await sio.emit('auth', response)

if __name__ == '__main__':
    import uvicorn
    uvicorn.run("server:app", host='0.0.0.0', port=8080, log_level="info")

uvicorn server Dockerfile

FROM python:3.9
ENV PYTHONUNBUFFERED 1
WORKDIR /app
COPY requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt
RUN pip freeze > requirements.txt
COPY . /app
EXPOSE 8080
CMD python server.py

client React socket.io component-wrapper withSocket.tsx

import React from "react";
import { io, Socket } from "socket.io-client";

interface ServerToClientEvents {
    auth: (data: any) => void
}

interface ClientToServerEvents {
    connect: (connected: string) => void
    example-event: (data: any) => void
}

// component wrapper that allows to use socket globally
function withSocket (WrappedComponent: any) {
    const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(process.env.REACT_APP_SOCKET_URL!, {
        transports: ["websocket"],
        path: "/socket.io"
    });


    const WithSocket = (props: any) => {

        // function to subscribe to events
        const socketListen = async (queue: any, callback: any) => {
            await socket.on(queue, (data?: any) => {
                callback(data)
            })
            await socket.on('disconnect', () => socket.off());

        }

        const socketSend = async (queue?: any, data?: any) => {
            await socket.emit(queue!, JSON.stringify(data))

        }

        return (
            <WrappedComponent
                {...props}
                socketSend={socketSend}
                socketListen={socketListen}
            />
        )
    }
    return WithSocket
}


export default withSocket

client socket.io component App.tsx

import React, {useState, useEffect} from 'react'
import withSocket from "../withSocket";
import "./AuthForm.css"

function AuthForm({socketListen, socketSend}: { socketListen: any; socketSend: any }) {
    const [data, setData] = useState('');
    const [message, setMessage] = useState('');

    useEffect(() => {
        socketListen('auth', (data: any) => {
            setMessage(JSON.stringify(data.auth_log))
        })
    }, [socketListen])

    let handleSubmitData = async (e: any) => {
        e.preventDefault();
        try {
            socketSend('example-event', {'data': data})
        } catch (err) {
            console.log(err);
        }
    }

    return(
        <>
        <div className="form-wrapper">
            <form onSubmit={handleSubmitData}>
                <label>
                    <input type="text" name="data" placeholder="Data" onChange={(e) => setData(e.target.value)}/>
                </label>
                <button type="submit">Send</button>
            </form>
            <div className="message">
                {message ? <p>{message}</p> : null}
            </div>
        </div>
        </>
    )
}

export default withSocket(AuthForm)

client .env file

REACT_APP_SOCKET_URL=ws://example-site.com

client react & nginx Dockerfile

# build environment
FROM node:alpine as builder
WORKDIR /app
COPY package.json .
COPY yarn.lock .
RUN yarn
COPY . .
RUN yarn build


# production environment
FROM nginx:1.15.2-alpine
RUN rm -rf /etc/nginx/conf.d
COPY conf /etc/nginx
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
# Copy .env file and shell script to container
WORKDIR /usr/share/nginx/html
COPY ./env.sh .
COPY .env .
# Add bash
RUN apk add --no-cache bash
# Make our shell script executable
RUN chmod  x env.sh
# Start Nginx server
CMD ["/bin/bash", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\""]

nginx configuration default.conf

# Use site-specific access and error logs and don't log 2xx or 3xx status codes
map $status $loggable {
    ~^[23] 0;
    default 1;
}

access_log /var/log/nginx/access.log combined buffer=512k flush=1m if=$loggable;
error_log /var/log/nginx/error.log;

upstream socket-server-upstream {
    server api:8080;
    keepalive 16;
}

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

  server_name example-site.com www.example-site.com;
  client_max_body_size 15M;

  location / {
    root /usr/share/nginx/html;
    index index.html index.htm;
    try_files $uri $uri/ /index.html;
    # enable WebSockets
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    expires -1;
  }

  location /socket.io {
    proxy_pass http://socket-server-upstream/socket.io;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Headers *;
    proxy_redirect off;
    proxy_buffering off;
  }

  error_page   500 502 503 504  /50x.html;
  location = /50x.html {
    root /usr/share/nginx/html;
  }
}

nginx MIME-Type configuration gzip.conf

gzip on;
gzip_http_version  1.0;
gzip_comp_level    5; # 1-9
gzip_min_length    256;
gzip_proxied       any;
gzip_vary          on;

# MIME-types
gzip_types
  application/atom xml
  application/javascript
  application/json
  application/rss xml
  application/vnd.ms-fontobject
  application/x-font-ttf
  application/x-web-app-manifest json
  application/xhtml xml
  application/xml
  font/opentype
  image/svg xml
  image/x-icon
  text/css
  text/plain
  text/x-component;

As i already wrote at the beginning, the code is working locally without nginx and client is emitting events and the uvicorn server is recieving and emitting regularly. With nginx deployed on Ubuntu Server the services are also connected but client is not emitting. Why?

CodePudding user response:

When you have 2 Docker containers (1 "frontend" container and 1 "backend" container) and they need to exchange information via HTTP or WebSocket then make sure that your domain points to your "backend" container e.g. api.your-domain.com (and call api.your-domain.com from your "frontend" container because you're reaching your API service from your browser).

If you have two "backend" containers like in your docker-comopose.yml file api and another-service and need to exchange information then make sure both services are on the same docker network. Take a look here: https://docs.docker.com/compose/networking/#specify-custom-networks

If they are on the same network then they can call each other by their service name e.g. another-service makes an API call over http://api:8080 (see here: https://stackoverflow.com/a/66588432/5734066)

  • Related