Docker Nginx Let’s Encrypt 인증서 발급 및 자동갱신

Docker 환경으로 구동하는 nginx에 Let’s Encrypt 무료 SSL 인증서를 적용하셔야 하나요? Docker compose를 사용하여 nginx를 구동하고, let’s encrypt 인증서 발급받고, nginx 서버에 적용하는 방법, 그리고 마지막으로 인증서 자동갱신 방법까지 모두 알아보도록 하겠습니다.

준비물

  • https를 적용하려면 사용하려는 도메인 주소(예, osg.kr)를 가지고 있어야 합니다.
  • docker와 docker compose를 미리 설치해 두셔야 합니다. Ubuntu Docker Compose 설치 방법(22.04 기준)을 참고하시면 docker compose 설치에 도움이 될 거예요.
  • VULTR, AWS, Gabia 등 여러분이 사용할 도메인에 대한 네임서버 설정을 할 수 있어야 합니다.

네임 서버 설정: VULTR 활용

VULTR에서는 네임서버를 무료로 제공하고 있습니다. 만약 도메인을 아직 등록하지 않았다면 VULTR DNS 서비스에 도메인 등록하는 방법을 참고해서 먼저 등록해 주세요.

VULTR외의 다른 네임서버를 사용하신다면 해당 업체의 네임서버를 꼭 설정해 주시기 바랍니다. 인증서 발급을 위해서는 Let’s Encrypt 인증서를 발행하는 서버가 접속할 수 있어야만 하거든요.

여기에서는 네임서버와 관련된 설명은 생략하겠습니다. 각자의 도메인에 대한 네임 서버 설정이 됐다고 가정하고 바로 docker compose 설정으로 넘어가겠습니다.

작동 원리 이해

지금부터 여러분이 사용할 도메인을 A라고 하겠습니다. 우선 Let’s Encrypt 서비스를 통해 A 도메인에 대한 인증서를 발급 받고 싶으시죠?

아래 그림 1을 보면 사용자가 먼저 Let’s Encrypt 서버에 도메인 A에서 사용할 인증서 발급을 요청합니다. 그러면 Let’s Encrypt는 인증문자 C를 주면서 파일 B에 문자를 넣어놓으라고 합니다. 여러분이 요즘 많이 사용하는 휴대전화 본인 인증 단계 같은 겁니다.

서버에 설정을 하고 준비가 다 됐다고 알려주면, Let’s Encrypt 서버에서 해당 인증파일 B를 확인한 후 인증문자 C가 있으면 인증서를 사용자에게 발급해 줍니다. 여기에서 사용자에 해당하는 부분은 certbot이 그 역할을 해 줄 겁니다. 여러분이 할 일은 certbot이 일할 수 있는 환경을 만들고 certbot이 일하게 하는 것입니다.

그림 1. Let's Encrypt 인증서 발급 과정
그림 1. Let’s Encrypt 인증서 발급 과정

인증서를 발급 받으면 다시 Nginx 서버에 도메인 A에 대한 SSL 인증서를 설정해 주고 nginx를 재시작해 주면 모든 설정이 끝납니다. 이후에 웹 브라우저로 도메인 A에 접근하면, SSL 인증서가 적용되어 https를 쓰게 되며 그림 2와 같이 자물쇠 표시를 확인할 수 있게 됩니다.

그림 2. SSL 인증서가 적용되어 https로 접속했을 때 볼 수 있는 자물쇠 아이콘
그림 2. SSL 인증서가 적용되어 https로 접속했을 때 볼 수 있는 자물쇠 아이콘

Docker Compose Nginx 설정

docker compose 없이 nginx 컨테이너를 띄우려면 다음과 같이 꽤 긴 명령어를 입력해야 합니다. 짧다고 느끼실 수 있겠지만, 도커 컨테이너로 nginx를 띄울 때마다 이렇게 입력해야 한다면 지칩니다. 차라리 실행하는 스크립트를 작성해 두는 편이 낫겠죠. 하지만 스크립트 파일이 많아지면 그것 또한 관리하기 힘들어집니다.

docker run --name nginx -d -p 80:80 --restart always nginx:1.22.1-alpine
Python

그래서 저같이 귀찮은 게 싫은 사람들은 docker compose를 사용합니다. docker compose는 compose.yaml 파일 하나에 도커 관련 설정을 모두 끝내고, 명령어 한 줄로 도커 컨테이너를 띄우게 됩니다.

최신 docker compose file 공식 문서에서는 version이 deprecated 상태이므로 명시를 하지 말라고 합니다. 그래서 이 설정에서 version은 넣지 않았습니다. 그리고 최근에는 docker-compose.yaml도 길다고 compose.yaml 사용을 권장합니다. 위에서 사용한 docker 명령어를 사용한 cli 방식을 compose.yaml 파일로 구성하면 다음과 같습니다.

services:
  nginx:
    container_name: nginx
    image: nginx:1.22.1-alpine
    restart: always
    ports:
      - 80:80
YAML

이제 docker compose 명령어로 nginx를 띄우는 것은 아래의 명령어와 같이 매우 간단합니다. 아래의 명령어를 사용하면 compose.yaml에 설정된 모든 컨테이너를 띄우게 됩니다.

