NGINX!

개요

트레일링 슬래시(trailing slash)

출처: https://djkeh.github.io/articles/Why-do-we-put-slash-at-the-end-of-URL-kor/

URL의 끝에 붙이는 슬래시(/)를 트레일링 슬래시(trailing slash)라고 부른다.
웹사이트에 들어가 보면 맨 끝에 이것이 어느새 알아서 붙어있다.

1
2
https://www.google.com/example/ -> 디렉토리입니다.
https://www.google.com/example -> 파일입니다.

트레일링 슬래시가 없는 URL을 요청할 때, 서버는 해당 리소스를 우선 파일로 간주한다.

서버의 처리 동작은 다음과 같습니다

  1. 해당 이름의 파일이 존재하는지를 먼저 확인.
  2. 없을 경우, 해당 이름의 디렉토리를 확인.
  3. 디렉토리가 있으면, 그 안의 기본 파일을 확인.
    nginx 기본파일은 index.html

그렇다면 아래의 URL 은 어떤 자원을 요청하는 것일까… 디렉토리? 파일?

https://www.naver.com

파일일 것 같지만 디렉토리를 가리킨다. 해당 URL 의 루트 디렉토리.
해당 요청은 브라우저에 의해 자동으로 https://www.naver.com/ 변환된다.

CORS(Cross Origin Resource Sharing)

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

최근 서비스 사이트에서 한 페이지 안에 모든 데이터를 꽉꽉 담아 한번에 출력하는 페이지는 일반적으로 만들지 않는다.

네이버 메인페이지만 봐도 자체 HTML은 naver.com에, 쇼핑, 뉴스, 증권 등 각종 데이터를 각종 도메인에서 비동기 요청을 통해 json 형식으로 데이터를 수신받아 메인페이지에 출력한다.

naver.com 에서 받은 페이지에서 some-ews.com 에서 받은 뉴스정보 데이터를 받아 브라우저에서 랜더링하는데 SOP(Same Origin Policy) 보안 정책 에러가 발생한다.

CORS 에러는 SOP 보안정책으로 발생하는 문제로, 동일한 도메인이 아닌 서로 다른 도메인에서 받은 데이터 요청시 발생한다.

쿠키를 포함한 민감한 정보가 JS 를 통해 다른 도메인 서버에도 전달될 가능성이 있기 때문에 막는것이다.

Preflighted requests in CORS

OPTIONS 메소드를 통해 preflight(사전 전달) 보내 서버가 해당 parameters를 포함한 요청(GET, POST 등)을 보내도 되는지에 대한 응답을 줄 수 있게 한다.

1
2
3
4
# request header
Access-Control-Request-Headers: authorization,content-type
Access-Control-Request-Method: POST
...

Access-Control-Request-Headers - 실제 요청에서 어떤 HTTP 헤더를 사용할지 서버에게 알려줌
Access-Control-Request-Method - 실제 요청에서 어떤 HTTP 메서드를 사용할지 서버에게 알려줌

1
2
3
4
5
6
# response header
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Allow-Methods: HEAD,GET,POST,PUT,DELETE
Access-Control-Allow-Origin: http://localhost:8080
...

Access-Control-Allow-Credentials - 요청에 대한 응답을 표시할 수 있는지를 나타냄, credentials을 사용하여 실제 요청을 수행할 수 있는지를 나타냅니다.
Access-Control-Allow-Headers - 실제 요청시 사용할 수 있는 HTTP 헤더를 나타냅니다.
Access-Control-Allow-Origin - 브라우저가 해당 출처가 리소스에 접근하도록 허용

js25{: .shadow}

그림과 같이 OPTIONS 요청에 200 OK 반환값과 특정 메서드와 헤더가 사용 가능함을 알게되고 데이터를 요청하게 된다.

이는 브라우저의 약속이기에 preflight를 발생시키지 않는 simple request 로 변경하여 CORS 이슈 없이 처리 가능하지만 보안상의 이유로 브라우저에서 별도의 설정을 통해서만 가능하다.

CGI, FastCGI, ASGI, WSGI

CGI(Common Gateway Interface)
HTTP request 가 발생하면 resource 반환하는 인터페이스
각 요청마다 프로세스를 생성하기에 많은 부하가 발생, 성능이 느리다

FastCGI
CGI 의 문제점을 개선한 것으로 요청마다 일어나는 서버의 부하를 줄이기 위해 만들어진 개선된 인터페이스
FastCGI는 요청마다 프로세스가 만들어지는 것이 아닌, 기존에 만들어진 프로세스가 계속해서 새로운 요청들을 처리한다.
미리 만들어 놓은 프로세스와 캐시 데이터를 주고 받음으로서 CGI 보다 더 효율적이고 빠르게 작동한다

