본문 바로가기
WIKI 기술 지식 베이스

Caddyfile 개념

원문 보기 위키 갱신

Caddyfile 개념 (Caddyfile Concepts)

이 문서는 HTTP Caddyfile을 자세히 배우도록 도와줘요.

출처: Caddy 공식 문서

본문

이 문서는 HTTP Caddyfile을 자세히 배우도록 도와줘요.

  1. 구조(Structure)

  2. 전역 옵션(Global options)

  3. 주소(Addresses)

  4. Matchers

  5. Placeholders

  6. 스니펫(Snippets)

  7. 이름 있는 라우트(Named Routes)

  8. 주석(Comments)

  9. 환경 변수(Environment variables)

구조 (Structure)

Caddyfile의 구조는 시각적으로 설명할 수 있어요:

{
	email [email protected]
	servers {
		trusted_proxies static private_ranges
	}
}

(snippet) {
	# this is a reusable snippet
	log {
		output file /var/log/access.log
	}
}

example.com {
	@post {
		method POST
	}
	reverse_proxy @post localhost:9001 localhost:9002 {
		lb_policy first
	}
	file_server /static
	import snippet
}

www.example.com {
	redir https://example.com{uri}
	import snippet
}

범례(Legend):

  • 전역 옵션 블록 (Global options block)
  • 스니펫 (Snippet)
  • 사이트 블록 (Site block)
  • matcher 정의 (Matcher definition)
  • 옵션 이름 (Option name)
  • 옵션 값 (Option value)
  • 주석 (Comment)
  • 사이트 주소 (Site address)
  • 지시문 (Directive)
  • matcher 토큰 (Matcher token)
  • 인자 (Argument)
  • 하위 지시문 (Subdirective)

핵심 요점:

  • 선택적 전역 옵션 블록이 파일의 맨 처음일 수 있어요.

  • 스니펫이나 이름 있는 라우트가 다음에 선택적으로 나타날 수 있어요.

  • 그 외에는 Caddyfile의 첫 줄이 항상 서빙할 사이트의 주소예요.

  • 모든 지시문과 matcher는 반드시 사이트 블록 안에 있어야 해요. 사이트 블록 간에 전역 범위나 상속은 없어요.

  • 사이트 블록이 하나뿐이면 중괄호 { }는 선택적이에요.

Caddyfile은 하나 이상의 사이트 블록으로 구성되며, 항상 사이트의 주소 하나 이상으로 시작해요. 주소 앞에 나타나는 지시문은 파서를 혼란스럽게 해요.

블록 (Blocks)

블록을 열고 닫는 것은 중괄호로 해요:

... {
	...
}

  • 여는 중괄호 {는 그 줄의 끝에 있어야 하고 앞에 공백이 있어야 해요.

  • 닫는 중괄호 }는 자기 줄에 있어야 해요.

사이트 블록이 하나뿐이면 중괄호(및 들여쓰기)는 선택적이에요. 단일 사이트를 빠르게 정의하기 위한 편의예요. 예를 들어 이것:

localhost

