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
가정
- 대부분의 인증서는 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
이 부분만 살펴보면 되는데,
- domains 부분에 nginx conf에도 넣어줬던 자신의 도메인을 넣어주기
- email 부분에 자신의 유효한 이메일 넣어주기(생략 가능)
- 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 되어 내 서비스 컨테이너에 정상적으로 동작하는 것을 확인할 수 있다.