무색
기술블로그
에세이
연구
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

contact@museck.com

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관·연락처
INCHEON, KR
Postiz 소셜 미디어 자동화 배포: Helm OCI + Temporal 삽질기
홈랩 삽질기
2026. 1. 5.

Postiz 소셜 미디어 자동화 배포: Helm OCI + Temporal 삽질기

PostizHelmTemporalArgoCDKubernetes

소셜 미디어 여러 채널에 글을 일일이 올리는 건 솔직히 귀찮다. LinkedIn, X, YouTube까지 관리하려면 손이 세 개라도 모자란다. Postiz는 이런 문제를 풀어주는 오픈소스 소셜 미디어 자동화 도구인데, 이번에 홈랩 K8s 클러스터에 올리면서 꽤 고생했다.

결론부터 말하면 커밋 13개가 필요했다. Helm subchart 서비스 이름이 틀리고, Temporal이 필수 의존성이 되었는데 문서에 안 나와 있고, ConfigMap 마운트 경로가 entrypoint를 덮어쓰고, Google OAuth가 내부적으로 YouTube 환경변수를 공유하고... 하나 고치면 다음 문제가 터지는 연쇄 삽질이었다.

전체 아키텍처

먼저 완성된 구성을 보자. ArgoCD multi-source Application으로 OCI Helm 차트, Git values, Git manifests 세 소스를 조합했다.

핵심 구성 요소는 이렇다.

  • postiz-app: PM2가 frontend(4200), backend(3000), orchestrator 세 프로세스를 관리
  • Temporal: auto-setup 서버 + 전용 PostgreSQL(emptyDir). 워크플로우 엔진으로 v2.12.0부터 필수
  • PostgreSQL + Redis: Bitnami subchart로 배포. 5Gi/2Gi PVC
  • NFS PVC: 업로드 파일 저장용

ArgoCD Multi-Source 배포

Postiz는 공식 Helm 차트를 OCI(ghcr.io)로 배포한다. 여기에 우리 환경에 맞는 values를 Git에서 override하고, Helm이 관리하지 않는 Temporal 매니페스트까지 같은 Application으로 묶어야 했다. ArgoCD의 multi-source 기능이 딱 맞는 상황이었다.

apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
  sources:
    # Source 1: OCI Helm 차트
    - repoURL: ghcr.io/gitroomhq/postiz-helmchart/charts/postiz-app
      chart: postiz-app
      targetRevision: 1.0.5
      helm:
        valueFiles:
          - $values/k8s-company-internal/services/postiz/postiz-values.yaml
    # Source 2: Git values 참조
    - repoURL: https://gitea.xssh.org/homelab/homelab.git
      targetRevision: master
      ref: values
    # Source 3: Temporal 등 추가 매니페스트
    - repoURL: https://gitea.xssh.org/homelab/homelab.git
      targetRevision: master
      path: k8s-company-internal/services/postiz/manifests

$values는 Source 2를 가리키는 참조 변수다. Source 1의 Helm 차트가 설치될 때 Source 2의 values 파일을 끌어다 쓴다. Source 3은 Helm 차트 밖의 매니페스트(Temporal Deployment, ConfigMap 등)를 배포한다.

삽질 1: Bitnami Subchart 서비스 이름

첫 배포에서 바로 터진 문제다. Postiz의 DATABASE_URL과 REDIS_URL이 연결 실패를 뿜었다.

Helm subchart에서 Bitnami PostgreSQL/Redis를 띄우면 서비스 이름이 직관과 다르게 생성된다. fullnameOverride를 지정하지 않으면 릴리스 이름 기반으로 postiz-postgresql, postiz-redis-master가 된다. 처음에 postgresql이나 redis로 접근하려다 계속 실패했다.

# 잘못된 설정
DATABASE_URL: postgresql://user:pass@postgresql:5432/postiz

# 올바른 설정 (릴리스명 기반)
DATABASE_URL: postgresql://user:pass@postiz-postgresql:5432/postiz
REDIS_URL: redis://postiz-redis-master:6379

거기에 Bitnami는 특정 이미지 태그를 주기적으로 삭제한다. 15.x.x 같은 버전을 values에 고정해두면 어느 날 갑자기 ImagePullBackOff가 뜬다. 결국 latest를 쓰는 게 가장 안정적이었다. 프로덕션에서는 좀 꺼림칙하지만 홈랩이니까 괜찮다.

삽질 2: Temporal이 선택이 아닌 필수

Bitnami 문제를 잡고 나니 백엔드가 기동하다가 죽었다. 로그를 보니 ECONNREFUSED :7233이 찍혀 있었다. 포트 7233은 Temporal 서버 포트다.

Postiz v2.12.0부터 orchestrator가 Temporal 워크플로우 엔진을 사용하는데, 문서에는 "optional"이라고 적혀 있었다. 실제로는 백엔드 기동 시 Temporal 연결을 시도하고 실패하면 크래시한다. 소스 코드를 직접 까봐서야 확인했다.

교훈: 오픈소스 프로젝트의 공식 문서보다 소스 코드의 환경변수 사용법을 직접 확인하는 게 더 정확하다.

Temporal을 별도 Deployment로 배포했다. auto-setup 이미지를 쓰면 스키마 생성까지 자동으로 해준다. Temporal용 PostgreSQL은 emptyDir 볼륨으로 잡았는데, Postiz가 자체 DB에 스케줄 상태를 관리하므로 Temporal DB가 날아가도 괜찮기 때문이다.

삽질 3: ConfigMap 마운트 경로 충돌