reverse_proxy /api/* localhost:9001
file_server

은 다음와 동일해요:

localhost {
	reverse_proxy /api/* localhost:9001
	file_server
}

단일 사이트 블록만 있을 때는 취향의 문제예요.

같은 Caddyfile로 여러 사이트를 설정하려면 각각을 중괄호로 감싸 설정을 분리해야 해요:

example1.com {
	root /www/example.com
	file_server
}

example2.com {
	reverse_proxy localhost:9000
}

요청이 여러 사이트 블록과 일치하면, 가장 구체적으로 일치하는 주소를 가진 사이트 블록이 선택돼요. 요청은 다른 사이트 블록으로 연쇄되지는 않아요.

지시문 (Directives)

지시문은 사이트가 서빙되는 방식을 커스터마이즈하는 기능적 키워드예요. 사이트 블록 안에 반드시 나타나야 해요. 예를 들어 완전한 파일 서버 설정은 이렇게 보일 수 있어요:

localhost {
	file_server
}

또는 리버스 프록시:

localhost {
	reverse_proxy localhost:9000
}

이 예시에서 file_server와 reverse_proxy는 지시문이에요. 지시문은 사이트 블록에서 한 줄의 첫 단어예요.

두 번째 예시에서 localhost:9000은 지시문 뒤 같은 줄에 나타나므로 **인자(argument)**예요.

때로 지시문은 자체 블록을 열 수 있어요. **하위 지시문(subdirective)**은 지시문 블록 안의 각 줄 시작에 나타나요:

localhost {
	reverse_proxy localhost:9000 localhost:9001 {
		lb_policy first
	}
}

여기서 lb_policy는 reverse_proxy의 하위 지시문이에요(백엔드 사이에 사용할 부하 분산 정책을 설정).

달리 문서화되지 않는 한, 지시문은 다른 지시문 블록 안에서 사용할 수 없어요. 예를 들어 basic_auth는 file_server 안에서 쓸 수 없어요. 파일 서버가 인증 방법을 모르기 때문이에요. 하지만 route, handle, handle_path 블록 안에서는 지시문을 쓸 수 있어요. 이들은 지시문을 그룹화하도록 특별히 설계됐기 때문이에요.

HTTP Caddyfile이 어댑트될 때 HTTP 핸들러 지시문은 route 블록 안이 아닌 한 특정 기본 지시문 순서에 따라 정렬되므로, route 블록을 제외하고는 지시문의 나타나는 순서는 중요하지 않아요.

토큰과 따옴표 (Tokens and quotes)

Caddyfile은 파싱되기 전에 토큰으로 어휘 분석(lex)돼요. Caddyfile에서 공백은 중요해요. 토큰이 공백으로 구분되기 때문이에요.

종종 지시문은 특정 수의 인자를 기대해요. 단일 인자의 값에 공백이 있으면 두 개의 별도 토큰으로 어휘 분석될 거예요:

directive abc def

이것은 문제가 되어 에러나 예상치 못한 동작을 일으킬 수 있어요.

abc def가 단일 인자의 값이 되도록 하려면 따옴표를 쳐야 해요:

directive "abc def"

따옴표 안에서 따옴표를 써야 한다면 이스케이프할 수도 있어요:

directive "\"abc def\""

따옴표 이스케이프를 피하려면 대신 백틱 ``으로 토큰을 감쌀 수 있어요. 예:

directive `{"foo": "bar"}`

따옴표 안의 토큰에서 다른 모든 문자는 공백, 탭, 줄바꿈을 포함해 문자 그대로 처리돼요. 따라서 여러 줄 토큰이 가능해요:

directive "first line

	second line"

따옴표는 중괄호에서도 작동해요. 따옴표 친 "{" 또는 "}"는 일반 인자이며 블록을 열거나 닫지 않아요:

respond "{"

Heredoc도 지원돼요:

example.com {
	respond <<HTML
		<html>
		  <head><title>Foo</title></head>
		  <body>Foo</body>
		</html>
		HTML 200
}

여는 heredoc 마커는 <<로 시작하고 그 뒤에 아무 텍스트가 와야 해요(대문자 권장). 닫는 heredoc 마커는 같은 텍스트여야 해요(위 예시에서는 HTML). 여는 마커는 필요하면 \<<로 이스케이프해 heredoc 파싱을 막을 수 있어요.

닫는 마커는 들여쓰기할 수 있는데, 그러면 모든 텍스트 줄에서 그만큼 들여쓰기가 제거돼요(PHP에서 영감을 받음). 이는 블록 안에서 가독성에 좋으면서도 토큰 텍스트의 공백을 훌륭히 제어하게 해줘요. 뒤의 줄바꿈도 제거되지만, 닫는 마커 앞에 빈 줄을 추가하면 유지할 수 있어요.

닫는 마커 뒤에는 지시문의 인자로 추가 토큰이 올 수 있어요(위 예시의 상태 코드 200처럼).

전역 옵션 (Global options)

Caddyfile은 선택적으로 키가 없는 특별한 블록인 전역 옵션 블록으로 시작할 수 있어요:

{
	...
}

있으면 config의 맨 처음 블록이어야 해요.

전역으로 적용되거나 특정 사이트에 적용되지 않는 옵션을 설정하는 데 쓰여요. 안에서는 전역 옵션만 설정할 수 있어요. 일반 사이트 지시문은 쓸 수 없어요.

예를 들어 트러블슈팅에 흔히 사용되는 상세 로그를 생성하는 debug 전역 옵션을 활성화하려면:

{
	debug
}

자세한 내용은 전역 옵션 페이지를 읽어보세요.

주소 (Addresses)

주소는 항상 사이트 블록의 맨 위에 나타나며, 보통 Caddyfile의 첫 번째 항목이에요.

유효한 주소의 예시들:

주소 (Address) 효과 (Effect)
example.com 관리되는 공개적으로 신뢰받는 인증서로 HTTPS
*.example.com 관리되는 와일드카드 공개 인증서로 HTTPS
localhost 관리되는 로컬 신뢰 인증서로 HTTPS
http:// http_port의 영향을 받는 HTTP catch-all
https:// https_port의 영향을 받는 HTTPS catch-all
http://example.com Host matcher가 있는 명시적 HTTP
example.com:443 https_port 기본값과 일치해 HTTPS
:443 https_port 기본값과 일치해 HTTPS catch-all
:8080 비표준 포트의 HTTP, Host matcher 없음
localhost:8080 유효한 도메인을 가져서 비표준 포트의 HTTPS
https://example.com:443 HTTPS지만, https://와 :443을 둘 다 갖는 건 중복
127.0.0.1 로컬 신뢰 IP 인증서로 HTTPS
http://127.0.0.1 IP 주소 Host matcher가 있는 HTTP(localhost 거부)

사이트 주소에 호스트명이나 IP 주소가 포함되면 자동 HTTPS가 활성화돼요. 하지만 이 동작은 순전히 암시적이라 명시적 설정을 결코 덮어쓰지 않아요.

예를 들어 사이트 주소가 http://example.com이면 scheme이 명시적으로 http://이므로 auto-HTTPS가 활성화되지 않아요.

주소에서 Caddy는 잠재적으로 사이트의 scheme, 호스트, 포트를 추론할 수 있어요. 주소에 포트가 없으면, scheme이 지정됐다면 scheme에 맞는 포트를 선택하거나 기본 포트 443을 가정해요.

호스트명을 지정하면 Host 헤더가 일치하는 요청만 수락돼요. 즉 사이트 주소가 localhost이면 Caddy는 127.0.0.1에 대한 요청을 일치시키지 않아요.

와일드카드(*)는 사용할 수 있지만, 호스트명의 정확히 한 라벨만 나타낼 수 있어요. 예를 들어 *.example.com은 foo.example.com과 일치하지만 foo.bar.example.com은 아니고, *는 localhost와 일치하지만 example.com은 아니에요. 실제 예시는 와일드카드 인증서 패턴을 참고해요.

모든 호스트를 잡으려면 주소의 호스트 부분을 생략해요. 예를 들어 그냥 https://로요. 이는 도메인을 미리 알 수 없을 때 On-Demand TLS를 쓸 때 유용해요.

여러 사이트가 같은 정의를 공유하면 모두 함께, 공백과 쉼표로 구분해 나열할 수 있어요(공백이 적어도 하나 필요). 다음 세 예시는 동일해요:

# 쉼표로 구분된 사이트 주소
localhost:8080, example.com, www.example.com {
	...
}

또는

# 공백으로 구분된 사이트 주소
localhost:8080 example.com www.example.com {
	...
}

또는

# 쉼표와 줄바꿈으로 구분된 사이트 주소
localhost:8080,
example.com,
www.example.com {
	...
}

주소는 고유해야 해요. 같은 주소를 두 번 이상 지정할 수 없어요.

Placeholder는 주소에서 쓸 수 없지만, Caddyfile 스타일 환경 변수는 쓸 수 있어요:

{$DOMAIN:localhost} {
	...
}

기본적으로 사이트는 모든 네트워크 인터페이스에 바인딩돼요. 이것을 재정의하려면 bind 지시문이나 default_bind 전역 옵션을 사용해요.

Matchers

HTTP 핸들러 지시문은 기본적으로 모든 요청에 적용돼요(달리 문서화되지 않는 한).

요청 matcher를 사용해 주어진 기준으로 요청을 분류할 수 있어요. matcher로 특정 지시문이 정확히 어떤 요청에 적용되는지 지정할 수 있어요.

matcher를 지원하는 지시문에서 지시문 다음의 첫 인자는 matcher 토큰이에요. 몇 가지 예시:

root *           /var/www  # matcher token: *
root /index.html /var/www  # matcher token: /index.html
root @post       /var/www  # matcher token: @post

matcher 토큰은 모든 요청과 일치하도록 완전히 생략할 수 있어요. 예를 들어 다음 인자가 경로 matcher처럼 보이지 않으면 *를 줄 필요가 없어요.

자세한 내용은 Request Matchers 페이지를 읽어보세요.

Placeholders

Placeholder는 정적 설정에 동적 값을 주입하는 간단한 방법이에요. 지시문과 하위 지시문의 인자로 사용할 수 있어요. 쿠키, 헤더, 경로 같은 요청 데이터에는 아래 약어를 사용해요(예: {cookie.session}은 {http.request.cookie.session}으로 확장됨). 다른 서버에서는 이것을 **변수(variables)**라고 부를 수 있어요.

Placeholder는 양쪽이 중괄호 { }로 둘러싸이고 안에 식별자가 들어가요. 예: {foo.bar}. 여는 placeholder 중괄호는 \{like.this}로 이스케이프해 치환을 막을 수 있어요. Placeholder 식별자는 보통 모듈 간 충돌을 피하기 위해 점으로 네임스페이스화돼요.

어떤 placeholder를 사용할 수 있는지는 컨텍스트에 달려 있어요. 모든 placeholder가 config의 모든 부분에서 사용 가능한 건 아니에요. 예를 들어 HTTP 앱이 설정하는 placeholder는 HTTP 요청 처리와 관련된 config 영역(즉 HTTP 핸들러 지시문과 matcher에서는, 하지만 tls 설정에서는 아님)에서만 사용할 수 있어요. 일부 지시문이나 matcher는 자체 placeholder를 설정할 수도 있고, 뒤따르는 어떤 것들이든 사용할 수 있어요. 일부 placeholder는 전역적으로 사용 가능해요.

Caddyfile에서 아무 placeholder나 쓸 수 있지만, 편의를 위해 Caddyfile 파싱 때 확장되는 동등한 약어 중 일부를 쓸 수도 있어요:

Caddyfile 대체 (Replaces)
{cookie.*} {http.request.cookie.*}
{client_ip} {http.vars.client_ip}
{dir} {http.request.uri.path.dir}
{err.*} {http.error.*}
{file_match.*} {http.matchers.file.*}
{file.base} {http.request.uri.path.file.base}
{file.ext} {http.request.uri.path.file.ext}
{file} {http.request.uri.path.file}
{header.*} {http.request.header.*}
{host} {http.request.host}
{hostport} {http.request.hostport}
{labels.*} {http.request.host.labels.*}
{method} {http.request.method}
{orig_method} {http.request.orig_method}
{orig_uri} {http.request.orig_uri}
{orig_path} {http.request.orig_uri.path}
{orig_dir} {http.request.orig_uri.path.dir}
{orig_file} {http.request.orig_uri.path.file}
{orig_query} {http.request.orig_uri.query}
{orig_?query} {http.request.orig_uri.prefixed_query}
{path.*} {http.request.uri.path.*}
{path} {http.request.uri.path}
{%path} {http.request.uri.path_escaped}
{port} {http.request.port}
{query.*} {http.request.uri.query.*}
{query} {http.request.uri.query}
{%query} {http.request.uri.query_escaped}
{?query} {http.request.uri.prefixed_query}
{re.*} {http.regexp.*}
{remote_host} {http.request.remote.host}
{remote_port} {http.request.remote.port}
{remote} {http.request.remote}
{rp.*} {http.reverse_proxy.*}
{resp.*} {http.intercept.*}
{scheme} {http.request.scheme}
{tls_cipher} {http.request.tls.cipher_suite}
{tls_client_certificate_der_base64} {http.request.tls.client.certificate_der_base64}
{tls_client_certificate_pem} {http.request.tls.client.certificate_pem}
{tls_client_fingerprint} {http.request.tls.client.fingerprint}
{tls_client_issuer} {http.request.tls.client.issuer}
{tls_client_serial} {http.request.tls.client.serial}
{tls_client_subject} {http.request.tls.client.subject}
{tls_version} {http.request.tls.version}
{upstream_hostport} {http.reverse_proxy.upstream.hostport}
{uri} {http.request.uri}
{%uri} {http.request.uri_escaped}
{vars.*} {http.vars.*}

모든 config 필드가 placeholder를 지원하는 건 아니지만, 기대할 법한 곳에서는 대부분 지원해요. 그 필드들에 placeholder 지원이 명시적으로 추가되어야 해요. 플러그인 저자는 이 기사를 읽고 자신의 모듈에 placeholder 지원을 추가하는 법을 배울 수 있어요.

Placeholder는 보통 그것을 지원하는 모듈이 런타임에 평가해요. 즉 import 지시문이나 Caddyfile 환경 변수({$ENV}) 같은 Caddyfile 파싱 시점 기능과는 달라요. config가 로드되는 동안 컴파일·검증되는 필드(정규 표현식 matcher 패턴 등)는 입력 값으로 런타임 placeholder를 지원하지 않을 수 있어요. Caddyfile이 어댑트되기 전에 config 텍스트를 제공해야 할 때는 import나 {$ENV}를 사용해요.

스니펫 (Snippets)

이름을 괄호로 감싸서 스니펫이라는 특별한 블록을 정의할 수 있어요:

(logging) {
	log {
		output file /var/log/caddy.log
		format json
	}
}

그런 다음 특별한 import 지시문을 사용해 필요할 때마다 어디서나 이것을 재사용할 수 있어요:

example.com {
	import logging
}

www.example.com {
	import logging
}

import 지시문은 그 자리에 다른 파일을 포함하는 데도 사용할 수 있어요. 인자가 정의된 스니펫과 일치하지 않으면 파일로 시도돼요. 여러 파일을 import하는 glob도 지원해요. 특수한 경우로, Caddyfile 어디에나(다른 지시문의 인자로는 제외) 사이트 블록 밖을 포함해 나타날 수 있어요:

{
	email [email protected]
}

import sites/*

import된 설정(스니펫 또는 파일)에 인자를 전달하고 다음과 같이 사용할 수 있어요:

(snippet) {
	respond "Yahaha! You found {args[0]}!"
}

a.example.com {
	import snippet "Example A"
}

b.example.com {
	import snippet "Example B"
}

⚠️ 실험적(Experimental) | v2.9.x+

import된 스니펫에 선택적 블록을 전달하고 다음과 같이 사용할 수도 있어요:

(snippet) {
	{block}
	respond "OK"
}

a.example.com {
	import snippet {
		header +foo bar
	}
}

b.example.com {
	import snippet {
		header +bar foo
	}
}

자세한 내용은 import 지시문 페이지를 읽어보세요.

이름 있는 라우트 (Named Routes)

⚠️ 실험적(Experimental)

이름 있는 라우트는 스니펫과 비슷한 문법을 사용해요. 사이트 블록 밖에 정의되는 특별한 블록이며, 이름을 사이에 두고 &(로 시작해 )로 끝나요.

&(app-proxy) {
	reverse_proxy app-01:8080 app-02:8080 app-03:8080
}

그런 다음 이 이름 있는 라우트를 어떤 사이트에서든 재사용할 수 있어요:

example.com {
	invoke app-proxy
}

www.example.com {
	invoke app-proxy
}

같은 route가 여러 사이트에서 필요하거나, 같은 route를 호출하기 위해 여러 다른 matcher 조건이 필요할 때 메모리 사용을 줄이는 데 특히 유용해요.

각 이름 있는 라우트는 고유한 이름을 가져야 해요. 이름 있는 라우트는 스니펫이나 파일을 import할 수 있고, 다른 이름 있는 라우트를 invoke할 수 있어요.

자세한 내용은 invoke 지시문 페이지를 읽어보세요.

주석 (Comments)

주석은 #로 시작해 줄 끝까지 이어져요:

# Comments can start a line
directive  # or go at the end

주석용 해시 문자 #는 토큰 중간에 나타날 수 없어요(즉 앞에 공백이 있거나 줄 시작에 있어야 해요). 이 덕분에 URI나 다른 값 안에서 따옴표 없이 해시를 사용할 수 있어요.

환경 변수 (Environment variables)

설정이 환경 변수에 의존한다면 Caddyfile에서 쓸 수 있어요:

{$ENV}

이 형태의 환경 변수는 Caddyfile 파싱이 시작되기 전에 치환되므로, 빈 값(즉 ""), 부분 토큰, 완전한 토큰, 심지어 여러 토큰과 줄로 확장될 수 있어요.

예를 들어 환경 변수 UPSTREAMS="app1:8080 app2:8080 app3:8080"는 여러 토큰으로 확장돼요:

example.com {
	reverse_proxy {$UPSTREAMS}
}

환경 변수를 찾지 못했을 때의 기본 값을 :를 변수 이름과 기본 값 사이의 구분자로 사용해 지정할 수 있어요:

{$DOMAIN:localhost} {

}

환경 변수의 치환을 런타임까지 연기하고 싶다면 {env.*} 표준 placeholder를 사용할 수 있어요. 단, 모든 config 매개변수가 이 placeholder를 지원하는 건 아니라는 점을 주의해요. 모듈 개발자가 치환을 수행하는 코드 한 줄을 추가해야 하기 때문이에요. 동작하지 않는 것 같으면 지원을 요청하는 이슈를 제기해 주세요.

예를 들어 caddy-dns/cloudflare 플러그인이 설치되어 있고 DNS 챌린지를 설정하고 싶다면, CLOUDFLARE_API_TOKEN 환경 변수를 이렇게 플러그인에 전달할 수 있어요:

{
	acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}

Caddy를 systemd 서비스로 실행한다면, 환경 변수를 정의하도록 서비스 오버라이드를 설정하는 이 지침을 참고해요.

Docker secrets처럼 비밀 번호를 파일로 Caddy에 제공한다면, {env.*}가 지원되는 곳이면 어디서든 {file.*} placeholder로 런타임에 읽을 수 있어요. 이렇게 하면 Caddy의 프로세스 환경에서 비밀 번호를 빼낼 수 있는데, 프로세스 환경은 자식 프로세스가 상속하고 일부 진단 도구에 보이기 때문이에요:

{
	acme_dns cloudflare {file./run/secrets/cloudflare_api_token}
}

/나 . 없는 {file.<name>}은 {file.ext}처럼 요청 경로 일부의 약어이므로 절대 경로를 사용해요.

더 알아보기 (Learn more)