이번 회사에서는 멀티뷰어처럼 제한적인 환경에서 5명정도 트래픽을 감당할 수 있는 저사용 리눅스 서버를 요청했다.
(클라우드 하고 싶다..ㅋㅋㅋ) 개념을 잡는겸 대용량 트래픽처리는 없으나 도커 컨테이너를 실행 시켜서 웹 백엔드 , 웹 프론트엔드
개발 서버처럼 구현 하고자 했다. 구동 방식에 대해서 정리 하고자 블로그에 글을 남기면서 정리하고자 내용을 남기겠다.
sudo apt update
sudo apt install firewalld
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 |
---|