WSGI(Web Server Gateway Interface)
Python 웹서버에서 사용,

ASGI(Asynchronous Server Gateway Interface)
Python 웹서버에서 사용, 비동기 웹 애플리케이션을 지원하기 위해 WSGI의 비동기 확장판으로 개발된 인터페이스

정적 파일을 처리하는 HTTP 서버로서의 역할

단순 웹서버의 역할은 HTML, CSS, Javascript, 이미지와 같은 정보를 웹 브라우저(Chrome, Iexplore, Opera, Firefox 등)에 전송하는 역할을 한다. (HTTP 프로토콜을 준수)

리버스 프록시는 Event driven 형식으로 요청을 처리하며, 이런 정적파일을 호스팅 하는 역할을 쉽게 할 수 있다.

응용프로그램 서버에 요청을 보내는 리버스 프록시로서의 역할

두번째 역할은 리버스 프록시(reverse proxy)인데, 한마디로 말하면 클라이언트는 가짜 서버에 요청하면, 프록시 서버가 배후 서버(reverse server)로부터 데이터를 가져오는 역할을 한다.

1
2
프록시 서버: Nginx
리버스 서버: 어플리케이션 서버

nginx1.png{: .shadow}

리버스 프록시를 두는 이유는 네트워크 대역이 다른 두 그룹 사이의 중개인 역할도 있지만,
요청에 대한 버퍼링, 캐싱, 요청을 분산시킬 수 있는 로드 밸런스기능 을 수행할 수 있기 때문.

Nginx 구조

참고: https://12bme.tistory.com/366

nginx2.png{: .shadow}

엔진엑스의 실행이 막 시작된 순간에는 Master Process 라 부르는 특별한 프로세스 한 개만 존재한다.

마스터 프로세스 자신은 어떤 클라이언트 요청도 처리하지 않으며 단지 그 일을 대신 수행할 작업자 프로세스를 낳아서 증식(invoke)하는데, 이때 작업자 프로세스의 사용자/그룹은 바뀐다.

환경 설정에서 작업자 프로세스의 수, 작업자 프로세스당 최대 접속 수 등을 정할 수 있다.

Nginx 기본 명령어

1
2
3
# 버전확인
$ nginx -v
nginx version: nginx/1.14.0 (Ubuntu)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# /etc/nginx/nginx.conf 문법 체크
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

# config 파일 위치 지정하여 문법체크
$ sudo nginx -t -c /etc/nginx/nginx.conf
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

# nginx 프로세스 종료
$ nginx -s stop
# 현재 연결 중인 컨넥션이 모두 완료될 때까지 기다린 후 종료한다.
$ nginx -s quit
# 설정파일 수정후 적용
$ nginx -s reload

Nginx 지시어

nginx 서버를 사용하기위한 수많은 지시어 들이 있고 이를 컨피그 파일로 관리하며 서버실행시에 적용한다.

https://nginx.org/en/docs/
https://opentutorials.org/module/384/4526

ssl 처리와 basic auth 를 위한 파일생성은 아래 명령어 참고

1
2
3
4
5
6
7
openssl req -x509 -nodes -days 36500 -newkey rsa:2048 \
-keyout etc/nginx/ssl/selfsigned.key \
-out etc/nginx/ssl/selfsigned.crt \
-subj "/CN=*"

# id pw 는 서버 환경에 맞춰서 변경 필요
htpasswd -cb nginx/.htpasswd test test

아래와 같은 형태로 nginx.conf 를 구성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
worker_processes auto; # 작업프로세스 개수,  CPU 코어 만큼 자동설정, 

error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

events {
worker_connections 1024; # 동시연결처리개수
}

