무료 HTTPS 인증서 붙이는 법 — Caddy와 DuckDNS로 도메인 없이 시작하기
- 공유 링크 만들기
- X
- 이메일
- 기타 앱

서버를 하나 띄웠는데 주소창에 "주의 요함"이 뜬다면, 도메인을 사거나 인증서를 결제할 필요는 없어요. DuckDNS로 무료 서브도메인을 받고 Caddy를 올리면 인증서가 알아서 발급되고 90일마다 알아서 갱신됩니다.
이 글은 실제로 그렇게 돌고 있는 서버에서 값을 뽑아 정리한 겁니다. 설정이 얼마나 짧은지, 그리고 짧아서 오히려 놓치는 함정 세 개를 같이 적었어요.
📌 3줄 결론
- 설정 파일은 도메인 한 줄, 전달할 주소 한 줄. 인증서 관련 설정은 한 글자도 안 씁니다
-localhost라고 쓰면 502가 납니다. IPv6로 붙기 때문이에요
- 인증서 자동 갱신은 도메인이 계속 내 서버를 가리킨다는 전제 위에 있습니다. 그 전제를 지키는 장치가 따로 필요합니다
확인일 2026-08-14 · 대상: 제가 직접 운영하는 리눅스 서버 1대
작성·검증 방식
ARION 운영자가 실제 서버의 인증서·포트·서비스 설정을 확인했습니다. AI는 목차와 문장 정리에만 보조로 사용했고, 명령어 출력과 수치는 비밀값을 제거한 로컬 증거 파일https-caddy-duckdns-2026-08-14.txt와 공식 문서를 다시 대조했습니다.
검증 범위 한눈에 보기
| 확인 항목 | 직접 확인한 값 | 이 글에서 보장하지 않는 것 |
|---|---|---|
| Caddy 상태 | v2.11.4 · active · enabled | 모든 배포판의 설치 명령이 같다는 뜻은 아님 |
| 인증서 | Let's Encrypt · 2026-11-04 만료 | 이후 갱신이 반드시 성공한다는 보장은 아님 |
| 리스닝 포트 | Caddy가 80·443 사용 | 공유기·클라우드 방화벽 설정은 환경별로 다름 |
| DuckDNS 갱신 | cron 0건 · systemd 타이머 0건 | 고정 IP 환경에도 갱신 장치가 항상 필요하다는 뜻은 아님 |
이 표는 성공 사례만 보여주기 위한 것이 아닙니다. 현재 정상인 부분과 아직 자동화되지 않은 부분을 함께 기록한 실측 결과입니다.
왜 Caddy인가 — 인증서 설정을 안 쓴다
Caddy는 공개 도메인이 붙은 사이트에 기본으로 HTTPS를 켭니다. 인증서를 받고, 저장하고, 만료 전에 갱신하는 걸 알아서 합니다. 공식 문서가 이걸 "Automatic HTTPS"라고 부르고, 설정에서 끄지 않는 한 켜져 있어요.
제 서버에서 실제로 쓰는 설정 파일 전체입니다. 인증서라는 단어가 한 번도 안 나옵니다.
arionflow.duckdns.org {
reverse_proxy 127.0.0.1:8000
log {
format filter {
fields {
request>headers>Authorization delete
request>headers>Cookie delete
}
}
}
}
첫 줄이 도메인, 둘째 줄이 "받은 요청을 내부 8000번 포트로 넘겨라"입니다. 나머지는 로그에서 인증 헤더를 지우는 설정이에요. 이건 필수가 아니지만 넣어두는 게 좋습니다. 안 지우면 토큰이 로그 파일에 평문으로 쌓입니다.
실제로 뭐가 돌고 있나
설명만으로는 못 믿으니 값을 뽑았습니다.
Caddy v2.11.4 · active · enabled
issuer Let's Encrypt
subject arionflow.duckdns.org
발급 2026-08-06
만료 2026-11-04
90일짜리입니다. Let's Encrypt 인증서의 유효기간이 90일이고, Caddy가 만료 전에 알아서 다시 받습니다.
포트도 확인했어요.
LISTEN *:80 caddy
LISTEN *:443 caddy
80번도 Caddy가 잡고 있어야 합니다. 인증서를 받을 때 인증 기관이 80번으로 확인을 오기 때문이에요. 80번을 막아두면 인증서 발급이 실패합니다.
인증서 파일은 /var/lib/caddy/.local/share/caddy/certificates/ 아래에 도메인 이름으로 .crt, .key, .json 세 개가 생깁니다. 손으로 만질 일은 없지만 어디 있는지는 알아두는 게 좋아요.
함정 1 — localhost 라고 쓰면 502가 난다
가장 많이 겪는 문제입니다. 전달할 주소를 localhost:8000이라고 쓰면 Caddy가 IPv6 주소 [::1]로 붙습니다. 그런데 백엔드가 IPv4(0.0.0.0)만 듣고 있으면 연결이 거부되고, 브라우저에는 502가 뜹니다.
증상이 고약한 이유는 설정이 틀린 것처럼 안 보이기 때문이에요. localhost는 누가 봐도 맞는 말이고, 서버에서 curl localhost:8000을 해보면 잘 됩니다. curl은 IPv4로 붙거든요.
해결은 한 글자입니다.
reverse_proxy 127.0.0.1:8000
이름 대신 숫자로 씁니다. 이러면 어느 쪽으로 붙을지 헷갈릴 여지가 없어요.
함정 2 — 유닛에 EnvironmentFile 이 없으면 .env 가 안 먹는다
이건 Caddy가 아니라 뒤에 있는 앱 쪽 함정인데, HTTPS를 붙이고 나서 "왜 설정이 반영이 안 되지"로 겪게 되는 자리라 같이 적습니다.
.env 파일에 값을 넣었는데 앱이 못 읽는 경우가 있어요. systemd 서비스 유닛에 EnvironmentFile= 이 없으면 systemd는 .env 의 존재를 모릅니다. 파일을 만들어두면 알아서 읽어줄 것 같지만 그렇지 않아요.
제 서버 유닛에도 그 줄이 없습니다. 대신 시작 스크립트가 직접 읽도록 돼 있어요.
set -a
source ./.env
set +a
둘 중 하나는 있어야 합니다. 확인은 이렇게 합니다.
systemctl cat 서비스이름 | grep EnvironmentFile
아무것도 안 나오면 시작 스크립트 쪽을 보세요. 양쪽 다 없으면 .env 는 그냥 텍스트 파일일 뿐입니다.
함정 3 — 인증서 갱신은 도메인이 살아 있다는 전제 위에 있다
이게 제일 중요합니다. 그리고 이 글을 쓰려고 값을 뽑다가 제 서버에서 발견한 겁니다.
DuckDNS는 원래 집 인터넷처럼 IP가 자주 바뀌는 환경을 위한 서비스예요. 그래서 보통은 IP가 바뀔 때마다 DuckDNS에 새 주소를 알려주는 갱신 장치를 걸어둡니다.
제 서버에서 찾아봤더니 이랬습니다.
$ crontab -l | grep -i duckdns
(cron 에 duckdns 없음)
$ systemctl list-timers --all | grep -i duck
(타이머에도 없음)
하나도 없습니다. 지금 당장은 문제가 없어요. 클라우드 서버라 IP가 고정이고, 도메인이 서버를 정확히 가리키고 있는 것도 확인했습니다.
문제는 사슬입니다. 인증서 자동 갱신은 인증 기관이 그 도메인으로 서버를 찾아올 수 있을 때만 성공합니다. 도메인이 엉뚱한 곳을 가리키면 갱신이 조용히 실패하고, 90일이 지난 어느 날 사이트 전체가 경고 화면이 됩니다.
서버를 옮기거나 다시 만들면 IP가 바뀝니다. 그때 도메인을 손으로 고치는 걸 잊으면, 그 사실은 인증서가 만료되는 날에야 드러납니다. 자동화가 하나 있고 그 앞에 사람이 기억해야 하는 단계가 있으면, 그건 자동화가 아니라 시한폭탄입니다.
직접 확인하는 법
지금 돌고 있는 인증서를 보는 명령입니다. 도메인 자리만 바꾸세요.
echo | openssl s_client -connect 내도메인:443 -servername 내도메인 2>/dev/null | openssl x509 -noout -issuer -dates
도메인이 진짜 내 서버를 가리키는지 확인합니다. 두 값이 같아야 해요.
getent hosts 내도메인
curl -s https://api.ipify.org
80과 443을 누가 듣고 있는지 봅니다.
ss -ltnp | grep -E ':80 |:443 '
주소 갱신 장치가 있는지 확인합니다. 아무것도 안 나오면 함정 3에 해당합니다.
crontab -l | grep -i duckdns
systemctl list-timers --all | grep -i duck
서버 자동화, 다음 단계까지 같이 읽기
- 인증서처럼 조용히 멈출 수 있는 작업은 장기 액세스 토큰을 systemd 타이머로 점검한 기록과 같은 방식으로 만료·실패 알림을 붙일 수 있습니다.
- 서버 전체 상태를 한 번에 확인하려면 이상이 있을 때만 알리는 일일 운영 체크의 점검 항목에 인증서 만료일과 DNS 응답을 추가하세요.
자주 묻는 질문 (FAQ)
Q. 도메인을 꼭 사야 하나요?
아니요. DuckDNS는 무료 서브도메인을 줍니다. 다만 주소가 이름.duckdns.org 형태라 고객용 서비스에는 어울리지 않아요. 개인 프로젝트나 API 엔드포인트로는 충분합니다.
Q. 인증서가 90일이면 3개월마다 손대야 하나요?
아니요. Caddy가 만료 전에 알아서 갱신합니다. 사람이 할 일은 갱신이 아니라 갱신이 실패했을 때 알아채는 장치를 두는 겁니다. 만료일을 감시 항목에 넣어두세요.
Q. Nginx로도 되나요?
됩니다. 다만 인증서 발급·갱신을 위한 도구를 따로 붙이고 설정도 직접 씁니다. Caddy는 그걸 기본값으로 갖고 있는 게 차이예요.
Q. 80번 포트를 닫아도 되나요?
닫으면 인증서 발급 방식에 따라 실패할 수 있습니다. 제 서버는 80과 443을 같은 프로세스가 듣고 있고, 80으로 온 요청은 HTTPS로 넘깁니다.
Q. 함정 3은 고쳤나요?
아직입니다. 지금 IP가 고정이라 당장 안 터지는데, 그래서 오히려 잊기 쉬운 자리예요. 이 글을 쓰면서 발견했고 조치는 아직 안 했습니다. 고치면 그 기록을 따로 남기겠습니다.
실행 체크리스트
- DuckDNS에서 서브도메인을 만들고 서버 IP를 넣는다
- 서버에 Caddy를 설치하고 도메인 블록 한 줄로 설정한다
- 전달할 주소는 이름이 아니라
127.0.0.1로 쓴다 - 로그에서
Authorization·Cookie헤더를 지우는 설정을 넣는다 - 80과 443이 열려 있는지
ss -ltnp로 확인한다 openssl s_client로 실제 발급된 인증서의 만료일을 눈으로 본다- 도메인이 내 서버를 가리키는지 정기적으로 확인할 장치를 둔다 — 이게 없으면 나머지가 다 무의미해진다
- 인증서 만료일을 아침 점검 항목에 넣는다
참고한 공식 문서
- Caddy 자동 HTTPS 동작 방식: Automatic HTTPS
- 인증서 유효기간과 갱신 시점: Let's Encrypt FAQ
- DuckDNS 주소 갱신 규격: DuckDNS spec
EnvironmentFile=등 서비스 실행 환경: systemd.exec 매뉴얼
다음 글 예고
다음은 플레이스토어에 앱을 올릴 때 만나는 versionCode 중복 오류입니다. 로컬 저장소의 숫자를 믿었다가 스토어에 이미 쓰인 번호와 부딪힌 기록이에요. 이것도 손에 있는 값이 진실이 아니었던 경우입니다.
참고 자료
출처 확인일: 2026-08-14
댓글