Caddy와 Discourse 사이에 TCP 대신 Unix 소켓을 사용하는데, 간헐적으로 'unix:' 오류가 발생합니다

로그에는 다음과 같은 내용이 표시됩니다:

PG::InvalidTextRepresentation (ERROR: invalid input syntax for type inet: "unix:" LINE 7: client_ip = 'unix:', ^ ) lib/mini_sql_multisite_connection.rb:109:in 'MiniSqlMult

rack-mini-profiler (4.0.1) lib/patches/db/pg/alias_method.rb:109:in 'PG::Connection#exec'
rack-mini-profiler (4.0.1) lib/patches/db/pg/alias_method.rb:109:in 'PG::Connection#async_exec'
mini_sql (1.6.0) lib/mini_sql/postgres/connection.rb:217:in 'MiniSql::Postgres::Connection#run'
mini_sql (1.6.0) lib/mini_sql/active_record_postgres/connection.rb:38:in 'block in MiniSql::ActiveRecordPostgres::Connection#run'
mini_sql (1.6.0) lib/mini_sql/active_record_postgres/connection.rb:34:in 'block in MiniSql::ActiveRecordPostgres::Connection#with_lock'
activesupport (8.0.5.1) lib/active_support/concurrency/null_lock.rb:9:in 'ActiveSupport::Concurrency::NullLock#synchronize'
mini_sql (1.6.0) lib/mini_sql/active_record_postgres/connection.rb:34:in 'MiniSql::ActiveRecordPostgres::Connection#with_lock'
mini_sql (1.6.0) lib/mini_sql/active_record_postgres/connection.rb:38:in 'MiniSql::ActiveRecordPostgres::Connection#run'
lib/mini_sql_multisite_connection.rb:109:in 'MiniSqlMultisiteConnection#run'
mini_sql (1.6.0) lib/mini_sql/postgres/connection.rb:196:in 'MiniSql::Postgres::Connection#exec'
app/models/user_auth_token.rb:244:in 'UserAuthToken#rotate!'
lib/auth/default_current_user_provider.rb:260:in 'Auth::DefaultCurrentUserProvider#refresh_session'
lib/current_user.rb:52:in 'CurrentUser#refresh_session'
app/controllers/application_controller.rb:91:in 'ApplicationController#perform_refresh_session'
activesupport (8.0.5.1) lib/active_support/callbacks.rb:361:in 'block in ActiveSupport::Callbacks::CallTemplate::MethodCall#make_lambda'
activesupport (8.0.5.1) lib/active_support/callbacks.rb:207:in 'ActiveSupport::Callbacks::Filters::After#call'
activesupport (8.0.5.1) lib/active_support/callbacks.rb:563:in 'block in ActiveSupport::Callbacks::CallbackSequence#invoke_after'
activesupport (8.0.5.1) lib/active_support/callbacks.rb:563:in 'Array#each'
activesupport (8.0.5.1) lib/active_support/callbacks.rb:563:in 'ActiveSupport::Callbacks::CallbackSequence#invoke_after'
activesupport (8.0.5.1) lib/active_support/callbacks.rb:134:in 'block in ActiveSupport::Callbacks#run_callbacks'
app/controllers/application_controller.rb:444:in 'block in ApplicationController#with_resolved_locale'
i18n (1.14.8) lib/i18n.rb:354:in 'I18n::Base#with_locale'
app/controllers/application_controller.rb:444:in 'ApplicationController#with_resolved_locale'
activesupport (8.0.5.1) lib/active_support/callbacks.rb:129:in 'block in ActiveSupport::Callbacks#run_callbacks'
app/controllers/application_controller.rb:1126:in 'ApplicationController#ensure_dont_cache_page'
activesupport (8.0.5.1) lib/active_support/callbacks.rb:129:in 'block in ActiveSupport::Callbacks#run_callbacks'
activesupport (8.0.5.1) lib/active_support/callbacks.rb:140:in 'ActiveSupport::Callbacks#run_callbacks'
actionpack (8.0.5.1) lib/abstract_controller/callbacks.rb:260:in 'AbstractController::Callbacks#process_action'
actionpack (8.0.5.1) lib/action_controller/metal/rescue.rb:27:in 'ActionController::Rescue#process_action'
actionpack (8.0.5.1) lib/action_controller/metal/instrumentation.rb:76:in 'block in ActionController::Instrumentation#process_action'
activesupport (8.0.5.1) lib/active_support/notifications.rb:210:in 'block in ActiveSupport::Notifications.instrument'
activesupport (8.0.5.1) lib/active_support/notifications/instrumenter.rb:58:in 'ActiveSupport::Notifications::Instrumenter#instrument'
activesupport (8.0.5.1) lib/active_support/notifications.rb:210:in 'ActiveSupport::Notifications.instrument'
actionpack (8.0.5.1) lib/action_controller/metal/instrumentation.rb:75:in 'ActionController::Instrumentation#process_action'
actionpack (8.0.5.1) lib/action_controller/metal/params_wrapper.rb:259:in 'ActionController::ParamsWrapper#process_action'
activerecord (8.0.5.1) lib/active_record/railties/controller_runtime.rb:39:in 'ActiveRecord::Railties::ControllerRuntime#process_action'
actionpack (8.0.5.1) lib/abstract_controller/base.rb:152:in 'AbstractController::Base#process'
actionview (8.0.5.1) lib/action_view/rendering.rb:40:in 'ActionView::Rendering#process'
rack-mini-profiler (4.0.1) lib/mini_profiler/profiling_methods.rb:90:in 'block in ActionController::Base#profile_method'
actionpack (8.0.5.1) lib/action_controller/metal.rb:252:in 'ActionController::Metal#dispatch'
actionpack (8.0.5.1) lib/action_controller/metal.rb:335:in 'ActionController::Metal.dispatch'
actionpack (8.0.5.1) lib/action_dispatch/routing/route_set.rb:67:in 'ActionDispatch::Routing::RouteSet::Dispatcher#dispatch'
actionpack (8.0.5.1) lib/action_dispatch/routing/route_set.rb:50:in 'ActionDispatch::Routing::RouteSet::Dispatcher#serve'
actionpack (8.0.5.1) lib/action_dispatch/journey/router.rb:53:in 'block in ActionDispatch::Journey::Router#serve'
actionpack (8.0.5.1) lib/action_dispatch/journey/router.rb:133:in 'block in ActionDispatch::Journey::Router#find_routes'
actionpack (8.0.5.1) lib/action_dispatch/journey/router.rb:126:in 'Array#each'
actionpack (8.0.5.1) lib/action_dispatch/journey/router.rb:126:in 'ActionDispatch::Journey::Router#find_routes'
actionpack (8.0.5.1) lib/action_dispatch/journey/router.rb:34:in 'ActionDispatch::Journey::Router#serve'
actionpack (8.0.5.1) lib/action_dispatch/routing/route_set.rb:908:in 'ActionDispatch::Routing::RouteSet#call'
lib/middleware/omniauth_bypass_middleware.rb:35:in 'Middleware::OmniauthBypassMiddleware#call'
lib/middleware/crawler_hooks.rb:13:in 'Middleware::CrawlerHooks#call'
rack (2.2.23) lib/rack/tempfile_reaper.rb:15:in 'Rack::TempfileReaper#call'
rack (2.2.23) lib/rack/conditional_get.rb:27:in 'Rack::ConditionalGet#call'
rack (2.2.23) lib/rack/head.rb:12:in 'Rack::Head#call'
actionpack (8.0.5.1) lib/action_dispatch/http/permissions_policy.rb:38:in 'ActionDispatch::PermissionsPolicy::Middleware#call'
lib/content_security_policy/middleware.rb:12:in 'ContentSecurityPolicy::Middleware#call'
lib/middleware/anonymous_cache.rb:425:in 'Middleware::AnonymousCache#call'
lib/middleware/csp_script_nonce_injector.rb:13:in 'Middleware::CspScriptNonceInjector#call'
lib/middleware/track_view_session_id_injector.rb:12:in 'Middleware::TrackViewSessionIdInjector#call'
config/initializers/008-rack-cors.rb:14:in 'Discourse::Cors#call'
rack (2.2.23) lib/rack/session/abstract/id.rb:266:in 'Rack::Session::Abstract::Persisted#context'
rack (2.2.23) lib/rack/session/abstract/id.rb:260:in 'Rack::Session::Abstract::Persisted#call'
actionpack (8.0.5.1) lib/action_dispatch/middleware/cookies.rb:706:in 'ActionDispatch::Cookies#call'
actionpack (8.0.5.1) lib/action_dispatch/middleware/callbacks.rb:31:in 'block in ActionDispatch::Callbacks#call'
activesupport (8.0.5.1) lib/active_support/callbacks.rb:100:in 'ActiveSupport::Callbacks#run_callbacks'
actionpack (8.0.5.1) lib/action_dispatch/middleware/callbacks.rb:30:in 'ActionDispatch::Callbacks#call'
actionpack (8.0.5.1) lib/action_dispatch/middleware/debug_exceptions.rb:31:in 'ActionDispatch::DebugExceptions#call'
actionpack (8.0.5.1) lib/action_dispatch/middleware/show_exceptions.rb:32:in 'ActionDispatch::ShowExceptions#call'
logster (2.21.0) lib/logster/middleware/reporter.rb:40:in 'Logster::Middleware::Reporter#call'
lib/middleware/default_headers.rb:13:in 'Middleware::DefaultHeaders#call'
railties (8.0.5.1) lib/rails/rack/logger.rb:41:in 'Rails::Rack::Logger#call_app'
railties (8.0.5.1) lib/rails/rack/logger.rb:29:in 'Rails::Rack::Logger#call'
config/initializers/100-quiet_logger.rb:20:in 'DiscourseRackQuietAssetsLogger#call'
config/initializers/100-silence_logger.rb:29:in 'SilenceLogger#call'
actionpack (8.0.5.1) lib/action_dispatch/middleware/request_id.rb:34:in 'ActionDispatch::RequestId#call'
lib/middleware/enforce_hostname.rb:23:in 'Middleware::EnforceHostname#call'
rack (2.2.23) lib/rack/method_override.rb:24:in 'Rack::Metho
Job exception: ERROR: invalid input syntax for type inet: "unix:" LINE 2: SET ip_address = 'unix:' ^