http {
client_max_body_size 15M;
# 10m 버퍼, 초당 15회, 설정이름은 auth_limit
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=15r/s;
access_log /dev/stdout; # 표준 출력으로 로그 출력
error_log /dev/stderr; # 표준 에러로 로그 출력

include mime.types;
default_type application/octet-stream;
server_tokens off;

gzip off;
gzip_comp_level 6;
gzip_vary on;
gzip_min_length 1000;
gzip_proxied any;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
gzip_buffers 16 8k;

server {
listen 443 ssl;
ssl_certificate /etc/nginx/ssl/selfsigned.crt;
ssl_certificate_key /etc/nginx/ssl/selfsigned.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;

# Docker 내 DNS 서버 사용
resolver 127.0.0.11 valid=10s;
# monitoring
set $grafana "http://grafana:3000"; # 변수 생성, docker service name 으로 사용
set $redisinsight "http://redisinsight:5540";


location / {
root /usr/share/nginx/html;
index index.html index.htm;
}

location /redisinsight {
# 특정 요청의 경우 burst 만큼 요청회수초과해도 허용
limit_req zone=auth_limit burst=100 nodelay;
# basic auth 처리
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;

proxy_pass $redisinsight;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
}

# monitoring
location /grafana {
rewrite ^/grafana(/.*)$ $1 break; # /grafana/ 뒤의 uri 를 후속 reverse proxy uri 로 지정
proxy_pass $grafana; # 변수 참조
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 원래 클라이언트의 IP 주소 이전,
# ex) X-Forwarded-For: 203.0.113.45
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 웹소켓 연결을 위한 Connection:Upgrade 헤더 지원
proxy_connect_timeout 7d; # 소켓 유지기간 7일
proxy_send_timeout 7d;
proxy_read_timeout 7d;
}
}
}

http, server, location 블록은 계층구조를 가지고 있다.
http > server > location 범위로 작성되는것이 빌잔적,
상위블록의 지시어는 하위 블록의 기본값으로 설정된다.

많은 지시어가 각각의 블록에서 중복선언될 수 있는데 http 블록은 여러개를 사용할 수 있지만 관리상의 이슈로 한번만 사용하는 것을 권장한다.

mime

mime.types 에는 각종 통신에 사용되는 각종 문자열들이 key-value 형식으로 저장되어있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# /etc/nginx/mime.types

types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
...
text/plain txt;
...
image/png png;
...
application/json json;
...
}

upstream 블록

nginx 만으로 클러스터링 구성을 지원하는 블록

nginx는 라운드 로빈 로드밸런싱 메커니즘으로 요청을 어플리케이션에 할당 한다.

아래 설정으로 메커니즘 변경이 가능하다.

http://nginx.org/en/docs/http/ngx_http_upstream_module.html

  • ip_hash
    같은 방문자로부터 도착한 요청은 항상 같은 업스트림 서버가 처리 할 수 있게 한다. session clustering 이 구성되지 않은 경우 유용하다.
  • least_conn
    최소요청을 처리한 서버에 우선적으로 요청을 배치
  • least_time
    최소요청시간 서버에 우선적으로 요청을 배치
  • weight=n
    업스트림 서버의 비중을 나타낸다. 이 값을 2로 설정하면 두번의 요청이 해당 서버에서 먼저 처리된다.
  • max_fails=n
    n으로 지정한 횟수만큼 실패가 일어나면 서버가 죽은 것으로 간주한다.
  • fail_timeout=n
    max_fails가 지정된 상태에서 이 값이 설정만큼 서버가 응답하지 않으면 죽은 것으로 간주한다.
  • down
    해당 서버를 사용하지 않게 지정한다. ip_hash; 지시어가 설정된 상태에서만 유효하다.
  • backup
    모든 서버가 동작하지 않을 때 backup으로 표시된 서버가 사용되고 그 전까지는 사용되지 않는다.
  • least_conn
    요청이 제일적은 서버로 요청 할당
  • keepalive
    서버 연결을 유지하기위한 캐시 할당, 연결 유지 수를 설정
  • keepalive_request
    keepalive 되는동안 요청 가능한 횟수, 모두 채우면 연결이 닫힘
  • keepalive_timeout
    keepalive 유지 시간

keepaliveworker_connections 최대 연결 개수를 넘지 말것
연결객체 유지때문에 새로운 연결을 받지 못할 수 있음

location 블록

location 블록은 server 블록 안에 등장하면서 특정 URL을 처리하는 방법을 정의한다.

이를테면 http://.../course/1, http://.../module/1 처럼 URL 뒤에 붙는 context 로 접근하는 요청을 다르게 처리하고 싶을 때 사용한다.

1
2
3
location @rewrites {
rewrite ^(.+)$ /index.html last;
}
  • 클라이언트가 요청한 경로가 실제 파일 또는 API가 아니고,
  • Nginx 설정상 별도로 처리할 수 없다면,
  • 그 요청을 /index.html로 리다이렉트하겠다는 의미.

Vue/React 앱에서 자주 사용

prefix match

1
2
3
4
# prefix match
location /greet {
return 200 'this is prefix match';
}

위에서 작성한 /greetprefix match 이다.

http://localhost/greetAbracadabra 로 접근해도 /greet... 까지는 일치하기 때문에 위의 location 블록에 해당된다.

1
2
3
4
# redirect
location /logo {
return 307 /thumb.png;
}

