[DevOps] init-letsencrypt.sh 분석 (매번 찾기 귀찮아서 정리)
Nginx + Let's Encrypt 닭과 달걀 문제 해결 스크립트 분석 및 개인 메모.
맨날 서버 셋팅할 때마다 구글링해서 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 적용할 때 항상 걸리는 데드락이 있다.
- Nginx를 켜려면 SSL 인증서가 있어야 함. (
nginx.conf설정 때문에) - 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
해결 로직 요약
- Dummy 인증서 생성: 아무거나 만들어서 일단 파일 경로만 맞춰줌.
- Nginx 실행: 가짜라도 파일이 있으니까 에러 없이 켜짐.
- Dummy 삭제 & 진짜 요청: Nginx 켜진 상태에서 가짜 지우고 Certbot으로 진짜 요청.
- 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)다. 끊김 없이 인증서만 교체.
결론
별거 아닌 스크립트지만, 이거 없으면 수동으로 해야 할 작업이 너무 많다. 다음에 또 까먹으면 구글링하지 말고 이거 보고 복붙하자.
마침.
다른 글 보기
NAT 인스턴스가 왜 필요함? (패킷: 어디로 가야하오)
ALB 뒤에 숨긴 인스턴스가 인터넷이 안 되는 이유. IGW, Private Subnet, 그리고 t3.micro NAT 인스턴스를 활용한 격리 기록.
아, 당신이 ALB시군요... (feat. 퍼블릭 IP 한도 초과)
퍼블릭 IP 부족 사태로 인한 ALB 도입과 최적화 기록.
쇼핑몰? 그까짓거 그냥 하지~ 라고 할 뻔;
쇼핑몰 페이지, 개발 이외의 것들
초기창업패키지 사업 지원.
실패한 건 실패한거고, 다음으로 넘어가야지. 이번엔 진짜 제대로.