rack-mini-profiler-4.0.1/lib/patches/db/pg/alias_method.rb:109:in 'PG::Connection#exec'
rack-mini-profiler-4.0.1/lib/patches/db/pg/alias_method.rb:109:in 'PG::Connection#async_exec'
mini_sql-1.6.0/lib/mini_sql/postgres/connection.rb:217:in 'MiniSql::Postgres::Connection#run'
mini_sql-1.6.0/lib/mini_sql/active_record_postgres/connection.rb:38:in 'block in MiniSql::ActiveRecordPostgres::Connection#run'
mini_sql-1.6.0/lib/mini_sql/active_record_postgres/connection.rb:34:in 'block in MiniSql::ActiveRecordPostgres::Connection#with_lock'
activesupport-8.0.5.1/lib/active_support/concurrency/null_lock.rb:9:in 'ActiveSupport::Concurrency::NullLock#synchronize'
mini_sql-1.6.0/lib/mini_sql/active_record_postgres/connection.rb:34:in 'MiniSql::ActiveRecordPostgres::Connection#with_lock'
mini_sql-1.6.0/lib/mini_sql/active_record_postgres/connection.rb:38:in 'MiniSql::ActiveRecordPostgres::Connection#run'
/var/www/discourse/lib/mini_sql_multisite_connection.rb:109:in 'MiniSqlMultisiteConnection#run'
mini_sql-1.6.0/lib/mini_sql/postgres/connection.rb:196:in 'MiniSql::Postgres::Connection#exec'
/var/www/discourse/app/models/user.rb:1145:in 'User.update_ip_address!'
/var/www/discourse/lib/auth/default_current_user_provider.rb:235:in 'block in Auth::DefaultCurrentUserProvider#current_user'
/var/www/discourse/lib/scheduler/defer.rb:135:in 'block in Scheduler::Deferrable#do_work'
rails_multisite-7.0.0/lib/rails_multisite/connection_management/null_instance.rb:49:in 'RailsMultisite::ConnectionManagement::NullInstance#with_connection'
rails_multisite-7.0.0/lib/rails_multisite/connection_management.rb:17:in 'RailsMultisite::ConnectionManagement.with_connection'
/var/www/discourse/lib/scheduler/defer.rb:130:in 'Scheduler::Deferrable#do_work'
/var/www/discourse/lib/scheduler/defer.rb:116:in 'block (2 levels) in Scheduler::Deferrable#start_thread' 