http://localhost/logo 로 접근하면 http://localhost/thumb.png 로 리다이렉트 된다.(URI 도 자동으로 변환된다)

preferential prefix match

1
2
3
4
# preferential prefix match
location ^~ /greet {
return 200 'this is regex match';
}

약간 우선순위가 높은 prefix match 이다.
위의 prefix matchregex match 보다 높은 우선순위를 가진다.

exact match

1
2
3
4
# exact match
location = /greet {
return 200 'this is exact match';
}

완전히 매칭되는 URI 대해서만 location을 지정하고 싶으면 위처럼 exact match를 사용

regex match

1
2
3
4
# case sensitive regex match
location ~ /greet[0-9] {
return 200 'this is regex match';
}

regex 를 사용해서 URL location을 지정이 가능하다. ~ 를 사용해 regex match를 사용

1
2
3
4
# case insensitive regex match
location ~* /greet[0-9] {
return 200 'this is regex match';
}

case insensitive(대소문자 구분x) 를 사용하면 ~* 를 사용.

priority 순서는 정의한 위치일것 같지만 아래와 같다.

exact > preferential > regex > prefix

root, alias 지시자

root: location 으로 넘어온 부분을 root로 설정한 경로에 추가한다.
alias: location 으로 넘어온 부분 alias 로 설정한 경로에서 찾는다.

1
2
3
4
location /static/ {
root /var/www/app/static;
autoindex off;
}

/var/www/app/static/static 경로에서 index.html 을 찾는다.

1
2
3
4
location /static/ {
alias /var/www/app/static/;
autoindex off;
}