Temporal이 뜨긴 하는데 search attribute 타입 제한에 걸렸다. PostgreSQL visibility store는 타입당 search attribute를 3개까지만 허용한다. dynamic config로 이 제한을 풀어야 했다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: temporal-dynamic-config
data:
  dynamic_config.yaml: |
    limit.searchAttributesNumberOfKeysPerRequest:
      - value: 100
    limit.searchAttributesTotalSizePerRequest:
      - value: 262144

이 ConfigMap을 처음에 /etc/temporal에 마운트했다. 컨테이너가 아예 시작을 안 했다. Temporal의 auto-setup 이미지는 /etc/temporal/entrypoint.sh를 실행하는데, ConfigMap 마운트가 이 디렉토리를 통째로 덮어써버린 거다.

마운트 경로를 /etc/temporal-dynamic/으로 바꾸고 환경변수에서 이 경로를 참조하게 수정하니 정상 동작했다. 사소한 경로 하나가 컨테이너를 완전히 먹통으로 만든 셈이다.

삽질 4: 메모리 1Gi에서 4Gi까지

Temporal까지 잡고 나니 이번에는 orchestrator가 OOMKilled로 죽었다. 처음에 1Gi 제한을 줬는데, orchestrator는 시작할 때 플랫폼별 workflow bundle을 webpack으로 컴파일한다. 이 과정에서 피크 메모리가 치솟는다.

1Gi에서 OOM. 2Gi로 올려도 OOM. 결국 4Gi까지 올려서야 안정화됐다. 정상 운영 중에는 1Gi도 안 쓰는데 초기 빌드 때문에 4Gi가 필요한 구조다. 홈랩에서 메모리 32GB짜리 노드를 쓰고 있으니 감당 가능하지만 좀 아깝긴 하다.

삽질 5: Google OAuth 3단계 수정

기능이 다 돌아가니까 마지막으로 Google 계정 연동을 설정했다. 여기서 또 삽질 3라운드가 벌어졌다.

  1. 1라운드: GOOGLE_CLIENT_ID/SECRET 환경변수를 설정했는데 동작 안 함
  2. 2라운드: Generic OIDC Provider로 바꿔봤는데 역시 안 됨
  3. 3라운드: 소스 코드를 뒤져보니 Postiz의 GoogleProvider가 내부적으로 YOUTUBE_CLIENT_ID를 사용. YouTube 연동과 Google 인증이 같은 credential을 공유하는 구조

YOUTUBE_CLIENT_ID와 YOUTUBE_CLIENT_SECRET을 설정하니 Google OAuth가 바로 동작했다. 환경변수 이름만 봐서는 절대 유추할 수 없는 구조였다.

내부 전용 접근으로 전환

원래 museck.com 도메인의 서브도메인으로 배포하려 했는데, 생각해보니 소셜 미디어 관리 도구는 외부에 노출할 이유가 없다. Cloudflare Tunnel을 타는 museck.com 대신 내부망에서만 접근 가능한 xssh.org 도메인으로 전환했다. Traefik IngressRoute만 수정하면 되니 간단했다.

정리하며

커밋 13개에 걸친 삽질을 돌아보면 결국 문제는 전부 "문서에 안 써있는 것"이었다.

  • Bitnami subchart의 서비스 이름 규칙은 Helm 차트 소스를 봐야 안다
  • Temporal이 필수 의존성인 건 Postiz 소스 코드를 봐야 안다
  • auto-setup 이미지의 entrypoint 경로는 Dockerfile을 봐야 안다
  • GoogleProvider가 YOUTUBE_CLIENT_ID를 쓰는 건 Provider 코드를 봐야 안다

복잡한 오픈소스를 셀프 호스팅할 때는 README나 문서를 믿기보다 소스 코드를 직접 읽는 게 결국 가장 빠른 길이다. 특히 Helm subchart가 여러 겹 쌓여있고 외부 의존성(Temporal)까지 끼어들면 "사소한" 설정(서비스 이름, 마운트 경로, 환경변수 이름) 하나가 연쇄 장애를 만든다.

한 가지 위안이라면 커밋 하나하나가 각각 독립적인 교훈이라는 점이다. 이 글이 Postiz를 K8s에 올리려는 누군가에게 삽질 시간을 줄여줬으면 한다.

자주 묻는 질문

Postiz를 K8s에 셀프 호스팅할 때 Temporal이 필수인가요?
네. Postiz의 예약 발행 기능이 Temporal 워크플로우 엔진에 의존합니다. Temporal 없이 배포하면 앱은 뜨지만 예약 발행이 동작하지 않습니다.
Bitnami subchart 서비스 이름이 예상과 다를 때 어떻게 해결하나요?
Bitnami PostgreSQL/Redis subchart는 릴리스 이름을 서비스 이름에 포함시킵니다. fullnameOverride로 서비스 이름을 명시적으로 고정하면 앱의 환경변수와 일치시킬 수 있습니다.
Postiz Helm 차트에서 ConfigMap 마운트 경로 충돌은 어떻게 해결하나요?
ConfigMap을 앱 소스코드 경로에 마운트하면 기존 파일을 덮어씁니다. 별도 경로에 마운트하고 환경변수로 참조하거나 initContainer에서 복사하는 방식으로 우회합니다.
홈랩 삽질기(12/19)
Prev

OpenClaw AI 게이트웨이를 홈랩에 배포한 이야기

Next

ntfy + ArgoCD 배포 알림: 글로벌 구독의 함정과 per-app 알림