docker compose + nginx, letsencrypt를 활용한 SSL 적용

반응형
배경

본인은 간단한 사이드 프로젝트, 연습용 배포를 할 때 docker compose와 nginx를 통한 reverse-proxy 기반의 배포를 자주 활용한다.

보통 하나의 인스턴스에 nginx를 띄우고 내가 띄우고자 하는 서비스(예. express 서버)로 proxy_pass해주는 방식을 활용한다.

위와 같이 nginx-proxy라는 이미지를 활용해 따로 nginx 설정 없이 편하게하는 방법도 있지만,

이번에는 직접 nginx config 파일을 작성하고, letsencrypt 사설 인증서도 shell-script를 통해 받아오는 과정을 진행한다.

위 두 자료를 활용해서 진행했고, 이를 조금 더 개선해서 본인만의 예제 자료를 만들었다.

https://github.com/SeungWookHan/docker-nginx-certbot-example

 

GitHub - SeungWookHan/docker-nginx-certbot-example

Contribute to SeungWookHan/docker-nginx-certbot-example development by creating an account on GitHub.

github.com

 

가정

  • 대부분의 인증서는 IP 주소 기반이 아닌, 도메인 기반으로 적용해야하기에, 사전에 진행해야할 것은 도메인과 특정한 컴퓨터(예. 서버 컴퓨터)의 IP 주소를 라우팅해 준 상태를 가정한다.
  • 또한 해당 컴퓨터에서 작업을 진행한다고 가정한다.
  • 마지막으로는 해당 컴퓨터에 docker, docker compose가 설치되어 있다고 가정한다.

이에 대해서는 따로 한번 포스팅 할 예정이다.

 

코드 설명

위 레포지토리의 README을 꼼꼼히 읽는 것 만으로도 진행이 가능하지만 부가 설명을 붙여본다.

data/nginx/app.conf 를 보자

