서론
지난 1편에서 라즈베리파이 서버를 설치하고 네트워크 구성했고, 이어서 Nginx, MySQL, WAS(NestJS)를 Docker를 이용하여 구성하고 전 과정을 코드로 관리하여, Github Actions로 자동화까지 해보겠습니다.
Docker & docker-compose 설치
먼저 docker와 docker-compose를 설치해 준다.
# Docker 디렉토리 생성 후 docker install
mkdir Docker
cd Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Docker 그룹에 사용자 추가
sudo usermod -aG docker ${USER}
groups ${USER}
sudo reboot
docker version
# Docker 를 시작 프로그램에 추가
sudo systemctl enable docker
# docker-compose 설치
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
각각의 서비스를 서로 다른 docker-compose.yml 파일에 정의하고, 서로 통신이 되어야 하기에 공통으로 사용할 docker network를 정의해 준다.
docker network create docker-network
docker network inspect docker-network
MySQL 세팅
# docker-compose.yml
services:
database:
image: mysql/mysql-server:latest
ports:
- 3306:3306
environment:
- MYSQL_ROOT_HOST=%
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
volumes:
- ./data:/var/lib/mysql
container_name: mysql
networks:
- docker-network
networks:
docker-network:
external: true
위와 같이 docker-compose 파일을 작성해 준다.
docker-compose --env-file <.env 경로> up -d
실행할 때는 환경변수를 사용할 수 있도록 경로를 지정해 준다.
NestJS CI/CD세팅
필자는 이미 개발된 NestJS 프로젝트가 있어서 NestJS를 사용했을 뿐, 어떤 프레임워크로 개발하던 동일하게 적용할 수 있다.(그것이 API 서버가 아닌 Front 일지라도)
# Dockerfile
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 8081
CMD ["node", "dist/src/main"]
docker image를 만들기 위해 Dockerfile을 작성해 준다.
docker buildx create --use
docker buildx build --load --platform linux/arm64 -t <이미지 이름 지정> .
docker images
여기가 중요한데, 이미지를 빌드할 때 꼭 platform을 arm64로 지정해줘야 한다. 기본으로 설정하면 윈도우 PC나 github actions에서 빌드될 때, x86 아키텍처 기준으로 빌드되어 다른 CPU 아키텍처에서 호환이 안된다.
# docker-compose.yml
services:
nestjs-app:
image: <이미지 이름 지정>:latest
container_name: <컨테이너 이름 지정>
ports:
- "8081:8081"
env_file:
- .env
networks:
- docker-network
networks:
docker-network:
external: true
위와 같이 docker-compose를 작성할 수 있다. 이때 WAS에서 사용하는 환경변수가 있다면 경로를 명시하고, 사용하는 포트로 설정해 주도록 하자. 본인은 8081 포트를 사용하는데, 이거는 docker 내부에서 사용하는 포트라서 어떤 것으로 설정하던지 상관없다.(다른 이미지와 겹치지 않는 선에서) 후술 할 Nginx 리버스 프록시 설정할 때 80, 443으로 요청받을 수 있도록 설정해 줄 것이다.
Nginx 세팅 (+ssl 설정)
ssl 설정에 관해서는 아래의 포스팅을 참고했다.
https://pentacent.medium.com/nginx-and-lets-encrypt-with-docker-in-less-than-5-minutes-b4b8a60d3a71
# docker-compose.yml
services:
nginx:
image: nginx:latest
volumes:
- ./conf/nginx.conf:/etc/nginx/nginx.conf
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot
ports:
- 80:80
- 443:443
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
networks:
- docker-network
certbot:
image: certbot/certbot
volumes:
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
networks:
- docker-network
networks:
docker-network:
external: true
차근차근 따라 하면 여기까지는 쉽게 올 수 있다. 조심할 것은 인증서 발급과정에서 도메인 연결이 되어있어야 하고, 80 포트를 확인하기 때문에 포트포워딩으로 외부에서 라즈베리파이 80 포트에 접근할 수 있도록 뚫어놔야 한다.
본인은 도메인 레코드를 AWS Route53을 이용해서 관리하고 있다. (호스팅영역 당 월 0.5$ 비용 발생) 도메인은 가비아 등 여러 DNS 제공 업체에서 구매할 수 있으니 비교해 보고 원하는 도메인을 고르면 된다.
포트포워딩은 공유기 제조사마다 다르니 구글링 해보면 어렵지 않게 설정할 수 있을 것이다. 단, 집에서 통신사 모뎀을 쓰고 있다면 통신사 모뎀에서도 포트포워딩을 해줘야 하니 참고하자.
# /conf/nginx.conf
events {
worker_connections 1024;
}
http{
server {
listen 80;
server_name <사용하는 도메인 주소>;
server_tokens off;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name <사용하는 도메인 주소>;
server_tokens off;
ssl_certificate /etc/letsencrypt/live/<사용하는 도메인 주소>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<사용하는 도메인 주소>/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
resolver 127.0.0.11; # Docker의 내부 DNS 추가
location / {
proxy_pass http://<WAS Docker 컨테이너 명>:8081;
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;
}
}
}
nginx.conf가 중요한데, 80 포트로 들어오면 https로 리다이렉트 시키고, 443으로 들어오면 ssl 인증서를 이용해서 WAS 서버로 요청을 전송하는 리버스 프록시를 구성한다.
본인은 request를 서브도메인으로 구분하여, 각기 다른 WAS에 요청을 전송하도록 구성하였다. 이때 ssl 인증서는 각 완전한 도메인 별로 발급받아야 한다. (와일드카드를 이용하면 더 쉽게 될 것 같기도 하다)
주의할 점이라면 Docker 내부 DNS 주소인 127.0.0.11을 명시해야 컨테이너명으로 WAS 서버와 연결을 해준다는 점이다.
인프라 배포를 위한 Github Actions 설정
마지막으로 Github에 인프라 설정 코드를 push 하면 자동으로 라즈베리파이에 적용되도록 github actions를 설정해 보았다.
git config --global credential.helper store
git pull origin main
먼저 라즈베리파이 서버에 위와 같이 github 인증키를 저장해 준다. 실제 사용할 환경에 적용할 때는 ssh 키 적용하는 것이 보안 측면에서 좋다.
https://docs.github.com/en/authentication/connecting-to-github-with-ssh
# .github/workflows/deploy.yml
name: Deploy to Raspberry Pi
...
jobs:
deploy:
runs-on: ubuntu-latest
steps:
...
- name: Deploy MySQL with SSH
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.RASPBERRY_HOST }}
username: ${{ secrets.RASPBERRY_USERNAME }}
password: ${{ secrets.RASPBERRY_PASSWORD }}
port: ${{ secrets.RASPBERRY_PORT }}
script: |
cd Docker/personal-server-setup
git pull origin main
cd home/raspberry-pi/mysql
docker-compose down || true
sh init.sh
- name: Deploy Nginx with SSH
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.RASPBERRY_HOST }}
username: ${{ secrets.RASPBERRY_USERNAME }}
password: ${{ secrets.RASPBERRY_PASSWORD }}
port: ${{ secrets.RASPBERRY_PORT }}
script: |
cd Docker/personal-server-setup
git pull origin main
cd home/raspberry-pi/nginx
docker-compose down || true
sh init.sh
https://github.com/appleboy/ssh-action
ssh를 이용해서 github actions가 라즈베리파이에 접속하고, 코드를 pull 한 뒤에 docker-compose를 down/up 해주는 과정이다. 동일한 이유로 password를 직접 사용하는 것보다, ssh 키를 발급받아서 사용하는 것이 좋다.
WAS 배포를 위한 Github Actions 설정
- name: Build Docker Image
run: |
docker buildx create --use
docker buildx build --load --platform linux/arm64 -t <이미지 명> .
docker images
docker save <이미지 명>:latest -o <이미지 명>.tar
chmod 664 <이미지 명>.tar
ls -al
- name: Copy file to Raspberry Pi
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.RASPBERRY_HOST }}
username: ${{ secrets.RASPBERRY_USERNAME }}
password: ${{ secrets.RASPBERRY_PASSWORD }}
port: ${{ secrets.RASPBERRY_PORT }}
source: '<이미지 명>.tar,Dockerfile,docker-compose.yml,.env'
target: '<라즈베리파이 상 WAS 경로>'
- name: Deploy Docker Image on Remote Server
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.RASPBERRY_HOST }}
username: ${{ secrets.RASPBERRY_USERNAME }}
password: ${{ secrets.RASPBERRY_PASSWORD }}
port: ${{ secrets.RASPBERRY_PORT }}
script: |
cd <라즈베리파이 상 WAS 경로>
ls -al
docker load < <이미지 명>.tar
rm <이미지 명>.tar
docker-compose down || true
docker-compose up -d
백엔드 소스를 Checkout 해서 github actions VM 위에서 Docker Image로 빌드하고, 이미지를 tar 압축을 한 뒤 scp로 라즈베리파이로 옮겨준다. 이후 라즈베리파이에서 이미지를 load 한 뒤, docker-compse up/down을 해주는 로직이다. scp를 안 쓰고, DockerHub를 경유해 push 하는 방법도 있을 것 같다.
최종적으로 docker ps를 했을 때 위와 같이 나오면 성공이다.
성능 측정 및 분석
Google PageSpeed Insights
swagger page를 불러오는 속도를 비교해 보았다.
어느 포인트에서 접속해서 측정하는지는 확실하지 않으나, 홈 서버가 클라우드에 비해서 접속 시간은 2,3배 느렸다.
tools.pingdom.com
Health check를 해보았을 때, 홈 서버는 182ms, 클라우드는 123ms로 측정되었다.
blazemeter.com
20명 user로 10분 동안 Apache JMeter로 API 테스트를 진행해 본 결과 아래와 같다. (DB에서 게시물 하나를 조회하는 API, Asia Japan 리전에서 접속)
90% Response Time을 보면 홈서버가 1383ms, AWS 서버가 906ms로 응답시간 측면에서 AWS에 비해 1.52배 정도 더 걸린다는 것을 확인할 수 있었다.
분석
추가로 API 테스트 도중에 각각의 CPU/MEM 사용량을 확인해 본 결과 아래와 같다.
홈 서버에서 top 명령어로 확인해 본 결과에서 us, sy 합인 CPU 사용률이 35~45% 정도 왔다 갔다 하고,
CloudWatch로 확인한 AWS EC2의 CPU 사용률이 40% 정도에 수렴하는 걸 감안했을 때, CPU 성능은 WAS와 함께 DB, WS까지 돌리고 있는 라즈베리파이가 더 우수한 듯하다.
RAM도 용량 측면에서 라즈베리파이가 2GB, AWS t3.micro는 1GB로 라즈베리파이가 더 우수하다.
(다만, top 결과를 보면 RAM은 거의 꽉꽉 채워 쓰고도 부족해서 스왑 메모리까지 돌아가고 있는 걸 볼 수 있는데, SSD가 아니었으면 상당히 느렸을 것 같다. 이렇게 여러 개 docker로 띄워서 쓰려면 램 빵빵한 모델로 사는 게 장기적으로 좋을 것이다.)
그렇다면 위의 API 테스트 결과를 분석해 봤을 때 CPU/RAM 성능의 차이보다는 네트워크 성능에서 차이가 나는 것으로 추정해 볼 수 있다.
아무래도 클라우드 환경에서 t3.micro 기준 Burst시 5 Gbps를 지원하는데 비해, 라즈베리파이는 1 Gbps (125MB/s)를 지원하는 점과 가정 내에 공유기와 모뎀이 라즈베리파이만을 위한 독립된 환경이 아니라는 점에서 이러한 현상이 생기는 것으로 추정이 된다.
'홈 네트워크 & 서버' 카테고리의 다른 글
[홈 서버] 라즈베리파이로 홈 서버 구성하기, SSD 부팅, 네트워크 구성, 총비용 (0) | 2025.01.27 |
---|