docker compose up -d
ShellScript

docker compose ps를 통해서 nginx 컨테이너가 작동하고 있는 것을 확인할 수 있습니다.

docker compose ps
ShellScript

웹 브라우저에서 http://localhost에서 확인해 보면 그림 3과 같이 nginx 초기 웹 페이지가 잘 뜨는 것을 볼 수 있습니다.

그림 3. 웹 브라우저에서 엔진엑스 작동 확인
그림 3. 웹 브라우저에서 엔진엑스 작동 확인

docker compose down 명령어를 사용하면 docker compose up -d와는 반대로 compose.yaml에 설정된 모든 컨테이너를 중지시키고 삭제합니다.

docker compose down
ShellScript

Certbot을 위한 compose.yaml 설정

certbot은 인증서를 /etc/letsencrypt 디렉토리 하위에 보관합니다. certbot을 위해서 라인 9와 같이 볼륨 설정을 추가해 줍니다.

또한 라인 10은 letsencrypt로 소유자 확인 과정에서 임시로 파일을 만들고 삭제할 디렉토리입니다. 통상적으로 /var/lib/letsencrypt를 이용하며, 우리도 이 경로를 사용할 것이므로 설정해 줍니다.

라인 11에서는 앞으로 사용할 사이트들의 서버 설정 파일을 모아놓을 경로가 설정된 sites.conf를 잡아줍니다.

마지막으로 라인 12와 같이 nginx의 sites-enabled 디렉토리를 볼륨으로 잡아줍니다. 라인 12의 ${DOCKER_COMPOSE_ROOT}는 .env에서 DOCKER_COMPOSE_ROOT로 설정된 상수 값을 가져와서 사용하도록 해 두었습니다.

services:
  nginx:
    container_name: nginx
    image: nginx:1.22.1-alpine
    restart: always
    ports:
      - 80:80
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt
      - /var/lib/letsencrypt:/var/lib/letsencrypt
      - ${DOCKER_COMPOSE_ROOT}/nginx/sites.conf:/etc/nginx/conf.d/sites.conf
      - ${DOCKER_COMPOSE_ROOT}/nginx/sites-enabled:/etc/nginx/sites-enabled
YAML

위의 코드에서 사용한 sites.conf의 내용은 아래와 같습니다. 아래와 같이 적어주면 nginx 컨테이너에서 해당 경로 하위에 있는 모든 설정을 include 하게 됩니다. 매번 conf를 추가할 때마다 굳이 compose.yaml 파일을 수정할 필요 없이 nginx만 재시작 해주면 됩니다.