server {
    listen 80;
    server_name spy-stock.com;  # 여기에 자신의 도메인 입력하기
    server_tokens off;

    location /.well-known/acme-challenge/ {
        allow all;
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

# # 백엔드 upstream 설정
# upstream myweb-api {
#     server express-app:80; # 여기에 자신의 컨테이너:포트, 인증서 적용에 성공하면 주석 해제
# }

# server {
#     listen 443 ssl;
#     server_name spy-stock.com; # 여기에 자신의 도메인 입력하기
#     server_tokens off;

#     ssl_certificate /etc/letsencrypt/live/spy-stock.com/fullchain.pem; # 여기에 자신의 도메인 입력하기
#     ssl_certificate_key /etc/letsencrypt/live/spy-stock.com/privkey.pem; # 여기에 자신의 도메인 입력하기
#     include /etc/letsencrypt/options-ssl-nginx.conf;
#     ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

#     location / {
#         proxy_pass  http://myweb-api; # 여기에 자신이 proxy_pass할 upstream 입력하기
#         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;
#     }
# }

여기서 자신의 도메인에 맞춰서 # 여기에 자신의 도메인 입력하기 부분을 채워 넣어주면 된다.

초기에 주석을 달아 설정한 이유는, 초기 인증서 발급 때 에러를 유발하기 때문이다.

인증서를 발급받을 때는 http, 즉 80번 포트에 대한 설정만으로도 충분하다.

 

본인은 spy-stock.com 이라는 도메인을 활용하기에 이와 같이 설정해두었다.

주석 달아져 있는 부분에도 미리 채워 놔준다.

예를 들어 자신의 도메인이, a.com 이라면 위에서 해당 도메인이 들어가는 부분은,

server_name a.com;
server_name a.com;
ssl_certificate /etc/letsencrypt/live/a.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/a.com/privkey.pem;

이와 같이 될 것이다.

 

위 설정은 기본적으로 자신의 도메인으로 모든 http 요청에 대해 https로 디라이렉션하는 nginx 설정이다.

letsencrypt는 도메인에서 .well-known 으로 요청하여, 해당 도메인에 대한 유효성을 검사하고, 특정 응답을 받게되면 이에 대해 인증서를 부여하는 방식이다.

예를 들면, 이와 같다고 생각하면 된다.

“야 너 정말 그 도메인 소유자 맞아?”

“그러면 내가 어떠한 파일을 줄테니까 그거를 한번 띄워봐”

“그러면 내가 거기로 요청해본 뒤에 해당 파일이 정말 오면 인증해줄게”

이를 위해

location /.well-known/acme-challenge/ {
		allow all;
    root /var/www/certbot;
}

해당 라인이 존재한다고 생각하면 된다.

 

또한 주석 부분에서는, 위에서 검증 통과 후 발급받은 인증서를 적용하기 위해

ssl_certificate /etc/letsencrypt/live/spy-stock.com/fullchain.pem; 
ssl_certificate_key /etc/letsencrypt/live/spy-stock.com/privkey.pem; 
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

인증서, 개인키, 추가 설정을 넣어준다고 생각하면 된다.

 

추가적으로 나는 docker-compose.yml에 정의한 express-app 이라는 서비스로 proxy pass 해주고자 upstream으로 myweb-api라는 것을 정의해주었다.

# 백엔드 upstream 설정
upstream myweb-api {
    server express-app:80; # 여기에 자신의 컨테이너:포트, 인증서 적용에 성공하면 주석 해제
}

 

이러한 myweb-api를 아래 라인의 proxy_pass에 적용하였다.

proxy_pass  http://myweb-api; # 여기에 자신이 proxy_pass할 upstream 입력하기

 

upstream의 이름을 바꾼다면 아래 proxy_pass에도 바꿔주면 된다.

역시 upstream 안의 컨테이너:포트도 자신의 입맛에 맞게 설정해준다.

 

그 다음은 docker-compose.yml 파일을 보자

version: "3.9"
services:
  nginx:
    image: nginx:1.15-alpine
    container_name: nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./data/nginx:/etc/nginx/conf.d
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    command: '/bin/sh -c ''while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g "daemon off;"'''
    restart: unless-stopped

  certbot:
    image: certbot/certbot
    container_name: 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;'"
    restart: unless-stopped

  # 내 서비스
  express-app:
    build: .
    image: "express-example:1.0"
    container_name: express-app
    expose:
      - "80"
    volumes:
      - ./src:/usr/src/app/src

 

nginx 컨테이너는 http, https에 대해 모두 포트를 맵핑해준다.

- ./data/nginx:/etc/nginx/conf.d
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot

volumes 부분만 이해하면 되는데,

첫번째 라인은 위에서 설정한 nginx config 파일을 컨테이너 안에서도 활용할 수 있게 해주는 것이다.

두번째, 세번째 라인은 위에서 “그러면 내가 어떠한 파일을 줄테니까 그거를 한번 띄워봐” 를 시전하기 위해 certbot에서 주는 파일을 nginx 컨테이너 안으로도 제공해주기 위함이다.

여기서 한개는 유효성 검사용, 실제 인증서용이라고 생각하면 된다.

 

cerbot 컨테이너도 역시 volumes 부분만 이해하면 된다.

- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot

유효성 검사용, 실제 인증서를 위하여 호스트와 볼륨 맵핑이 된 상태이고, 같은 호스트 볼륨을 공유하는 nginx가 해당 파일을 제공받을 수 있다고 생각하면 된다.

 

두 컨테이너는 각각 commnd, entrypoint 부분이 존재하는데, 이는 인증서가 만료되었을 때 자동으로 갱신하기 위함이다.

certbot에서 12시간 마다 인증서가 갱신되는지 확인하고, nginx에서는 만약 갱신이 되었다면 이를 6시간마다 다시 적용하고 리로드하기 위함이다.

 

express-app 컨테이너는 내가 nginx에서 proxy pass 하고자 하는 서비스이며, 여기서는 간단한 express를 다룬다.

(Springboot, Django, FastAPI 등 자신이 만든 서비스가 해당이 될 수 있을 것이다.)

해당 부분과 Dockerfile 부분에 대한 설명은 생략한다.

 

다음은 init-letsencrypt.sh 스크립트이다.

기존에 원저자가 만들어놓은 것은 명령어가 docker-compose 로 되어 있어서 이 부분만 전체적으로 docker compose 로만 수정해주었다.

굉장히 긴 스크립트이지만 간단하게는 아까 이야기 한 검증 받기 위한 용도라고 생각하면 된다.

domains=(spy-stock.com) # 여기에 자신의 도메인 입력하기
rsa_key_size=4096
data_path="./data/certbot"
email="" # Adding a valid address is strongly recommended
staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits

이 부분만 살펴보면 되는데,

  1. domains 부분에 nginx conf에도 넣어줬던 자신의 도메인을 넣어주기
  2. email 부분에 자신의 유효한 이메일 넣어주기(생략 가능)
  3. staging=1 로 세팅하여 too many certificates 방지(테스트용)

이 정도이다.

 

 

진행

1) 레포지토리 클론

먼저 서버 컴퓨터에서 자신이 원하는 디렉토리에 해당 예제 레포지토리를 받아준다.

git clone https://github.com/SeungWookHan/docker-nginx-certbot-example.git
├── Dockerfile
├── README.md
├── data
│   └── nginx
│       └── app.conf
├── docker-compose.yml
├── init-letsencrypt.sh
├── package.json
├── src
│   └── app.js
└── yarn.lock

레포지토리의 구조는 위와 같다.

1) Dockerfile은 src/app.js의 간단한 express 서버에 대한 이미지다.