이러한 상황이 발생하면 “Oops - Error 500” 페이지가 표시됩니다. 처음에는 Caddy가 클라이언트의 IP 주소를 Discourse로 전달하지 않는 것이 원인이라고 생각하여 여러 가지 다른 설정을 시도해 보았지만, 어떤 것도 문제를 해결하지 못했습니다.

참고로, 이 문제는 매우 드물게 발생합니다. 가끔씩만 발생하며, 발생했을 때 단순히 페이지를 새로고침하면 즉시 사이트가 복구됩니다. 그 후에는 오류가 다시 무작위로 발생하기까지 꽤 오랜 시간 동안 정상적으로 작동합니다.

저는 전문가가 아니라고 할 수 있지만, 제 셀프호스티드 인스턴스에서는 Caddy 대신 Nginx를 사용하고 있습니다. 혹시 ./app/containers.yml 파일에 있는 실제 Caddy와 Discourse 템플릿 설정을 공유해 주실 수 있을까요?

에러가 발생하기 전에 보통 어떤 작업을 하셨는지 기억나시나요? 예를 들어 관리자 설정 변경, 게시글 작성, 특정 플러그인 사용 같은 것들이요.

당신의 문제에 대해 구체적인 도움을 드리지 못해 죄송합니다. 하지만 당신의 답변이 커뮤니티에서 명확하고 빠른 응답을 얻을 수 있도록 질문에 가치를 더해 줄 수 있다고 생각합니다.