include /etc/nginx/sites-enabled/*;
ShellScript

Certbot을 위한 nginx 설정

이제 ${DOCKER_COMPOSE_ROOT}/nginx/sites-enabled/sitename.conf라는 파일에서 해당 도메인에 대한 처리를 할 수 있도록 준비합니다. 만약 이미 사용하는 설정 파일이 있다면 해당 파일을 수정하면 됩니다.

certbot이 소유자 확인을 위해 사용하는 경로는 http://도메인명/.well-known/acme-challenge/임의의파일입니다. /var/lib/letsencrypt는 webroot 경로로 사용자 인증을 위한 임의의 파일이 생성되는 공간으로 사용할 것입니다. server_name의 userdomain.com은 당연히 여러분이 사용할 도메인으로 바꾸셔야 합니다.

server {
  listen 80;
  server_name userdomain.com;
  location / {
    rewrite ^ https://$server_name$request_uri? permanent;
  }
  location /.well-known/acme-challenge {
    root /var/lib/letsencrypt/;
  }
}
ShellScript

이제 configuration 파일 설정이 다 됐다면 아래와 같이 nginx를 restart 해서 해당 configuration이 적용되게 해 주세요.

docker restart nginx

Let’s Encrypt 인증서 발급

과거에는 dockerhub에 certbot/certbot 이미지가 있는 줄 모르고 certbot 컨테이너 없이 nginx에 certbot 패키지를 설치해서 사용했었는데, 이제는 아예 certbot 도커 공식 이미지가 생겼습니다.

도커 기반으로 이용하다 보니 certbot이 nginx 컨테이너에서 분리된 게 매우 깔끔해진 느낌이 드네요.

dockerhub의 certbot/certbot 이미지로 띄우는 컨테이너는 상시 띄우는 컨테이너가 아닙니다. 따라서 아래 명령을 사용해서 certbot 컨테이너를 생성해서 인증서를 발급받고, 컨테이너는 바로 삭제할 것입니다.

docker run -it --rm --name certbot \
            -v "/etc/letsencrypt:/etc/letsencrypt" \
            -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
            certbot/certbot \
            certonly \
            --webroot \
            -w /var/lib/letsencrypt \
            -d userdomain.com \
            --agree-tos
ShellScript

단회적으로 이용할 것이므로 굳이 docker compose에 설정할 필요가 없습니다. 여기에서도 userdomain.com 도메인명은 여러분이 사용할 도메인명으로 바꿔서 사용하면 됩니다.

아무 문제가 없다면 그림 4와 같이 성공적으로 인증서를 발급받았다고 나옵니다. NEXT STEPS에서는 발행된 인증서는 만료되기 전에 재발급 받아야 한다고 알려줍니다.

그림 4. Let's Encrypt 인증서 발급 성공
그림 4. Let’s Encrypt 인증서 발급 성공

그림 3에서는 인증서가 저장된 경로(Certificate is saved at)와 개인키가 저장된 경로(Key is saved at)가 나와 있습니다. 우선 nginx 서버에 SSL 적용부터 해 보겠습니다.

발급받은 인증서로 Nginx 서버에 SSL 설정

이제 아래 코드와 같이 9-18 라인을 추가해 주면 SSL 설정이 끝납니다. 16라인과 17라인에 각각 그림 3에서 확인한 인증서 경로와 개인키 경로를 적어주면 됩니다.

server {
  listen 80;
  server_name userdomain.com;
  location / {
    rewrite ^ https://$server_name$request_uri? permanent;
  }
}

server {
   listen 443 ssl;
   server_name userdomain.com;
   location / {
       proxy_pass my-server:3000;
   }

   ssl_certificate /etc/letsencrypt/live/userdomain.com/fullchain.pem;
   ssl_certificate_key /etc/letsencrypt/live/userdomain.com/privkey.pem;
}
ShellScript

Let’s Encrypt 인증서 자동 갱신

Let’s Encrypt에서 발급하는 인증서는 유효기간이 3개월이므로 3개월간 무료로 사용할 수 있습니다. 따라서 Let’s Encrypt 인증서 발급 후 3개월이 되기 전에 새로운 인증서로 갱신해 주셔야 합니다.

갱신하는 명령어는 다음과 같습니다.

certbot renew

우리는 certbot/certbot 도커 이미지를 활용하고 있었으니까 다음과 같이 명령어를 실행하면 됩니다. 혹시라도 수동으로 업데이트하는 경우에는 아래 명령어를 사용하면 됩니다.

docker run --rm --name certbot -v "/etc/letsencrypt:/etc/letsencrypt" -v "/var/lib/letsencrypt:/var/lib/letsencrypt" certbot/certbot renew

하지만, 이걸 매번 신경 쓰기는 너무 귀찮습니다. 그리고 너무 자주 업데이트할 필요는 없기 때문에 crontab을 사용해서 자동화하는 편이 가장 적절합니다. crontab 편집은 다음 명령어를 사용합니다.

crontab -e

crontab에 매월 1일, 15일, 25일 오전 5시 0분에 인증서가 업데이트 되게 한다면 매우 편리하겠죠?

crontab 편집창이 열리면 제일 뒤에 아래의 스크립트를 작성해 줍니다. /var/log/certbot_renew.log로 리뉴얼된 기록도 남기도록 하죠. 어차피 빈번하게 로그가 기록되는 게 아니니 큰 문제는 없을 것입니다. 뭐 이것조차 필요 없다면 >> 이후 부분은 생략하시면 됩니다.

0 5 5,15,25 * * /usr/bin/docker run --rm --name certbot -v "/etc/letsencrypt:/etc/letsencrypt" -v "/var/lib/letsencrypt:/var/lib/letsencrypt" certbot/certbot renew >> /var/log/certbot_renew.log

그런데 빠진 게 하나 있습니다. 인증서 정보가 업데이트 됐으면, 해당 인증서로 nginx를 재시작해야 합니다.

두 가지 방법이 있습니다. 하나는 crontab에 nginx 재시작 스크립트를 하나 더 추가하는 것입니다. 또 다른 하나는 인증서 리뉴얼과 nginx 재시작 명령어를 하나의 스크립트로 작성하고 crontab에서는 해당 스크립트만 실행시키는 거죠. 어느쪽으로 처리하셔도 상관 없습니다.

따로 스크립트 만들면 설명이 길어지니 여기에서는 crontab에 nginx 재시작하는 스크립트를 하나 더 작성하겠습니다. 별도의 스크립트를 만들고 싶은 분은 직접 해보시기 바랍니다.

10 5 5,15,25 * * /usr/bin/docker restart nginx

인증서 리뉴얼은 보통 몇 초면 끝나니까 약 10분 후에 nginx를 재시작한다면 큰 무리가 없을 듯 싶습니다.

정리

여기까지 nginx와 certbot/certbot 도커허브의 이미지의 certbot을 활용하여 Let’s Encrypt 인증서 발급도 받고 인증서를 자동으로 갱신하는 과정까지 처리해 보았습니다. 막히는 부분이나 궁금한 부분 있으면 댓글에 남겨주세요. 저도 함께 고민해 보겠습니다.

관련자료

dockerhub에 있는 nginx 리포지터리, certbot/certbot 리포지터리입니다. certbot 공식 문서를 참고했습니다. 그리고 혹시 crontab에서 docker 실행이 안 되는 분들은 -it 옵션이 있어서일 수 있습니다. StackExchange의 Docker run is not working as cron command를 확인해 보시기 바랍니다.

같이 읽으면 좋은 글

Leave a Comment