/var/www/app/static/ 에서 찾는다.`

autoindex: on 으로 설정시 디렉토리 내부의 목록을 보여주는 index 페이지를 출력, off 는 index.html 이 없으면 에러반환

Nginx variable

http://nginx.org/en/docs/varindex.html
https://opentutorials.org/module/384/4508

variablenginx.conf 에서 사용할 수 있는 변수를 의미.
NGINX 에서 어던 변수들을 지원하는지 알아보자.

http://localhost:8080/test?arg=123&name=kouzie

실제 variable 이 어떻게 출력되는지 확인

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
location /test {
add_header Content-Type text/plain;
return 200
'
host: $host,
uri: $uri,
http_host: $http_host,
http_accept: $http_accept,
http_connection: $http_connection,
scheme: $scheme,
remote_addr: $remote_addr,
args: $args,
arg_name: $arg_name,
';
}
1
2
3
4
5
6
7
8
9
host: 192.168.100.101,
uri: /test,
http_host: 192.168.100.101:8080,
http_accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9,
http_connection: keep-alive,
scheme: http,
remote_addr: 192.168.100.101,
args: arg=123&name=kouzie,
arg_name: kouzie,
  • $host: host
  • $uri: uri
  • $http_{...}: http 헤더값 출력, 케밥표기식 사용
  • $args: 파라미터 문자열
  • $scheme: http 혹은 https
  • $remote_addr: 클라이언트 주소
  • $proxy_host: proxy_pass 지시자에 설정되어 있는 host, port
  • $proxy_port: proxy_pass 지시자에 설정되어 있는 port
  • $proxy_add_x_forwarded_for: 클라이언트가 전달한 X-Forwarded-For 헤더, 헤더가 존재하지 않는다면 $remote_addr값으로 대체|

configuration variable

configuration variableif와 같은 conditional state를 작성할때 주로 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
# configuration variable
set $weekend 'No';

# check if weekend
if ( $date_local ~ 'Saturday|Sunday' ){
set $weekend 'Yes';
}

# return $weekend value
location /is_weekend {
return 200 $weekend;
}
}

Nginx 모듈

rewrite

rewrite 는 입력받은 URI 을 정규표현식으로 찾아 URI을 변환하는 역할을 한다.

rewriteredirect와는 다르게 내부적으로 요청을 처리하고 URI는 기존에 호출했던 URI를 유지한다.

1
2
3
4
5
6
# rewrite
rewrite ^/user/\w+ /greet;

lcoation /greet {
return 200 "Hello User"
}

^ 는 문자열 시작을 알리는 메타문자
\w 는 모든 영숫자를 표시하는 메타문자

/user 로 시작하고 뒤에 /영숫자가 오는 모든 URI 를 /greetrewrite 하여 서버 리소스를 출력한다.

rewrite를 사용하면 URI의 특정 부분을 grap해 사용할 수 있다.

( ) 내부에 grap 하고싶은 URI의 특정 부분표기,
$grap 할 수 있다.

proxy_pass

프록시 서버의 프로토콜과 매핑 할 URI를 설정. http또는 https

1
2
3
location /name/ {
proxy_pass http://127.0.0.1/remote/;
}
1
2
3
4
location /name/ {
rewrite /name/([^/]+) /users?name=$1 break;
proxy_pass http://127.0.0.1;
}
1
2
3
4
5
6
location /other/ {
rewrite ^/other(/.*)$ $1 break; # url에서 other 뒤에 있는 URL을 전부 그대로 사용.
proxy_pass http://other;
proxy_set_header X-Real-IP $remote_addr; # 실제 접속자의 IP를 X-Real-IP 헤더에 입혀서 전송.
proxy_redirect off;
}

proxy_http_version

proxy_set_header

1
2
3
4
5
6
location /other/ {
rewrite /other(/.*)$ $1 break;
proxy_pass http://other;
proxy_set_header X-Real-IP $remote_addr; # 실제 접속자의 IP를 X-Real-IP 헤더에 입혀서 전송.
proxy_redirect off;
}

proxy_redirect

proxy_bind

proxy_bind address [transparent] | off;

proxy_bind $remote_addr transparent;

Nginx ssl 적용

CRL (Certificate Revocation List) - 기존에는 CRL 를 이용하여 인증서의 무결성 여부를 확인,
인증서에 기록된 CRL주소에서 CRL List를 다운로드 받아 인증서의 폐기여부를 확인,
CRL은 클라이언트에서 매우 큰 인증서 폐기목록을 모두 다운로드 받아 확인해야 하는 단점을 가지고 있음.

OCSP - 인증서의 상태를 실시간으로 체크하기 위한 프로토콜.
OCSP는 인증서의 시리얼을 통하여 실시간으로 인증서의 만료여부를 CA 인증서DB에 직접 요청.
OCSP는 CRL과 같이 불필요한 목록을 모두 받아 볼 필요가 없어 그 속도가 빠름.

OCSP Stapling - OCSP 요청을 처리해주는 CA 인증서버에 걸리는 부하를 해결하기 위해 만든 기술
클라이언트가 직접 CA서버를 통하여 인증서 만료여부를 확인하는 것이 아니라 서비스를 제공하는 웹서버에서 만료여부를 중계.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
listen 80;
server_name gateway.mydomain.co.kr;
return 404; # managed by Certbot
if ($host = gateway.mydomain.co.kr) {
return 301 https://$host$request_uri;
} # managed by Certbot
}

server {
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/gateway.mydomain.co.kr/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/gateway.mydomain.co.kr/privkey.pem; # managed by Certbot
# 브라우저는 이 도메인에 대해 HTTPS만 사용해야 함을 명시
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
access_log /var/log/nginx/gateway/access.log;
error_log /var/log/nginx/gateway/error.log;
server_name gateway.mydomain.co.kr;
...
}

Lets Encrypt

DNS 와 공인IP 가 있을 경우 인증서를 생성하고 Lets Encrypt 를 통해 인증서

1
2
3
4
5
6
7
$ apt-get update
$ sudo apt-get install certbot
# nginx 용 certbot 플러그인 설치
$ apt-get install python3-certbot-nginx

# nginx plugin 으로 certbot 통해서 domain 을 등록
$ sudo certbot --nginx -d gateway.mydomain.co.kr

보안관련 메일을 수신받을 email 주소를 입력, 그리고 각종 동의를 진행하면
certbot 이 자동으로 Lets Encript 와 통신하며 도메인 인증과정을 진행한다.

  • 기존 nginx 설정을 백업
  • 기본 nginx 설정에다 /.well-known/acme-challenge 등의 작업을 처리
  • Lets Encrypt 인즌 진행

letsencrypt 를 통해 생성된 인증서를 확인

1
2
$ sudo ls /etc/letsencrypt/live/gateway.mydomain.co.kr
# cert.pem chain.pem fullchain.pem privkey.pem README
1
2
3
4
sudo crontab -e
# 아래 예약 입력
0 0 * * * /usr/bin/certbot renew --quiet # 매일 자정
0 0 1 * * /usr/sbin/nginx -s reload # 매달 1일 자정
1
2
3
4
5
6
7
$ cd /etc/nginx/sites-avaliable
$ sudo vi gateway.mydomain.co.kr
# nginx 서버 설정 작성

$ cd /etc/nginx/sites-enabled
sudo ln -s /etc/nginx/sites-available/gateway.mydomain.co.kr /etc/nginx/sites-enabled/
# sites-enabled 에 symlink 작성
1
2
3
4
# nginx config test 
sudo nginx -t

sudo service nginx restart