2)README.md 생략

3) data/nginx/app.conf 는 샘플로 제공하는 nginx 설정 파일이다.

4) docker-compose.yml은 1)번의 이미지, nginx, certbot 총 3개의 이미지에 대한 컨테이너 설정이다.

5) package.json, yarn.lock 생략

6) src/app.js는 간단한 express 서버이다.

 

2) shell-script 실행

init-letsencrypt.sh을 실행해준다.

이전에 당연히 github README 또는 위의 코드 설명을 보고, nginx conf와 init-letsencrypt 스크립트에 자신의 도메인을 추가해 준 상태여야 한다.

./init-letsencrypt.sh

이때까지는 꼭 data/nginx/app.conf의 주석 해제를 하면 안된다.

또한 인증서 발급을 요청하는데 횟수 제한이 있기 때문에, 테스트를 할 때에는 해당 파일의 staging=0(기본값은 1)으로 설정하여 진행하는 것을 추천한다.

만약 테스트가 성공한다면 다시 기본값인 1로 바꾸어서 스크립트를 다시 한번 실행해야 한다.

 

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/spy-stock.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/spy-stock.com/privkey.pem
This certificate expires on 2022-12-15.
These files will be updated when the certificate renews.

이와 같은 문구가 노출되면 성공적으로 인증서 발급이 된 것이다.

data 디렉토리안에 cerbot이라는 폴더가 생겼을 것이고 그 안에 이것저것 잡다한 파일들이 생겼을 것이다.

 

3) app.conf 주석 해제

data/app.conf 의 주석을 모두 해제해준다.

server {
    listen 80;
    server_name spy-stock.com;  # 여기에 자신의 도메인 입력하기
    server_tokens off;

    location /.well-known/acme-challenge/ {
        allow all;
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

# 백엔드 upstream 설정
upstream myweb-api {
    server express-app:80; # 여기에 자신의 컨테이너:포트, 인증서 적용에 성공하면 주석 해제
}

server {
    listen 443 ssl;
    server_name spy-stock.com; # 여기에 자신의 도메인 입력하기
    server_tokens off;

    ssl_certificate /etc/letsencrypt/live/spy-stock.com/fullchain.pem; # 여기에 자신의 도메인 입력하기
    ssl_certificate_key /etc/letsencrypt/live/spy-stock.com/privkey.pem; # 여기에 자신의 도메인 입력하기
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass  http://myweb-api; # 여기에 자신이 proxy_pass할 upstream 입력하기
        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;
    }
}

 

4) 컨테이너 실행

docker compose up

docker compose up --build

 

5) 확인

해당 도메인으로 접속

 

결론

성공적으로 ssl 인증서가 발급되었고, 해당 도메인으로 접속 시 reverse-proxy 되어 내 서비스 컨테이너에 정상적으로 동작하는 것을 확인할 수 있다.

반응형