이번 회사에서는 멀티뷰어처럼 제한적인 환경에서 5명정도 트래픽을 감당할 수 있는 저사용 리눅스 서버를 요청했다.

(클라우드 하고 싶다..ㅋㅋㅋ) 개념을 잡는겸 대용량 트래픽처리는 없으나 도커 컨테이너를 실행 시켜서 웹 백엔드 , 웹 프론트엔드

개발 서버처럼 구현 하고자 했다. 구동 방식에 대해서 정리 하고자 블로그에 글을 남기면서 정리하고자 내용을 남기겠다.

 

sudo apt update 

sudo apt install firewalld

 
sudo apt-get update 
sudo apt-get install ufw

 

1) apt 업데이트

sudo apt-get update

 

2) mysql 설치

sudo apt-get install mysql-server

3) mysql 버전 확인

mysql --version

# 결과 (버전 확인 되면 설치 성공)
mysql  Ver 8.0.31-0ubuntu0.20.04.1 for Linux on x86_64 ((Ubuntu))

 

3. 접속

1) root 유저로 접속

sudo service mysql start

sudo mysql -u root -p

[Enter password : 가 나오면 password 입력하면 된다.

 

상태 확인 명령어

sudo service mysql status

 

mysql 필요 폴더 설치

sudo mkdir -p /var/run/mysqld sudo chown mysql:mysql /var/run/mysqld

 

서버 재시작

sudo systemctl start mysql

 

 

mysql 워크벤치 설치 방법

sudo apt install snapd

 

sudo snap install mysql-workbench-community

 

방화벽 설정 확인

sudo ufw status

방화벽 설정 명령어

sudo ufw allow 3306

 

mysql 설정 파일 수정

sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf

bind-address 설정이 127.0.0.1 또는 0.0.0.0 으로 변경

sudo systemctl restart mysql

 

mysql 접속 후 권한 체크

sudo mysql -u root -p

SELECT host, user FROM mysql.user;

없으면 root 권한 전체 주기

GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;

적용 저장

FLUSH PRIVILEGES;

 

비번 설정하기

sudo mysql

 

계정 확인

SELECT User, Host, plugin FROM mysql.user WHERE User = 'root';

 

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'uimd5191!';

FLUSH PRIVILEGES; 

EXIT;

 

다시 접속 확인

mysql -u root -p

 

위에 과정 다 진행 후 서비스로 시작하기

sudo systemctl stop mysql

sudo systemctl start mysql

sudo systemctl enable mysql

레디스 서버 설치

sudo apt install redis-server

 

nginx 설치

sudo apt install nginx

 

nginx 권한 설정

sudo chown -R www-data:www-data /etc/nginx

 

레디스 서버 외부 접속 허용

sudo nano /etc/redis/redis.conf

bind 0.0.0.0 으로 변경 후 저장

protected-mode no 로 변경

 

설치를 다 하고 나서

https://www.docker.com/products/docker-desktop/ 

 

 

Docker Desktop: The #1 Containerization Tool for Developers | Docker

Docker Desktop is collaborative containerization software for developers. Get started and download Docker Desktop today on Mac, Windows, or Linux.

www.docker.com

도커데스크탑 윈도우 다운로드

 
 

생성된 계정에 프론트, 백엔드 프로젝트

프론트
이미지 빌드 – (프론트 디렉토리에서 진행 해야함)
docker build -t vue3-app .
도커 이미지 태그 추가 – (현재 버전명으로 통일해서 올려야 함)
docker tag vue3-app coin255/vue3-app:0.1v
docker login - > 이미지 업로드 시 로그인 필요
docker push coin255/vue3-app:0.1v

백엔드
이미지 빌드 – (백엔드 경로로 이동 후 빌드 명령어 입력)
docker build -t nestjs-backend .
docker tag nestjs-backend coin255/nestjs-backend:0.1v
docker login - > 이미지 업로드 시 로그인 필요
docker push coin255/nestjs-backend:0.1v

 

각 프로젝트에서 위 명령어 실행 해서 이미지들 생성 후


리눅스 서버에 Docker와 Docker Compose를 설치

sudo apt update
sudo apt install -y docker.io
sudo curl -L "https://github.com/docker/compose/releases/download/2.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

 

초기 설치 진행 후 

리눅스 도커이미지 받기
docker login
id -> coin255
pw => 132645df!@

Docker Hub에서 이미지 가져오기

프론트 이미지 가져오기
sudo docker pull coin255/vue3-app:0.1v

프론트 백엔드 이미지 가져오기
sudo docker pull coin255/nestjs-backend:0.1

compose 실행 리눅스 서버에서 실행
sudo docker-compose up -d

nginx 로그 폴더 생성해줘야함
sudo mkdir -p /var/log/nginx
sudo chmod 755 /var/log/nginx
nginx 재시작
sudo docker-compose restart nginx

로그 확인 방법 -> 컨테이명 치면 나옴
sudo docker logs mysql
sudo docker logs redis

 

위 작업을 진행 후

docker-compose.yml 을 수정

version: '3.8'
services:
  backend:
    container_name: backend_service
    image: coin255/nestjs-backend:0.3v
    ports:
      - "3002:3002"
    restart: always
    environment:
      - DB_HOST=host.docker.internal
      - DB_PORT=3306
      - DB_USER=root
      - DB_PASSWORD=uimd5191!
      - DB_NAME=pb_db_web
    extra_hosts:
      - "host.docker.internal:192.168.0.xx"  # 리눅스에서 실제 IP 주소 사용
    volumes:
      - /home/coin/Desktop/backend
    networks:
      - mynetwork

  frontend:
    container_name: frontend_service
    image: coin255/vue3-app:0.3v
    ports:
      - "8080:8080"
    restart: always
    volumes:
      - /home/coin/Desktop/f
    networks:
      - mynetwork

  nginx:
    image: nginx:alpine
    container_name: nginx_service
    volumes:
      - ./logs:/etc/nginx/logs
      - /etc/nginx/nginx.conf:/etc/nginx/nginx.conf
    ports:
      - "80:80"
    restart: always
    networks:
      - mynetwork


networks:
  mynetwork:
    driver: bridge

 

sudo docker-compose up -d

 

정상 작동 TCP 등 API IP 우회를 위해서

백엔드 프론트 코드를 변경해준다.

 

웹소켓 + tcp 설정 TS 파일에 아래 구문을 추가해서 Ai 측 TCP 수락을 하는 부분에 코드를 추가하기 힘들다고 웹쪽으로 업무가 넘어와서  끊고 다시 시작 할 수 있도록 변경하는 구문으로 AI 측 오류를 해결하는 방안으로 코드 수정

restartTcpConnection() {
    if (this.connectedClient) {
      this.logger.warn('⚠️ 기존 TCP 연결을 종료하고 다시 연결을 시도');
      this.connectedClient.end(); // 안전하게 종료 요청
      this.connectedClient.destroy(); // 강제 종료
      this.connectedClient = null;
    }

    setTimeout(() => {
      this.logger.warn('🔄 TCP 서버 재연결을 시도합니다.');
      this.setupTcpServer('192.168.0.131', 11235);
    }, 500);
  }

 

async sendDataToEmbeddedServer(data: any): Promise<void> {
    // 데이터 중복 체크
    if (
      this.tcpQueue.some(
        (item) => JSON.stringify(item) === JSON.stringify(data),
      )
    ) {
      this.logger.warn('⚠️ 중복 데이터로 인해 전송이 무시되었습니다.');
      return;
    }

    // 데이터 큐에 추가
    this.tcpQueue.push(data);

    await this.processQueue(); // 큐 처리 시작
  }

  private async processQueue(): Promise<void> {
    if (this.isProcessing || !this.tcpQueue.length) {
      return;
    }

    this.isProcessing = true; // 처리 중 상태로 설정
    const data = this.tcpQueue.shift(); // 큐에서 데이터 가져오기

    try {
      if (this.connectedClient && !this.connectedClient.destroyed) {
        const serializedData = JSON.stringify(data.payload);

        if (serializedData && this.isNotDownloadOrUploading) {
          this.connectedClient.write(serializedData);
          this.logger.log(`웹백엔드 -> 코어로 전송: ${serializedData}`);
          this.notRes = true;

          // 데이터 전송 후 일정 시간 대기 (예: 100ms)
          await new Promise((resolve) => setTimeout(resolve, 100));
        }
      } else {
        this.logger.warn('⚠️ 활성화된 코어 TCP 없음. 데이터 전송 안됨.');
        this.notRes = false;
      }
    } catch (error) {
      this.logger.error(`🚨 TCP 데이터 전송 오류: ${error.message}`);
    } finally {
      this.isProcessing = false; // 처리 상태 해제
      await this.processQueue(); // 다음 큐 처리
    }
  }

  stopTcpServer(): void {
    if (this.connectedClient) {
      this.connectedClient.destroy();
    }
  }

  setupTcpServer(newAddress: string, newPort: number): void {
    const connectClient = () => {
      if (!this.connectedClient || this.connectedClient.destroyed) {
        const newClient = new net.Socket();

        newClient.setTimeout(10000); // 10초 동안 클라이언트 소켓이 데이터를 송수신하지 않으면 timeout 이벤트가 발생하도록 설정

        newClient.connect(newPort, newAddress, () => {
          this.logger.warn('코어 TCP 웹 백엔드 연결 성공');
          this.connectedClient = newClient;
          this.wss.emit('isTcpConnected', true);
          this.reconnectAttempts = 0; // 재연결 시도 횟수 초기화
          this.notRes = false;
        });

        newClient.on('timeout', () => {
          this.logger.error('🚨 코어 TCP 웹 백엔드 연결 타임아웃');
          this.handleReconnectFailure(newClient);
        });

        newClient.on('data', (chunk) => {
          this.logger.warn(`코어 TCP 서버로부터 데이터 수신 성공`); // 추가된 로깅
          if (this.wss) {
            this.sendDataToWebSocketClients(chunk);
            this.notRes = false;
          } else {
            this.logger.error('🚨 WebSocketService가 초기화되지 않았습니다.');
          }
        });

        newClient.on('end', () => {
          this.logger.warn('코어 TCP 클라이언트 연결 종료');
          this.sendDataToWebSocketClients({ err: true });
          this.handleReconnectFailure(newClient);
        });

        newClient.on('error', (err: any) => {
          this.logger.error(
            `🚨[${err.code} - 코어 서버 연결 거부] 코어 TCP 연결 오류 - ${err}`,
          );
          this.sendDataToWebSocketClients({ err: true });
          this.handleReconnectFailure(newClient);
        });
      } else {
        this.logger.warn(
          '⚠️ 이미 클라이언트 연결이 활성화되어 있습니다. 연결 재활성화 시 문제 없음 정상 코드',
        );
      }
    };

    connectClient();
  }

  private handleReconnectFailure(client: net.Socket) {
    if (!this.mainPc) {
      return;
    }
    this.reconnectAttempts++;
    client.destroy(); // 기존 소켓 종료
    this.connectedClient = null;

    this.logger.warn(
      `⚠️ TCP 연결 실패, 재연결 시도 중 (${this.reconnectAttempts}/${this.maxReconnectAttempts})... 재 연결 텀 1초`,
    );

    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      setTimeout(
        () => this.setupTcpServer('192.168.0.131', 11235),
        this.reconnectDelay,
      );
      // 연결 실패 후 즉시 재시도를 방지 - 끊기고 나서 바로 재연결 시도하면 여러가지 문제발생 할 수 있어서 바로 재시작 안함
      // 재연결 지연 시간을 두어, 자원 낭비를 줄이고 시스템을 안정화하려는 목적
    } else {
      this.logger.error('🚨 최대 재연결 시도 횟수 초과.');
    }
  }

 