즉, 요청/사용자의 원격 IP가 소켓을 가리키고 있으며, 이는 프록시 체인의 일부가 잘못 구성된 것을 의미합니다.

@satonotdead @Falco

app.yml

templates:
  - "templates/postgres.18.template.yml"
  - "templates/redis.template.yml"
  - "templates/web.template.yml"
  
  - "templates/web.socketed.template.yml"
  
  - "templates/enable-ruby-yjit.yml"


## 이 컨테이너에서 어떤 TCP/IP 포트를 노출해야 하나요?
## Apache나 nginx와 같은 다른 웹 서버와 포트를 공유하려면
## https://meta.discourse.org/t/17247 를 참조하세요
expose:
  # - "66:80"   # http
  # - "66:443" # https

params:
  ## 이 컨테이너에 어떤 Git 리비전을 사용할까요? (기본값: latest)
  version: latest
  ## 최대 업로드 크기 (기본값: 10m)
  upload_size: 150m
  
  db_default_text_search_config: "pg_catalog.english"

  ## db_shared_buffers를 총 메모리의 최대 25%로 설정하세요.
  ## bootstrap이 감지된 RAM에 따라 자동으로 설정되거나, 직접 오버라이드할 수 있습니다
  db_shared_buffers: "2048MB"

  ## 정렬 성능을 향상시킬 수 있지만, 연결당 메모리 사용량이 증가합니다
  #db_work_mem: "40MB"

  ## 이 컨테이너에 어떤 Git 리비전을 사용할까요? (기본값: tests-passed)
  #version: tests-passed

