웹서버
브라우저가 말을 걸어 주는 쉘 스크립트 20줄.
웹서버는 요청을 기다렸다가 응답을 보내는 프로그램일 뿐입니다. 그것을 증명하기 위해 bash 와 netcat, 이름 있는 파이프만으로 서버를 만듭니다 — 한 줄도 빠짐없이 읽을 수 있습니다. 그다음 실제 사이트를 HTTPS 까지 붙여 돌리는 Caddy 로 넘어갑니다.
웹서버는 대답하는 프로그램입니다
장비 랙과 상표를 걷어 내면, 웹서버는 한 가지 일을 되풀이하는 프로그램 하나입니다. 누가 연결하기를 기다렸다가, 무엇을 달라는지 읽고, 글을 돌려주고, 다시 기다립니다.
"서버"라는 낱말은 두 가지를 가리키고 사람들이 늘 섞어 씁니다. 데이터센터에 놓인 기계도 서버고, 에서 귀를 기울이는 프로그램도 서버입니다. 여기서 말하는 것은 프로그램 쪽이고, 그것은 지금 앞에 있는 노트북에서도 똑같이 잘 돕니다.
손님 한 명, 네 걸음
- 귀 기울이기프로그램이 포트 하나를 차지하고 기다립니다 — 예제에서는 8084 입니다. 포트는 기계에 번호가 붙은 문이라, 두 프로그램이 같은 문을 잡을 수는 없습니다.
- 요청 읽기가 연결해 몇 줄의 글을 보냅니다. 첫 줄에 어떤 방법으로 어떤 경로를 원하는지가 담깁니다. 예를 들면 경로 / 를 GET 해 달라는 식입니다.
- 판단하기무엇을 보낼지 정합니다. 디스크에서 파일을 읽거나, 코드를 돌리거나, 에 묻습니다. 예제는 늘 같은 파일만 보내기 때문에 20줄에 들어갑니다.
- 응답하고 끊기상태 줄과 몇 개의 헤더, 빈 줄 하나, 그리고 내용을 적어 보낸 뒤 연결을 끊고 다음 손님을 위해 처음으로 돌아갑니다.
여러분이 이름을 들어 본 모든 서버가 — Apache · Nginx · Caddy, 지금 이 사이트를 돌리는 Node 프로세스까지 — 위의 네 걸음을 합니다. 다른 점은 그것을 수천 명에게 동시에, 안전하게, HTTPS 로 한다는 것뿐입니다. 모양은 똑같습니다.
쉘 스크립트 한 장으로 만드는 웹서버
아래가 bash 로 쓴 완전한 웹서버입니다. 를 열고, 요청 줄을 읽고, 기록을 남기고, index.html 을 제대로 된 응답으로 돌려줍니다. 스무 줄, 없음, 그리고 는 차이를 알아채지 못합니다.
무거운 일은 두 명령이 맡습니다. netcat(nc) 이 네트워크를 말하고, mkfifo 가 이름 있는 파이프를 만들어 우리가 쓴 답을 nc 가 붙들고 있는 연결로 되돌려 보냅니다.
#!/bin/bash
PORT=8084
PIPE=simple-web-server-pipe
rm -f "$PIPE"
mkfifo "$PIPE"
response() {
read -r method path _
echo "[$(date)] $method $path" >&2
index=$(cat index.html)
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n%s' \
"$index"
}
while true; do
response < "$PIPE" | nc -l "$PORT" > "$PIPE"
done
한 줄씩 뜯어보기
PORT=8084귀를 기울일 포트입니다. 1024 보다 큰 번호는 관리자 권한 없이 쓸 수 있고, 8084 는 이미 쓰고 있는 것과 부딪힐 일이 적습니다.PIPE=simple-web-server-pipe파이프 파일의 이름입니다. 폴더에 보통 파일처럼 나타나지만 안에 저장되는 것은 없습니다 — 두 명령 사이로 자료를 지나가게 할 뿐입니다.mkfifo "$PIPE"이름 있는 파이프(FIFO)를 만듭니다. 바로 윗줄이 지난번에 남은 것을 지우는 이유는, mkfifo 가 이미 있는 파일 위에 덮어쓰기를 거부하기 때문입니다.read -r method path _요청의 첫 줄을 읽어 방법 · 경로 · 나머지로 나눕니다. 그 뒤에 오는 브라우저의 헤더들은 이 서버가 그냥 흘려보냅니다.echo "[$(date)] $method $path" >&2기록을 한 줄 남깁니다. 표준 오류로 방향을 돌린 덕분에 이 글이 브라우저로 가지 않고 터미널에만 나타납니다.index=$(cat index.html)지금 폴더의 index.html 을 읽어 변수에 담습니다. 서버를 그 파일이 있는 폴더에서 실행해야 하는 이유가 이것입니다.printf 'HTTP/1.1 200 OK\r\n…'응답을 씁니다. 상태 줄, 내용의 종류, 여기서 연결을 끝낸다는 헤더, 그리고 빈 줄과 페이지. 헤더와 내용을 가르는 것이 그 빈 줄이고, HTTP 는 모든 줄 끝에 캐리지리턴과 줄바꿈을 함께 요구합니다.response < "$PIPE" | nc -l "$PORT" > "$PIPE"이것이 동작하게 만드는 요령입니다. nc 는 파이프에서 우리 응답을 읽고, 브라우저의 요청을 그 파이프에 다시 써 넣습니다. 두 쪽이 서로에게 먹이를 주는 셈입니다.while true; do … done반복문입니다. nc 는 연결 하나를 처리하면 끝나 버리므로, 이 줄이 없으면 서버는 손님 한 명만 받고 멈춥니다.
요청이 함수에 닿아야 함수의 답이 nc 에 닿을 수 있습니다 — 고리 모양입니다. 그런데 쉘의 파이프라인은 한 방향으로만 흐릅니다. 이름 있는 파이프가 그 고리를 닫아 줍니다. 응답은 파이프라인을 타고 nc 로 가고, 요청은 FIFO 를 타고 되돌아옵니다. 두 명령이 양방향으로 말을 주고받게 하는 가장 작은 방법입니다.
직접 띄워 보기
스크립트를 server.sh 로 저장하고 옆에 index.html 을 두고 실행합니다. 그 터미널은 켜 둔 채로, 두 번째 터미널에서 요청을 보내 보세요.
# 1. create an index.html in the same folder
echo '<h1>Hello</h1>' > index.html
# 2. save the script above as server.sh and start it
bash server.sh
# 3. open it in a browser (or check from a second terminal)
open http://localhost:8084
curl http://localhost:8084
브라우저를 새로고침하면서 첫 터미널을 보세요. 요청 하나마다 기록이 한 줄씩 쌓입니다. Control-C 로 멈추고, 파이프 파일이 남아 있으면 지웁니다. 리눅스의 nc 는 포트 앞에 -p 를 더 붙여야 할 수 있고, macOS 에 기본으로 들어 있는 판은 적힌 그대로 동작합니다.
여기 쓰인 쉘 명령 배우기선을 타고 실제로 오가는 것
연결을 열어 보면 여러분이 손으로 칠 수도 있었을 평범한 글자가 흐릅니다. 쉘 웹서버가 가능한 이유가 이것이고, 웹에서 생긴 문제를 짐작이 아니라 읽어서 풀 수 있는 이유도 이것입니다.
브라우저가 보내는 요청
GET / HTTP/1.1
Host: localhost:8084
User-Agent: Mozilla/5.0
Accept: text/html
- 첫 줄이 우리 서버가 읽는 유일한 부분입니다. 방법(GET 은 가져오기, POST 는 보내기), 경로, 그리고 프로토콜 판입니다.
- Host 는 어느 사이트를 원하는지 말합니다. 서버 하나가 한 주소에서 수백 개 을 품을 수 있고, 그것을 구별하는 것이 이 헤더입니다.
- 나머지는 의 자기소개입니다. 자신이 어떤 프로그램인지, 어떤 형식을 읽을 수 있는지, 어떤 언어를 좋아하는지. 서버는 쓸 수도 있고 무시할 수도 있습니다.
서버가 보내는 응답
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Connection: close
<h1>Hello</h1>
- 상태 줄입니다. 프로토콜 판과, 일이 어떻게 됐는지 알리는 숫자. 200 은 요청이 잘 됐다는 뜻입니다.
- Content-Type 은 뒤따라올 것을 어떻게 읽어야 하는지 알려 줍니다. HTML 을 엉뚱한 종류로 보내면 브라우저가 태그를 페이지가 아니라 글자 그대로 보여 줍니다.
- charset 부분은 실제로는 선택이 아닙니다. 이것이 없으면 한글은 깨진 글자로 도착하기 쉽습니다.
- 그리고 빈 줄 하나. 그 뒤는 전부 내용입니다. 헤더와 페이지를 가르는 경계는 오직 그 빈 줄뿐입니다.
알아 둘 만한 상태 코드
| 코드 | 뜻 |
|---|---|
200 OK | 성공입니다. 요청한 것이 응답에 담겨 있습니다. 200 번대는 모두 이런 종류의 성공을 뜻합니다. |
301 Moved Permanently | 주소가 영구히 옮겨졌습니다. 새 주소는 Location 헤더에 있고 브라우저가 알아서 따라갑니다. 사이트가 http 손님을 https 로 보낼 때 쓰는 방법입니다. |
404 Not Found | 서버는 멀쩡한데 그 경로가 없습니다. 400 번대는 문제가 요청 쪽에 있다는 뜻입니다. |
500 Internal Server Error | 멀쩡한 요청을 처리하다 서버가 넘어졌습니다. 500 번대는 고쳐야 할 사람이 여러분이라는 뜻이고, 자세한 내용은 서버 기록에 있습니다. |
이 서버가 못 하는 일
예제는 정직한 교재이고 형편없는 서비스입니다. 어디서 모자란지 정확히 아는 것이 곧 진짜 서버가 대신 풀어 주는 문제를 아는 것입니다.
- 한 번에 한 명입니다. nc 는 연결 하나만 다루므로, 그것이 바쁜 동안 나머지는 모두 기다립니다.
- 경로를 무시합니다. 무엇을 달라고 하든 index.html 만 돌아옵니다 — 길 안내도 없고 두 번째 페이지도 없습니다.
- 전부 로 보냅니다. 그림도 스타일 파일도 내려받기도 저마다의 내용 종류가 필요한데, 이 는 딱 하나만 압니다.
- 가 없습니다. 모든 것이 맨몸으로 지나가므로, 중간의 어떤 네트워크든 읽거나 바꿔치기할 수 있습니다.
- 안전장치가 전혀 없습니다. 요청 크기를 재지 않고, 시간 제한도 없으며, 함부로 확장하면 남이 정한 파일 경로를 그대로 읽어 버립니다.
내 컴퓨터에서 돌려 HTTP 를 눈으로 보는 데 쓰고, 남이 찾아올 것에는 진짜 서버를 씁니다. 다음 절이 그 진짜 서버이고, 시작하기는 이것보다 크게 어렵지도 않습니다.
Caddy — 읽을 수 있는 채로 남은 진짜 웹서버
Caddy 는 실행 파일 하나로 된 현대적인 웹서버입니다. 쉘 예제 바로 다음에 배울 값어치가 있는 이유는, 읽을 수 있는 몇 줄이면 돌아간다는 느낌을 그대로 유지하면서 쉘 판이 못 하던 것을 전부 해결하기 때문입니다.
가장 내세우는 기능은 자동 입니다. 진짜 이름만 주면 인증서를 받아 오고, 설치하고, 만료 전에 갱신하고, http 손님을 https 로 넘겨 줍니다. 다른 서버에서는 몇 달마다 되풀이하는 잡일입니다.
설치하기
# macOS
brew install caddy
# Ubuntu / Debian
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
sudo apt update && sudo apt install caddy
# Docker alone is enough
docker run -p 8080:80 -v "$PWD":/usr/share/caddy caddy
명령 한 줄로 폴더 내놓기
설정 파일이 필요 없습니다. index.html 이 있는 폴더로 가서 한 줄만 치면 됩니다 — 같은 네트워크의 누군가에게 페이지를 보여 주는 가장 빠른 방법입니다.
# serve this folder's index.html on port 8080
caddy file-server --listen :8080
# add a directory listing for folders without an index.html
caddy file-server --listen :8080 --browse
Caddyfile
오래 둘 것이라면 Caddyfile 을 씁니다. 그냥 글자 파일이고, 사이트 하나가 네 줄로 끝납니다.
example.com {
root * /var/www/html
file_server
encode gzip zstd
}
example.com {사이트 주소입니다. 여기에 진짜 도메인을 적으면 Caddy 가 HTTPS 인증서를 알아서 마련하고, localhost 를 적으면 로컬 작업용으로 평범한 http 를 내놓습니다.root * /var/www/html파일이 디스크의 어디에 있는지입니다. 별표는 이 사이트의 모든 경로에 적용한다는 뜻입니다.file_server그 파일들을 실제로 내놓으라는 줄입니다. 이 줄이 없으면 Caddy 는 요청에 아무것도 주지 않습니다.encode gzip zstd보내기 전에 응답을 압축합니다. 한 낱말인데, 느린 회선에서 페이지가 눈에 띄게 빨리 도착합니다.
Caddy 는 Let's Encrypt 에 무료 인증서를 요청합니다. 그러려면 도메인의 가 내 기계를 가리켜야 하고 80 · 443 포트에 바깥에서 닿을 수 있어야 합니다. 앞에 있는 다른 프록시가 그 포트를 대신 받고 있으면 확인이 실패해 인증서가 끝내 오지 않습니다 — 처음 Caddy 를 세울 때 자물쇠가 초록색이 되지 않는 가장 흔한 이유입니다.
리버스 프록시 — 요청을 넘겨주기
대부분의 사이트는 디스크의 파일이 아니라 어느 로컬 에서 귀를 기울이는 응용 프로그램입니다. 는 바깥의 요청을 받아 그 프로그램에 건네고 답을 되돌려 주는 서버입니다. 한 줄이면 됩니다.
getpes.com {
reverse_proxy pes-web:3000
}
dl.getpes.com {
root * /srv/files
file_server browse
}
기계 한 대가 여러 사이트를 품는 방법도 이것입니다. 443 포트는 한 프로그램만 가질 수 있으므로 Caddy 가 그것을 쥐고, 손님이 어느 도메인을 찾았는지 읽어 각각 다른 프로그램으로 넘깁니다.
알아야 할 명령들
| 명령 | 하는 일 |
|---|---|
caddy run | 앞에서 실행하며 기록을 터미널에 뿌립니다. 설정을 아직 맞춰 가는 동안 원하는 것이 이쪽입니다. |
caddy start | 뒤로 보내고 프롬프트를 돌려줍니다. 잠깐 보여 줄 때는 괜찮지만, 제대로 된 서버는 시스템 서비스로 띄웁니다. |
caddy reload | 연결을 하나도 끊지 않고 바뀐 설정을 다시 읽습니다. Caddyfile 을 고쳤다고 재시작할 필요가 없습니다. |
caddy fmt --overwrite | 파일 서식을 제자리에서 바로잡습니다. 들여쓰기가 보기보다 중요하고, 이 명령이 그 다툼을 끝냅니다. |
caddy validate | 아무것도 띄우지 않고 설정에 잘못이 있는지 검사합니다. 돌고 있는 서버를 reload 하기 전에 먼저 돌립니다. |
PES 서버에서는 Caddy 하나가 80 · 443 포트를 쥐고 도메인 이름으로 길을 가릅니다. 본 사이트는 응용 프로그램 컨테이너로, 내려받기 서브도메인은 디스크에서 곧바로 내놓습니다. 새 사이트를 더하는 일은 위와 같은 파일 하나를 두고 reload 하는 것이 전부입니다 — 재시작도 없고, 포트를 두고 다투는 두 번째 프로그램도 없습니다.
Docker: 상자에 담은 Caddy 로 진짜 사이트를 인터넷에
는 프로그램과 그 프로그램이 도는 데 필요한 모든 것 — 라이브러리, 설정, 작은 운영체제까지 — 를 하나에 담고, 그 이미지를 로 실행합니다. 컨테이너는 내 노트북에서도 빌린 서버에서도 똑같이 움직이므로 "제 컴퓨터에서는 되는데요"라는 오래된 핑계가 사라집니다.
Caddy 는 이미 파일 하나인데 굳이 왜 도커를 쓸까요? 진짜 에서는 곧 여러 가지가 함께 돌기 때문입니다 — 사이트, 앱, . 도커는 그 하나하나에 자기 상자를 주고, 같은 몇 가지 명령으로 켜고 끄고 새로 바꾸고 지웁니다. 서버에 다른 것을 설치하지 않고 이웃도 건드리지 않습니다.
먼저 알아 둘 낱말 네 개
- 이미지프로그램과 그것에 필요한 모든 것을 얼려 둔 꾸러미입니다. 내려받거나(caddy:2) Dockerfile 이라는 조리법 파일로 직접 만듭니다. 실행하는 동안 바뀌지 않습니다.
- 컨테이너실행 중인 이미지입니다. 이미지 하나로 컨테이너를 여러 개 띄울 수 있고, 컨테이너를 지워도 이미지는 그대로입니다 — 다음에 켜면 새것으로 시작합니다.
- 포트 연결컨테이너에는 바깥에서 보이지 않는 자기만의 포트가 있습니다. -p 8080:80 은 내 컴퓨터의 8080 포트를 컨테이너 안의 80 포트에 이어서 브라우저가 닿을 수 있게 합니다.
- 볼륨컨테이너가 자기 안에 쓴 파일은 컨테이너를 지우면 함께 사라집니다. 볼륨이나 컨테이너에 연결한 내 폴더는 남는 저장소입니다 — 페이지, 설정, Caddy 의 인증서가 거기에 삽니다.
도커 설치
Mac 이나 Windows PC 에서는 docker.com 에서 Docker Desktop 을 설치하고 한 번 실행해 둡니다. 그러면 터미널에서 docker 명령이 됩니다. 리눅스 서버에서는 도커의 공식 스크립트가 한 줄로 모두 설치합니다. 다음으로 넘어가기 전에 확인 명령 두 개가 모두 답해야 합니다.
# macOS · Windows: install Docker Desktop from docker.com and start it once
# Ubuntu / Debian server: Docker's official install script
curl -fsSL https://get.docker.com | sh
# check that both commands answer
docker --version
docker compose version
명령 한 줄로 Caddy 띄우기
index.html 이 있는 폴더로 가서 아래 한 줄을 실행합니다. 처음에는 도커가 Caddy 이미지를 내려받고, 그다음부터는 1초쯤이면 켜집니다. http://localhost:8080 을 열면 내 페이지가 보입니다.
# serve this folder at http://localhost:8080 (Control-C stops it)
docker run --rm -p 8080:80 -v "$PWD":/usr/share/caddy caddy:2
docker run이미지로 컨테이너를 만들고 실행합니다.--rm멈추자마자 컨테이너를 지웁니다. 멈춘 컨테이너가 쌓이지 않게 하려는 것입니다. 이미지는 남으므로 다음 실행도 똑같이 빠릅니다.-p 8080:80내 컴퓨터의 8080 포트를 Caddy 가 기다리는 컨테이너 안의 80 포트에 잇습니다. 왼쪽 숫자는 마음대로 고르고, 오른쪽 숫자는 이미지가 정해 둔 값입니다.-v "$PWD":/usr/share/caddy지금 폴더($PWD)를 컨테이너 안의 /usr/share/caddy 로 보이게 합니다 — 공식 이미지가 기본으로 내놓는 폴더입니다. 여기서 파일을 고치면 다음 새로 고침에 바로 보입니다.caddy:2실행할 이미지 — 공식 Caddy 이미지의 2.x 버전입니다. :2 꼬리표를 빼면 도커는 latest 를 가져오는데, 어느 날 큰 버전이 바뀌어 발밑이 흔들릴 수 있습니다.
Control-C 로 멈춥니다. 잠깐 보기에는 그만이지만 명령이 길고, 터미널을 닫는 순간 잊힙니다. 오래 둘 것은 파일에 적어 둡니다 — 그것이 다음 프로젝트입니다.
프로젝트 1: HTML 홈페이지를 폴더에서 인터넷까지
프로젝트 전체가 작은 파일 세 개입니다. index.html 은 페이지, Caddyfile 은 Caddy 가 무엇을 내놓을지, compose.yaml 은 도커가 Caddy 를 어떻게 실행할지를 적습니다. Docker Compose 가 그 파일을 읽으므로 긴 docker run 명령을 다시 칠 일이 없습니다.
my-site/
├── compose.yaml
├── conf/
│ └── Caddyfile
└── site/
└── index.html
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./conf:/etc/caddy
- ./site:/srv
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
compose.yaml 한 줄씩
image: caddy:2실행할 이미지 — 앞에서와 같은 공식 Caddy 이미지입니다.restart: unless-stopped컨테이너가 죽거나 서버가 다시 켜지면, 일부러 멈추기 전까지 도커가 알아서 다시 띄웁니다."80:80" · "443:443"기계의 80(http) · 443(https) 포트를 Caddy 에 엽니다. 인증서를 받고 http 손님을 https 로 넘기려면 둘 다 필요합니다."443:443/udp"같은 443 포트를 UDP 로 한 번 더 엽니다. 브라우저가 가장 새로운 HTTP/3 를 쓸 수 있게 합니다. 빼도 해롭지 않고, 그러면 사이트는 HTTP/2 로 돕니다../conf:/etc/caddy내 conf 폴더가 Caddy 가 Caddyfile 을 찾는 /etc/caddy 가 됩니다. Caddy 문서가 파일 하나 대신 폴더를 연결하는 까닭은, 많은 편집기가 저장할 때 파일을 새것으로 갈아 끼워서 파일 하나만 연결해 두면 옛 내용이 계속 보이기 때문입니다../site:/srv내 site 폴더가 Caddyfile 이 내놓는 /srv 가 됩니다. 디스크에서 페이지를 고치면 다시 켜지 않아도 Caddy 가 바로 새 판을 내놓습니다.caddy_data:/data도커가 맡아 두는 이름 있는 볼륨입니다. Caddy 가 HTTPS 인증서와 키를 여기에 두므로 컨테이너를 지우고 다시 만들어도 살아남습니다. 옆의 caddy_config 도 Caddy 의 자동 설정을 같은 방식으로 지킵니다.
먼저 내 컴퓨터에서
Caddyfile 의 주소 자리에 :80 을 씁니다. "어떤 이름으로 물어 오든 80 포트에서 답하라"는 뜻으로, 인증서 없는 맨 http 입니다. 도메인이 없는 내 컴퓨터에는 이것이 딱 맞습니다. 그다음 켜고 http://localhost 를 엽니다.
:80 {
root * /srv
file_server
}
cd my-site
docker compose up -d # start Caddy in the background
open http://localhost # or type the address into a browser
docker compose logs -f # watch requests arrive (Control-C stops watching)
인터넷에 올리기
- 서버 빌리기공인 IP 주소가 있는 VPS 면 무엇이든 됩니다 — 작고 싼 것으로 충분합니다. 위의 스크립트로 도커를 설치하고, 방화벽이 80 · 443 포트를 들여보내는지 확인합니다.
- 도메인이 서버를 가리키게을 산 곳에서 에 A 레코드를 더합니다 — 이름은 example.com, 값은 서버의 IP 주소입니다. 인터넷 전체가 바뀐 값을 보기까지 몇 분에서 몇 시간이 걸릴 수 있습니다.
- 진짜 주소 적기Caddyfile 의 :80 을 내 도메인으로 바꿉니다. 이 한 곳을 바꾸는 것이 자동 HTTPS 를 켭니다 — Caddy 가 인증서를 요청하고, 설치하고, 갱신하고, http 손님을 https 로 넘깁니다.
- 복사하고 켜기rsync 로 폴더를 서버에 복사하고, ssh 로 들어가 docker compose up -d 로 켭니다. 기록을 지켜보다가 인증서를 받았다는 줄이 나오면 사이트가 열린 것입니다.
example.com {
root * /srv
file_server
encode gzip zstd
}
아래의 203.0.113.10 은 예시용으로 비워 둔 주소이니 내 서버의 IP 로 바꿉니다. 나중에 페이지를 고칠 때는 index.html 을 고치고 같은 rsync 를 다시 실행합니다. site 폴더가 컨테이너에 연결되어 있어서 아무것도 다시 켜지 않아도 다음 새로 고침에 새 페이지가 나옵니다.
# on your computer: copy the project folder to the server
rsync -av my-site/ root@203.0.113.10:~/my-site/
# on the server: start it — Caddy fetches the HTTPS certificate by itself
ssh root@203.0.113.10
cd ~/my-site
docker compose up -d
docker compose logs -f caddy
프로젝트 2: React 홈페이지
는 작게 나눠 다시 쓰는 부품(컴포넌트)으로 페이지를 만드는 라이브러리입니다. 배포에서 중요한 것은 하나입니다 — React 프로젝트는 그대로 내놓지 않습니다. 빌드 단계가 그것을 dist 폴더 안의 평범한 HTML · CSS · JavaScript 파일로 바꾸고, 거기서부터는 프로젝트 1 과 같은 일입니다.
아래 명령에는 내 컴퓨터에 Node.js 가 있어야 합니다. Vite 가 바로 돌아가는 React 프로젝트를 만들고, npm run dev 가 작업하는 동안 화면을 보여 주고, npm run build 가 완성된 사이트를 씁니다.
# needs Node.js (nodejs.org) — create a React project with Vite
npm create vite@latest my-react -- --template react
cd my-react
npm install
npm run dev # development server at http://localhost:5173
npm run build # the finished site is written to dist/
방법 A: 내 컴퓨터에서 빌드하고 결과만 복사
가장 쉬운 길은 프로젝트 1 을 그대로 다시 쓰는 것입니다. 빌드한 뒤 dist 폴더만 서버의 site 폴더로 복사합니다. --delete 는 이번 빌드에 없는 지난 빌드의 파일을 지워서 찌꺼기가 쌓이지 않게 합니다.
# way A: build here, copy only the result into Project 1's site folder
npm run build
rsync -av --delete dist/ root@203.0.113.10:~/my-site/site/
React 앱은 index.html 하나에서 모든 페이지를 스스로 그립니다. /about 같은 주소는 디스크의 파일이 아니라서, 도움 없이는 누군가 그 페이지를 새로 고치거나 공유된 링크로 들어오는 순간 Caddy 가 404 로 답합니다. try_files 는 Caddy 에게 "파일이 있으면 그것을, 없으면 index.html 을 주고 React 가 알맞은 페이지를 그리게 하라"고 말합니다.
example.com {
root * /srv
encode gzip zstd
try_files {path} /index.html
file_server
}
방법 B: 두 단계 Dockerfile 로 도커가 빌드하게
더 튼튼한 길은 도커 안에서 빌드하는 것입니다. 그러면 서버든 동료의 컴퓨터든 늘 똑같이 빌드되고, 서버에 Node 를 설치할 사람도 없습니다. Dockerfile 은 두 단계입니다 — 첫 단계는 Node 로 빌드하고, 둘째 단계는 결과만 챙깁니다.
# stage 1: build the React app with Node
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# stage 2: only the finished files go into the Caddy image
FROM caddy:2
COPY Caddyfile /etc/caddy/Caddyfile
COPY --from=build /app/dist /srv
Dockerfile 한 줄씩
FROM node:24-alpine AS build첫 단계는 Node.js 가 든 이미지에서 시작하고, 다음 단계가 여기서 파일을 가져갈 수 있게 이 단계에 build 라는 이름을 붙입니다.COPY package*.json ./ · RUN npm ci패키지 목록만 먼저 복사하고 잠금 파일에 적힌 버전 그대로 설치합니다. 목록이 그대로인 동안 도커가 이 단계를 다시 쓰므로, 다음 빌드부터는 느린 설치를 건너뜁니다.COPY . . · RUN npm run build나머지 소스를 복사하고 빌드합니다. 완성된 사이트가 build 단계 안의 /app/dist 에 생깁니다.FROM caddy:2둘째 단계는 깨끗한 Caddy 이미지에서 다시 시작합니다. 복사하지 않은 것은 첫 단계에서 하나도 따라오지 않습니다.COPY --from=build /app/dist /srvbuild 단계에서 빌드된 파일만 가져와 Caddyfile 이 내놓는 자리에 둡니다. Node 와 node_modules 는 남겨 두므로 최종 이미지가 작습니다.
try_files 를 넣은 Caddyfile 을 Dockerfile 옆에 두고 아래 compose.yaml 을 씁니다. build: . 은 이미지를 내려받는 대신 "이 폴더의 Dockerfile 로 만들라"는 뜻입니다. 서버에서 코드를 clone 하면 명령 하나로 빌드하고 켜고, 고칠 때마다 pull 한 뒤 같은 명령을 다시 실행합니다.
services:
web:
build: .
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
# keep node_modules and old builds out of the image
printf 'node_modules\ndist\n' > .dockerignore
# on the server: get the code, then build and start in one step
git clone https://github.com/you/my-react.git
cd my-react
docker compose up -d --build
# after you push a change: pull it and rebuild
git pull && docker compose up -d --build
자주 쓰는 명령
| 명령 | 하는 일 |
|---|---|
docker compose up -d | compose.yaml 에 적힌 것을 모두 뒤에서 켭니다. compose.yaml 을 고친 뒤 다시 실행하면 바뀐 것만 새로 만듭니다. |
docker compose ps | 이 프로젝트의 컨테이너와 도는지 여부를 보여 줍니다. 사이트가 답하지 않을 때 가장 먼저 봅니다. |
docker compose logs -f | 기록을 실시간으로 따라갑니다. 인증서 문제, 틀린 경로, 죽는 일이 모두 여기에 먼저 나옵니다. Control-C 는 서버가 아니라 지켜보기만 멈춥니다. |
docker compose exec -w /etc/caddy caddy caddy reload | 도는 컨테이너 안에서 Caddy 가 연결 하나 끊지 않고 Caddyfile 을 다시 읽게 합니다. Caddyfile 을 고친 뒤에 씁니다. |
docker compose down | 컨테이너를 멈추고 지웁니다. 이름 있는 볼륨은 남으므로 인증서도 살아남습니다. -v 를 붙이면 그것까지 지우니 피합니다. |
docker compose pull | 더 새 Caddy 이미지를 내려받습니다. 이어서 docker compose up -d 를 실행해야 새것으로 바뀝니다. |
인증서는 caddy_data 볼륨에 삽니다. docker compose down -v 로, 또는 compose.yaml 에서 볼륨을 빼먹어서 그것이 지워지면 Caddy 는 켤 때마다 인증서를 새로 요청해야 합니다. 인증 기관은 한 도메인이 한 주에 받을 수 있는 인증서 수를 제한하므로, 몇 번의 부주의한 재시작으로 사이트가 며칠 동안 HTTPS 없이 지낼 수 있습니다.
스스로 표시하는 칸이에요. 여기서 채점하지는 않아요.
이 단계를 이해한 것으로 표시할까요?
아직 취소할 수 없으니, 정말 이해했을 때만 표시해 주세요.
이해한 것으로 표시했어요
스스로 표시하는 칸이에요. 여기서 채점하지는 않아요.