docker nginx로 static file 전송하기

4 minute read

캡스톤 프로젝트를 진행하는 중에 client로부터 이미지를 전송받으면 이를 저장하고 나중에 요청이 들어오면 다시 전송해줘야하는 기능이 있었다.

이 과정을 어떻게 구현했고, 그때 만났던 이슈들을 어떻게 해결했는지 적어보려한다.

  • database에 이미지 저장(blob type)

    가장 먼저 떠오른 방법이었고 구현도 금방 할 수 있을 것같았다.

    하지만 이 방법을 이용하면 데이터베이스에 이미지라는 너무 큰 파일을 저장하게되고 이를 다시 전송할때 속도도 오래걸릴 수 있다는 문제가 있다.

    또한 추후 cloud로 서버를 이주한다면 aws S3와 같은 저장소가 있기 때문에 지금 단계에서도 그와 유사한 방법으로 저장해두어야 나중에 옮겼을때 고칠부분이 적을 것일라고 생각했다.

  • file system에 이미지 저장

    내가 선택한 방법이다. 서버에서 이미지를 받으면 이를 file system에 저장하고 저장한 path를 데이터베이스에 저장한다. 나중에 이미지에 대한 요청은 해당 path 형태로 올것이다.

    여기서 또 옵션이 있었다.

    1. 이미지를 application server가 읽어와서 전송한다.
    2. 이미지를 nginx가 전송한다.

    처음엔 1번으로 구현하려고 했으나 생각해보니 그렇게 하면 성능도 떨어지고 nginx의 장점을 살릴수 없을 것 같아서 2번으로 구현했다.

이미지 저장

현재 application server는 Nest JS로 구성되어있다. 따라서 npm의 multer 모듈을 이용해서 이미지를 받고 이를 저장했다.

이 방법은 어렵지 않고, 더 잘 설명해놓은 그들이 많아서 그 링크로 대체하겠다.