env:
  LC_ALL: en_US.UTF-8
  LANG: en_US.UTF-8
  LANGUAGE: en_US.UTF-8
  # DISCOURSE_DEFAULT_LOCALE: en

  ## https://meta.discourse.org/t/rescaling-the-server-which-configs-need-to-be-changed-unicorn-workers-memory-etc/252788
  ## 동시 웹 요청을 얼마나 지원할 수 있나요? 메모리와 CPU 코어 수에 따라 다릅니다.
  ## bootstrap이 감지된 CPU에 따라 자동으로 설정되거나, 직접 오버라이드할 수 있습니다
  UNICORN_WORKERS: 8

  ## TODO: 이 Discourse 인스턴스가 응답할 도메인 이름
  ## 필수. Discourse는 IP 주소만으로는 작동하지 않습니다.
  DISCOURSE_HOSTNAME: example.com

  ## 컨테이너가 위에서 지정한 도메인 이름(-h 옵션)과 동일한
  ## 호스트네임으로 시작되도록 하려면 주석을 해제하세요 (기본값 "$hostname-$config")
  #DOCKER_USE_HOSTNAME: true

  ## TODO: 초기 가입 시 관리자 및 개발자로 설정될 쉼표로 구분된 이메일 목록
  ## 예: 'user1@example.com,user2@example.com'
  DISCOURSE_DEVELOPER_EMAILS: 'admin+discourse@example.com'

  ## TODO: 새 계정을 검증하고 알림을 보내는 데 사용되는 SMTP 메일 서버
  # SMTP ADDRESS는 필수입니다
  # 경고: SMTP 비밀번호는 문제를 피하기 위해 따옴표로 감싸야 합니다
  DISCOURSE_SMTP_ADDRESS: smtp.provider.com
  DISCOURSE_SMTP_PORT: 587
  DISCOURSE_SMTP_USER_NAME: noreply@example.com
  DISCOURSE_SMTP_PASSWORD: "***"
  #DISCOURSE_SMTP_ENABLE_START_TLS: true           # (선택 사항, 기본값: true)
  DISCOURSE_SMTP_DOMAIN: example.com # (일부 제공업체에서 필수)
  DISCOURSE_NOTIFICATION_EMAIL: noreply@example.com
  #DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: peer        # (선택 사항, 기본값: peer, 유효한 값: none, peer, client_once, fail_if_no_peer_cert)
  #DISCOURSE_SMTP_AUTHENTICATION: plain            # (기본값: plain, 유효한 값: plain, login, cram_md5)

  ## Lets Encrypt 템플릿을 추가했다면, 무료 SSL 인증서를 받으려면 아래 주석을 해제하세요
  # LETSENCRYPT_ACCOUNT_EMAIL: admin+letsencrypt@example.com

  ## 이 Discourse 인스턴스의 http 또는 https CDN 주소 (인출(pull)하도록 구성됨)
  ## 자세한 내용은 https://meta.discourse.org/t/14857 를 참조하세요
  #DISCOURSE_CDN_URL: https://discourse-cdn.example.com

  ## IP 주소 조회를 위한 maxmind 지리 위치 IP 계정 ID 및 라이선스 키
  ## 자세한 내용은 https://meta.discourse.org/t/-/173941 를 참조하세요
  #DISCOURSE_MAXMIND_ACCOUNT_ID: 123456
  #DISCOURSE_MAXMIND_LICENSE_KEY: 1234567890123456

  # HTTPS 강제 사용
  DISCOURSE_FORCE_HTTPS: true
  
  # 요청 제한
  DISCOURSE_MAX_REQS_PER_IP_MODE: none
  DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE: 12000
  
  DISCOURSE_MAX_DATA_EXPLORER_API_REQ_MODE: none
  DISCOURSE_MAX_DATA_EXPLORER_API_REQS_PER_10_SECONDS: 1000

  DISCOURSE_YJIT_ENABLED: true

## Docker 컨테이너는 상태가 없으며(stateless), 모든 데이터는 /shared에 저장됩니다
volumes:
  - volume:
      host: /var/discourse/shared/standalone
      guest: /shared
  - volume:
      host: /var/discourse/shared/standalone/log/var-log
      guest: /var/log
  - volume:
      host: /var/discourse/plugins
      guest: /var/plugins

## 플러그인은 여기에 배치됩니다
## 자세한 내용은 https://meta.discourse.org/t/19157 를 참조하세요
hooks:
  after_code:
    - exec:
        cd: $home/plugins
        cmd:
          - git clone https://github.com/discourse/docker_manager.git
          - cp -a /var/plugins/. $home/plugins/

## 빌드 후 실행할 사용자 정의 명령
run:
  - exec: echo "Beginning of custom commands"
  ## 첫 번째 등록 시 'From' 이메일 주소를 설정하려면 주석을 해제하고 변경하세요:
  ## 첫 번째 가입 이메일을 받은 후, 해당 줄의 주석을 다시 설정하세요. 한 번만 실행하면 됩니다.
  #- exec: rails r "SiteSetting.notification_email='info@unconfigured.discourse.org'"
  - exec: echo "End of custom commands"

Caddyfile

#---------------------------------------------------------------------------------

{
	storage file_system {
		root /var/caddy/data
	}
}

#---------------------------------------------------------------------------------

# SSL 보안에서 A+ 등급을 달성하기 위해
(hsts) {
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
	}
}

