[DevOps] init-letsencrypt.sh 분석 (매번 찾기 귀찮아서 정리)

Nginx + Let's Encrypt 닭과 달걀 문제 해결 스크립트 분석 및 개인 메모.

Jun Noh

맨날 서버 셋팅할 때마다 구글링해서 init-letsencrypt.sh 찾고, 이게 뭐였더라 동작 원리 다시 읽어보는 게 너무 귀찮아서 한 번 정리해보려고 한다.

보통 어느 정도 규모가 있는 팀이나 기업에서는 AWS ALB 같은 걸 앞단에 둬서 인증서 관리를 위임하거나, Cloudflare 같은 걸 써서 SSL Offloading을 하곤 한다.

돈만 내면 그게 제일 편하고 안전하다.

하지만 나 같은 개인 개발자가 운영하는 소규모 서비스에 매달 로드밸런서 비용을 태우는 건 배보다 배꼽이 더 큰 짓이다.

그렇다고 Caddy나 Traefik 같은 모던한 도구를 쓰자니, 익숙하지 않은 설정 파일이랑 씨름해야 하는 게 또 일이다. 결국 돌고 돌아 가장 익숙하고 비용 0원인 Docker + Nginx + Let’s Encrypt 조합으로 오게 된다.

문제는 이 조합을 Docker Compose로 구성할 때 발생하는 닭과 달걀의 딜레마다.

Nginx 80 포트로 띄우고, Certbot 돌려서 인증서 받고, 다시 443 포트 열고, docker-compose 파일 수정하고… 이 짓을 매번 수동으로 할 수는 없다.

이 스크립트 하나면 해결되는데, 가끔 커스텀해야 할 때 동작 원리를 자꾸 까먹어서 기록용으로 남기려고 한다.

닭이 먼저냐, 달걀이 먼저냐

Docker로 Nginx 띄우고 SSL 적용할 때 항상 걸리는 데드락이 있다.

  1. Nginx를 켜려면 SSL 인증서가 있어야 함. (nginx.conf 설정 때문에)
  2. SSL 인증서를 발급받으려면 Nginx가 켜져 있어야 함. (Let’s Encrypt가 도메인 인증하려고 접속하니까)

인증서 없어서 Nginx 못 켜고, Nginx 안 켜져서 인증서 못 받는 상황.

init-letsencrypt.sh 스크립트는 **“가짜(Dummy) 인증서”**를 먼저 만들어서 Nginx를 속이고 띄운 다음, 진짜로 갈아치우는 방식이다.

전체 스크립트 (init-letsencrypt.sh)

이 스크립트의 원본은 wmnnd/nginx-certbot 레포지토리에서 가져온 것이다. Docker로 Nginx와 Certbot을 사용해 SSL을 자동 발급받는 가장 대중적인 예제라고 볼 수 있다.

필요할 때마다 복사해서 쓰려고 남겨둔다. (원본은 docker-compose v1 문법이지만, 내 환경에 맞춰 docker compose v2로 수정해둠)

#!/bin/bash

if ! [ -x "$(command -v docker)" ]; then
  echo 'Error: docker is not installed.' >&2
  exit 1
fi

domains=(example.org www.example.org)
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

if [ -d "$data_path" ]; then
  read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision
  if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then
    exit
  fi
fi


if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then
  echo "### Downloading recommended TLS parameters ..."
  mkdir -p "$data_path/conf"
  curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf"
  curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem"
  echo
fi

echo "### Creating dummy certificate for $domains ..."
path="/etc/letsencrypt/live/$domains"
mkdir -p "$data_path/conf/live/$domains"
docker compose run --rm --entrypoint "\
  openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\
    -keyout '$path/privkey.pem' \
    -out '$path/fullchain.pem' \
    -subj '/CN=localhost'" certbot
echo


echo "### Starting nginx ..."
docker compose up --force-recreate -d nginx
echo

echo "### Deleting dummy certificate for $domains ..."
docker compose run --rm --entrypoint "\
  rm -Rf /etc/letsencrypt/live/$domains && \
  rm -Rf /etc/letsencrypt/archive/$domains && \
  rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot
echo


echo "### Requesting Let's Encrypt certificate for $domains ..."
#Join $domains to -d args
domain_args=""
for domain in "${domains[@]}"; do
  domain_args="$domain_args -d $domain"
done

# Select appropriate email arg
case "$email" in
  "") email_arg="--register-unsafely-without-email" ;;
  *) email_arg="-m $email" ;;
esac

# Enable staging mode if needed
if [ $staging != "0" ]; then staging_arg="--staging"; fi

docker compose run --rm --entrypoint "\
  certbot certonly --webroot -w /var/www/certbot \
    $staging_arg \
    $email_arg \
    $domain_args \
    --rsa-key-size $rsa_key_size \
    --agree-tos \
    --force-renewal" certbot
echo

echo "### Reloading nginx ..."
docker compose exec nginx nginx -s reload

해결 로직 요약

  1. Dummy 인증서 생성: 아무거나 만들어서 일단 파일 경로만 맞춰줌.
  2. Nginx 실행: 가짜라도 파일이 있으니까 에러 없이 켜짐.
  3. Dummy 삭제 & 진짜 요청: Nginx 켜진 상태에서 가짜 지우고 Certbot으로 진짜 요청.
  4. Nginx 리로드: 진짜 인증서 들어왔으니 설정 다시 로드.

코드 뜯어보기

나중에 또 수정할 일 있을까봐 핵심만 메모.

1. 설정 변수

domains=(example.com www.example.com)
rsa_key_size=4096
data_path="./certbot"
email="" 
staging=0 

staging=1로 테스트 안 하고 바로 돌렸다가 Rate Limit 걸리면 멍청하게 기다려야 하니까 주의할 것.

2. 보안 설정 다운로드

if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] ...
  curl -s ... > ...

Certbot 팀이 권장하는 보안 설정을 받아온다. 이건 건드리지 말고 그냥 쓰는 게 낫다.

3. Dummy 인증서 만들기

docker compose ... run --rm --entrypoint "\
  openssl req -x509 ... -subj '/CN=localhost'" certbot

openssl로 셀프 서명된 인증서를 대충 만든다. 오로지 Nginx가 시작될 때 “파일 없음” 에러를 뱉지 않게 하기 위함임.

4. Nginx 시작 & 진짜 인증서 발급

docker compose ... up --force-recreate -d nginx
docker compose ... run --rm --entrypoint "\
  rm -Rf /etc/letsencrypt/live/$domains && ..." certbot
docker compose ... run --rm --entrypoint "\
  certbot certonly --webroot -w /var/www/certbot ..." certbot

가짜 인증서로 Nginx 띄우고 -> 가짜 삭제 -> --webroot 모드로 진짜 발급. --webroot가 핵심이다. 실행 중인 Nginx의 특정 디렉토리를 통해 인증 파일을 검증받는다.

5. Nginx 리로드

docker compose ... exec nginx nginx -s reload

재시작(restart)이 아니라 리로드(reload)다. 끊김 없이 인증서만 교체.

결론

별거 아닌 스크립트지만, 이거 없으면 수동으로 해야 할 작업이 너무 많다. 다음에 또 까먹으면 구글링하지 말고 이거 보고 복붙하자.

마침.

다른 글 보기