[Documentation NestJS - A progressive Node.js framework](https://docs.nestjs.com/techniques/file-upload)

Nest.js에서 파일 업로드하기

위 방법으로 client로 이미지를 받아서 저장하면 프로젝트 디렉토리의 특정 부분에 이미지가 저장된다. 이렇게 해도 상관없을 수 있겠지만 현재 서버가 docker container 위에서 돌아가고 있기 때문에 그냥 두면 문제가 발생한다.

위 방법까지만 적용한다면 container내부의 특정 디렉토리에 이미지가 저장된다. 이 이미지들은 컨테이너가 삭제된다면 같이 삭제될 것이다.

따라서 docker volume을 이용해 이미지들을 영속적으로 보관하게 되었다.

  • docker run 실행시
docker run -v host_dir:container_dir
  • docker-compose 이용시
volumes:
	- host_dir:contianer_dir

이런 식으로 contianer 내부의 특정 디렉토리를 host의 특정 디렉토리로 바인딩 시켜 데이터를 영속적으로 보관이 가능하다.

이미지 전송

저장된 이미지를 다시 client로 전송하기 위해 nginx를 이용할 것이다.

nginx가 제일 잘하는 일 중에 하나가 static file hosting이기 때문에 이를 적극 활용해보려고 한것이다.

현재 nginx 또한 docker container로 운영되고 있기 때문에 저장된 이미지를 이용하려면 volume을 이용해야한다.

  • docker run 실행시
docker run -v host_dir:container_dir
  • docker-compose 이용시
volumes:
	- host_dir:contianer_dir

이런 식으로 위와 동일하게 해주면 되는데 주의할 점은 host_dir은 이미지가 저장된 디렉토리여야만 하고 container_dir은 nginx가 요청을 받으면 이미지를 찾을 dir이어야한다.

nginx 설정사항을 정의하는 default.conf 파일을 보자, default.conf의 일부분이다.

server {
  listen               your_port default_server;
  client_max_body_size 20M;

  location / {
      proxy_pass your_server_address;
      proxy_set_header Host              $host;
      proxy_set_header X-Real-IP         $remote_addr;
      proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
  }

  location /images {
    root **container_dir**;
  }
  
}
  • location / {}

    이 부분은 nginx의 reverse proxy 기능을 이용해서 nginx로 들어온 요청을 application server로 보내주는 부분이다.

  • location /images {}

    이 부분이 이미지를 전송해주는 부분이다.

    http://address:port/images/image_name 이와 같은 형태로 요청이 들어온다면 nginx는 container_dir 에서 image_name에 해당하는 파일을 찾아서 리턴해준다.

    이 부분은 nginx container를 띄울 때 적용되는 설정이고 nginx 내부의 설정을 정의하는 부분이다. 따라서 container 내부의 디렉토리를 적어줘야한다.

    이렇게 되면 위에서 volume으로 실제 이미지가 저장된 host_dir과 container_dir을 바인딩 해줬기 때문에 결과적으로 host_dir에 있는 이미지가 전송된다.

  • client_max_body_size 20M;

    이 부분은 client로 부터 받을 데이터의 사이즈를 명시해주는 것인데, 이값을 설정하지 않으면 default로 1M이라고 한다.

    처음에 이걸 몰라서 이미지 업로드가 안되서 고생한 적이 있다….

여기까지 정리

지금까지 한 부분을 그림으로 알기 쉽게 표현하자면 다음과 같다

image

host file system의 /home/user/images 에 이미지들이 저장되고, 이곳을 nginx도 바라보고 있어서 이곳에 있는 이미지들을 전송할 수 있게 되는 것이다.

이슈 발생

이렇게까지 작성하고 테스트를 해봤는데…

  • 요청 GET http://address:port/images/file_name
<html>

<head>
	<title>404 Not Found</title>
</head>

<body>
	<center>
		<h1>404 Not Found</h1>
	</center>
	<hr>
	<center>nginx/1.19.6</center>
</body>

</html>

404 Not Found…

총 2가지의 문제가 있었고 어떤 문제였는지, 어떻게 해결했는지 공유하려고 한다.

  1. nginx volume 설정 문제

    바로 예를 들어서 설명하겠다.

    위 그림과 같은 형태로 container들이 실행되고 있고, default.conf파일을 다음과 같이 작성했다고 해보자.

     location /images {
         root **app/images**
       }
    

    언듯 봤을때는 문제 없을 것이라고 생각했는데 굉장히 큰 문제가 있었다.

    위처럼 작성되었다면 /images로 요청이 들어왔을때, /app/images/images에서 이미지를 찾아보고 없어면 404를 리턴하는 것이었다.

    다시 말해 root로 정해둔 폴더 밑에 uri로 받은 디렉토리로 가서 파일을 찾아보는 것이다.

    실제로 docker exec container_id bin/bash 명령을 통해 컨테이너 내부로 들어가서 해당 디렉토리를 들어가보니 app/images 디렉토리에 이미지들이 저장되어있었지 app/images/images 디렉토리는 없었다.

    오류는 아래와 같이 설정파일을 고쳐주면 해결된다.

     location /images {
         root **app**
       }
    
  2. nginx location 문제

    두 번째 이슈는 nginx가 location으로 나눠진 path들을 어떻게 처리하는 지에 관한것이다.

    nginx는 location 으로 여러 path가 나눠져있을때 가장 위에 정의된 path부터 요청이 들어온 url과 비교하면서 prefix가 일치하면 해당 블럭 내의 내용을 수행합니다.

     server {
       listen               your_port default_server;
       client_max_body_size 20M;
    
       location / {
           proxy_pass your_server_address;
           proxy_set_header Host              $host;
           proxy_set_header X-Real-IP         $remote_addr;
           proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
       }
    
       location /images {
         root container_dir;
       }
          
     }
    

    다음 예시에서 /images/file_name 로 요청이 들어오면 어떻게 될까요?

    제일 위에 적용된 location / {} 이 부분이 수행됩니다.

    /images/file_name에서 앞의 / 부분에서 이미 일치했기 때문에 nginx는 요청을 해당 블럭으로 보냅니다.

    이를 해결하는 방법으로는 두 location 블록의 순서를 바꿔주면 됩니다.

     server {
       listen               your_port default_server;
       client_max_body_size 20M;
    
     	location /images {
         root container_dir;
       }
    
       location / {
           proxy_pass your_server_address;
           proxy_set_header Host              $host;
           proxy_set_header X-Real-IP         $remote_addr;
           proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
       }
          
     }
    

    이렇게 하면 요청이 들어오면 제일 먼저 /images와 비교하기 때문에 해결됩니다.

앞으로

이슈를 해결하면서 nginx에서 location 설정시 최상위 루트 (/)에 대한 사용을 주의해야겠다고 생각했다. ‘/’는 모든 요청과도 일치하는 prefix이기 때문에 원하지 않는 결과를 초래할 수도 있다. 따라서 앞으로는 /api 등과 같이 특정 uri를 만들어서 오류가 발생하는 일을 줄이도록 해야겠다.

Leave a comment