# 셀프 호스팅 acme-dns 자격 증명
(tls-challenge) {
	tls admin+letsencrypt@example.com {
		dns acmedns {
			username ***
			password ***
			subdomain ***
			server_url http://acme.example.com:5005
		}
	}
}

#---------------------------------------------------------------------------------
# 포럼 (Discourse)
#---------------------------------------------------------------------------------
example.com {
	#-------------------------------------------------------------------------------
	import hsts
	import tls-challenge
	#-------------------------------------------------------------------------------
	request_header X-Real-IP {remote_host}
	reverse_proxy unix//var/discourse/shared/standalone/nginx.http.sock
	#-------------------------------------------------------------------------------
}

#---------------------------------------------------------------------------------
# 서브도메인
#---------------------------------------------------------------------------------

#---------------------------------------------------------------------------------
*.example.com {
	#-------------------------------------------------------------------------------
	import hsts
	import tls-challenge
	#-------------------------------------------------------------------------------

	# 도메인의 non-www 버전을 강제합니다
	@www host www.example.com
	redir @www https://example.com{uri} permanent
  
	#-------------------------------------------------------------------------------
  
	@store host store.example.com
	handle @store {
		@wc_private {
			path /license.txt
			path /readme.html

			# 중요한 파일들
			path /wp-config.php
			path /xmlrpc.php
			path /wp-settings.php
			path /wp-load.php
			path /wp-blog-header.php

			path /wp-admin.php

			path /wp-admin/install.php

			path /wp-content/uploads/wc-logs/*
			path /wp-content/uploads/nuvei-logs/*

			path /wp-content/uploads/woocommerce_uploads/*
		}

		# protect_php
		@wc_private_php {
			not path /wp-includes/ms-files.php

			path_regexp protect_php ^/(wp-includes|wp-admin/includes|wp-content/uploads)/.*\.php$
		}
		respond @wc_private 403
		respond @wc_private_php 403

		root * /usr/share/wordpress
		php_fastcgi unix//run/php/php8.3-fpm.sock
		file_server
	}
  
  #-------------------------------------------------------------------------------
  # 여러 개의 독립적인 HTML 파일
  #-------------------------------------------------------------------------------

  @join host join.example.com
  handle @join {
    root * /var/example/join
    file_server
  }

  #-------------------------------------------------------------------------------
  # 매처 없음
  #-------------------------------------------------------------------------------

	handle {
		respond 404
	}

	#-------------------------------------------------------------------------------
}

좀 더 구체적으로 말씀드리면, 이 오류는 이전 오류가 발생한 지 몇 시간 후에 발생하며, 매우 자주 발생하는 것은 아닙니다. app.yml이나 Caddyfile에서 특이한 점을 발견하지 못했습니다.

소켓 연결이 끊어질 때까지 정상적으로 유지된다면, 명시적인 타임아웃과 전송 옵션을 추가할 수 있다고 생각합니다.

명시적인 타임아웃과 전송 옵션을 추가할 수 있다고 생각합니다

어떻게요?

자, 앞서 말했듯이 저는 Caddy를 사용하지 않았지만, 공식 문서를 참고해 보실 수 있습니다.

문제를 찾은 것 같습니다. 재빌드를 수행할 때 Caddy를 재시작하지 않았습니다. 이제부터는 이렇게 하겠습니다:

./launcher rebuild app && systemctl reload caddy

재빌드 후 Caddy가 소켓 캐시나 유사한 것을 사용하는 것 같습니다. 하지만 보통 재빌드 직후에는 바로 발생하지 않습니다. 지켜보겠습니다.

몇 날째 이 문제를 해결하려고 노력하고 있습니다. Caddy가 헤더를 전송하고, nginx가 이를 수신하여 적용하며, CDN이 없으며, 소켓에 영향을 주는 다른 프로세스나 웹훅도 없다는 것을 모두 확인했습니다. 모든 테스트가 통과합니다. 그리고 Unix 에러가 계속 나타나고 있습니다. 리빌드 때문인 줄 알았지만, 그렇지 않았습니다. 이 모든 것은 매우 긴 시간 동안 랜덤하게 발생합니다.

지금 TCP가 제 유일한 선택지인가요?

제가 제안한 타임아웃을 Caddy 템플릿에 추가하셨나요? 전문가라고 할 수는 없지만, 그렇게 하면 문제가 해결될 것 같습니다.

그렇게 하는 건 의미가 없어요. 어떤 요청은 15초 후에 실패하는 반면, 다른 요청은 1초도 안 돼서 실패하거든요. 결국 별 쓸모가 없는 거예요.

네, 다음 두 링크를 확인해 보실 수 있습니다:

헤더가 누락되었거나, Discourse 템플릿 파일에서 신뢰 체인(chain of trust) 설정이 제대로 되어 있지 않은 것 같습니다.

@Falco @satonotdead

드디어 문제를 해결했습니다. 정말로 고쳐졌는지 확인하기 위해 꽤 오랜 시간을 기다렸고, 이제 정상적으로 작동하는 것 같습니다.

구성 (Setup)

Discourse는 Docker에서 실행되며 nginx를 Unix 소켓(/var/discourse/shared/standalone/nginx.http.sock)으로 노출합니다. Caddy는 그 앞에 리버스 프록시로 위치하며 X-Real-IP 헤더를 통해 클라이언트의 실제 IP를 전달합니다.

변경해야 할 두 가지가 있었습니다: 컨테이너 내부의 nginx 설정과 재빌드(rebuild) 실행 방식입니다.

1. app.yml

## Plugins go here
## see https://meta.discourse.org/t/19157 for details
hooks:
  # This after_code block is just my own plugin list — it has nothing to do with
  # the fix. Keep whatever you already have here.
  after_code:
    - exec:
        [...]

  # This is the part that matters.
  #   1. Writes an http-level map that turns the literal "unix:" into 127.0.0.1.
  #      The 00- prefix makes nginx load it before discourse.conf.
  #   2. Rewrites discourse.conf so X-Forwarded-For uses the mapped variable
  #      instead of the raw $remote_addr.
  #   3. nginx -t fails the build if the result is not valid.
  after_web_config:
    - exec: >-
        printf 'map $remote_addr $safe_remote_addr {\n  "unix:" 127.0.0.1;\n  default $remote_addr;\n}\nreal_ip_header X-Real-IP;\n'
        > /etc/nginx/conf.d/00-safe-remote-addr.conf
    - exec: >-
        sed -i 's/X-Forwarded-For \$remote_addr;/X-Forwarded-For $safe_remote_addr;/g'
        /etc/nginx/conf.d/discourse.conf
    - exec: nginx -t

핵심 코드는 after_web_config 블록 내부에 있는 부분입니다.

2. Caddyfile 블록

아래 스크립트는 플래그 파일을 통해 유지보수 모드를 제어하므로, 사이트 블록에서 이를 인식해야 합니다:

forum.example.com {
    request_header X-Real-IP {remote_host}

    root * /var/caddy/flags
    @maintenance file maintenance.flag
    handle @maintenance {
        respond "Maintenance in progress. We'll be back in a few minutes." 503
    }

    handle {
        reverse_proxy unix//var/discourse/shared/standalone/nginx.http.sock
    }
}

스크립트에서 사용하는 플래그 경로와 @maintenance 매처가 찾는 파일은 반드시 동일한 파일이어야 합니다. 하나를 변경하면 다른 하나도 함께 변경해야 하며, 그렇지 않으면 유지보수 모드가 조용히 작동하지 않습니다.

같은 Caddy 인스턴스가 다른 애플리케이션도 서빙하는 경우, 포럼 블록만 매칭되는 플래그를 사용하세요. 그렇지 않으면 Discourse 재빌드 시 다른 애플리케이션들도 함께 다운됩니다.

3. 재빌드 스크립트

이제 더 이상 ./launcher rebuild app을 직접 사용하지 않습니다. 대신 discourse-rebuild.sh를 사용합니다:

#!/bin/bash
#
# discourse-rebuild.sh — Rebuilds the Discourse container without leaving orphaned
# requests behind and without the `invalid input syntax for type inet: "unix:"` error.
#
# CONTEXT
#   Discourse runs in Docker and exposes nginx on a Unix socket
#   (/var/discourse/shared/standalone/nginx.http.sock). Caddy acts as a reverse
#   proxy in front of it and passes the client's real IP through X-Real-IP.
#
#   A `rebuild` destroys the container and recreates the socket with a new inode.
#   During that transition there are two problematic windows:
#
#     1) Caddy keeps state from the previous socket until it is reloaded.
#     2) nginx starts accepting connections as soon as it boots, but Unicorn
#        takes ~15s longer before it can serve them.
#
#   A request landing in either window may arrive with no X-Real-IP. $remote_addr
#   is then left holding the literal "unix:", which PostgreSQL rejects when
#   inserting it into an inet column -> HTTP 500.
#
# WHAT IT DOES
#   1. Raises a flag file that puts the site into 503 (Caddy checks it on every
#      request, so it takes effect instantly and with no reload).
#   2. Waits until nothing but nginx itself is holding the socket open.
#   3. Rebuilds the container.
#   4. Polls /srv/status against the socket until Unicorn answers 200.
#   5. Reloads Caddy so it picks up the new socket, and clears the flag.
#   6. Starts the watcher that logs any leftover "unix:" hit.
#
#   If the rebuild fails, or Discourse never answers within the polling window,
#   the flag is NOT removed: the site stays in maintenance on purpose, so a broken
#   container is never exposed. Bring it back up by hand with:
#       rm -f /var/caddy/flags/maintenance.flag
#
# REQUIREMENTS
#   - lsof installed, and root privileges.
#   - FLAG below must point at the exact same file the Caddyfile @maintenance
#     matcher looks for.
#   - request_header X-Real-IP {remote_host} in that same Caddyfile block.
#
# USAGE
#   ./discourse-rebuild.sh
#
# TO SEE WHAT THE WATCHER CAUGHT
#   cat /var/log/unixip-hits.log
#

set -e

FLAG=/var/caddy/flags/maintenance.flag
SOCK=/var/discourse/shared/standalone/nginx.http.sock

cleanup() {
  local code=$?
  systemctl reload caddy
  if [ $code -eq 0 ]; then
    rm -f "$FLAG"
    echo "✅ Rebuild finished. Site is online."
  else
    echo "⚠️  Rebuild failed (exit code $code). The site is still in maintenance."
    echo "    Check it, and once it's ready: rm -f $FLAG"
  fi
}
trap cleanup EXIT

mkdir -p "$(dirname "$FLAG")"
touch "$FLAG"
echo "🔧 Maintenance is on. Waiting for in-flight requests..."

# Rough heuristic: count the processes holding the socket open and wait until
# only the listener is left. Up to 30s.
for i in $(seq 30); do
  n=$(lsof -t "$SOCK" 2>/dev/null | wc -l || echo 0)
  [ "$n" -le 1 ] && break
  sleep 1
done

/var/discourse/launcher rebuild app

# Up to 60 attempts: about 2 minutes of sleeps, more if any curl hits its own
# 5s timeout.
echo "⏳ Waiting for Discourse to answer..."
status=000
for i in $(seq 60); do
  status=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \
    --unix-socket "$SOCK" http://localhost/srv/status 2>/dev/null || echo 000)
  [ "$status" = "200" ] && break
  sleep 2
done

if [ "$status" != "200" ]; then
  echo "⚠️  Discourse never answered within the polling window (last status code: $status)."
  exit 1
fi

echo "✅ Discourse ready after ~$((i*2))s."

systemd-run --unit=unixip-watch --collect \
  /bin/bash -c "docker exec app tail -F /var/log/nginx/access.log | grep --line-buffered 'unix:' >> /var/log/unixip-hits.log"

스크립트가 실제로 하는 일

  1. Caddy의 플래그 파일을 통해 사이트를 유지보수 모드로 전환합니다.
  2. 소켓이 조용해질 때까지 기다린 후 ./launcher rebuild app을 실행합니다.
  3. Unicorn이 200을 응답할 때까지 소켓을 통해 /srv/status를 폴링합니다. 이것이 핵심적인 부분입니다: Unicorn이 부팅 중일 때 요청이 nginx에 도달하는 것을 막아줍니다.
  4. Caddy를 재로드합니다(안전장치로 추가했으나, 실제로는 중요하지 않은 것으로 나타남).
  5. 해당 폴링이 성공한 경우에만 유지보수 플래그를 제거합니다.

수 주가 지나고 여러 번의 재빌드를 거쳤지만, 그 에러를 다시는 보지 못했습니다.

P.S.: 제가 대체 뭘 한 건지 정말로 모르겠습니다.