허트비트 도입으로 불안정한 TCP 끊길경우 재연결 시도 코드 추가를 하고

프론트 쪽에서는 이부분도 사실 윈도우 변수를 사용하고 싶지 않았지만..

AI 측에서 계속 응용프로그램처럼.. 쉽게 설치가 되었으면 좋겠다는.. 요구사항이 있어서 어쩔 수 없이.. exe 로 강제로 설치가 가능하게 IP도 수시로 바꿀 수 있게 윈도우 변수를 사용하여 전역으로 추가하였다.

config 에 윈도우 변수를 추가.. 한 후 공통으로 사용되는 http 정의 ts 파일을 수정해준다.

export function useHttpClient() {
    let apiBaseUrl: any = window.APP_API_BASE_URL || 'http://192.168.0.XX:3002';
    // type 용도 -> ? 쿼리 스트링으로 보낼지 여부
    const httpGet = async <T>(url: Endpoint, parameters?: string, type?: boolean, linuxServeSet = false): Promise<ApiResponse<T>> => {
        return httpGetAct(url.endpoint, parameters, type, linuxServeSet);
    };

    const httpGetAct = async <T>(url: string, parameters?: string, type?: boolean, linuxServeSet = false): Promise<ApiResponse<T>> => {
        const options: AxiosRequestConfig = {
            headers: {
                'Content-Type': 'application/json; charset=UTF-8',
                'Cache-Control': 'public, max-age=3600' // 응답을 1시간 동안 캐싱하도록 지정
            },
        };

        axios.defaults.withCredentials = true;
        const slush = parameters && parameters !== '' ? (type ? '?' : '/') : '';
        parameters = parameters || '';
        if (linuxServeSet) {
            apiBaseUrl = window.LINUXSERVERIP;
        } else {
            apiBaseUrl = window.APP_API_BASE_URL;
        }
        try {
            const response: HttpResponse<T> = await axios.get(`${apiBaseUrl}/${url}${slush}${parameters}`, options);
            return Promise.resolve(response.data || {code: 500, data: undefined, success: false});
        } catch (e) {
            return Promise.reject(e);
        }
    };

 

export const createH17 = async (request): Promise<ApiResponse<void>> => {
    return httpClient.httpPost(apiConstants.Hl7Create.post, request, '', false, window.LINUX_SERVER_SET );
};

 

config.js

window.APP_API_BASE_URL='http://192.168.0.xx:80/api'; // MultiViewer - 'http://192.168.0.xx:80/api',   Main PC Only - 'http://127.0.0.1:3002'
window.MAIN_API_IP = 'http://192.168.0.xx:80/api'; // MultiViewer - 'http://192.168.0.Xx:80/api',   Main PC Only - 'http://127.0.0.1:3002'
window.MAIN_API = '192.168.0.43'; // MultiViewer - 'http://192.168.0.xx:80/api',   Main PC Only - 'http://127.0.0.1:3002'
window.MAIN_WEBSOCKET_IP = 'http://192.168.0.xx:3002';  // MultiViewer - 'http://192.168.0.xx:3002', Main PC Only - 'http://127.0.0.1:3002'
window.PROJECT_TYPE='pb';  // pb or bm
window.PROJECT_VERSION='02.02.009';
window.WEB_BACKEND_VERSION='0.0.94v';
window.WEB_FRONTEND_VERSION='0.3.03v';
window.MACHINE_VERSION='100a';  // 12a or 100a
window.FORCE_VIEWER = 'main'; // main or viewer or exhibition
window.PORT = '8080';
window.LINUXSERVERIP = 'http://192.168.0.xx:3020';
window.LINUX_SERVER_SET = true;

 

위에처럼 로직을 전체 변경 추가 해준 후 내부망 IP 로 접근하면 아래와 같이 접속이 가능하다.

 

 

'DevOps > docker' 카테고리의 다른 글

프론트 코드 도코 이미지로 도커 허브에 올리기  (0) 2023.11.21

+ Recent posts