나의 블로그 환경은 2곳에서 돌고 있다. prod는 오라클 클라우드 오사카 리전의 무료 A1 서버가 맡는다. 글 작성과 워크플로우는 집에 있는 MacBook Pro의 dev에서 시험한다. 각 환경은 같은 compose 파일로 어디에서든 같은 서비스를 띄울 수 있도록 했다. 하지만 아직 할 일이 남아 있었다.

초안을 쓰는 LLM과 대표 그림을 그리는 이미지 모델은 Mac에만 있었다. OCI Always Free A1에는 GPU도 없고 메모리도 12 GB뿐이라 AI 모델을 올릴 수가 없다. 그러니 오사카에 있는 prod의 n8n도 집 Mac에서 구동중인 AI 모델을 사용할 수 있어야 했다. 반대 방향도 있었다. A1의 CPU·메모리·디스크 모니터링은 Mac에 띄운 Grafana에서 보고 싶었다.
방문자가 들어오는 Public 엔드포인트는 Cloudflare Tunnel이 이미 맡고 있다. 나의 리소스(A1, Mac) 사이에서만 오가는 비공개(Private) 네트워크가 필요했고 여기에 Tailscale을 사용했다. dev와 prod가 함께 써야하는 네트워크 자원을 먼저 정리한 뒤 Tailscale을 고른 이유, OCI A1과 Mac을 Private 네트워크에 연결한 방법, Tailscale을 연결하고 나서 달라진 점을 정리해보자.
무료 구성 요소를 고른 과정은 서버비 0원 블로그 조사 글에, 데이터 백업은 restic·R2 백업 글에 따로 정리해 두었다.
목차
- dev와 prod가 함께 쓰는 네트워크 자원
- Tailscale로 네트워크 구성이 단순해졌다
- OCI A1에 붙이기 — Oracle Linux 10
- 저장소와 설치는 cloud-init이 맡는다
- 가입은 Bastion으로 들어가 수동으로 진행한다
- 키 만료는 콘솔에서 끈다
- 방화벽은 건드리지 않는다
- Mac에 붙이기 — 서비스는 127.0.0.1에, 바깥으로는 serve로
- Homebrew의 tailscaled를 쓰는 이유
- Mac의 서비스는 All Open으로 열지 않는다
- 컨테이너 안에서는 이름을 고정한다
- Tailscale로 Private 네트워크를 구성하고
- dev와 prod가 같은 주소를 이용할 수 있게 되었다
- 서버를 옮겨도 AI 연결은 그대로다
- prod 모니터링을 Private 네트워크를 통해 Mac에 전송한다
- 핸드폰으로 백엔드를 늘 들여다 볼 수 있게 되었다
- 생각해봐야할 것들, Tailscale로 얻은 것들
- 정리
dev와 prod가 함께 쓰는 네트워크 자원
dev와 prod는 DB도, 데이터 폴더도, Cloudflare 터널도 따로 갖는다, 이것들은 모드마다 하나씩 있으니 서로 섞일 일이 없다. 하지만 일부 리소스는 네트워크로 연결되어 사용 가능해야 하고, 리소스 경합이 문제가 될 수 있었다. 정리해 보니 이렇다.
- prod는 OCI A1 환경에서 돌아가지만, 필요에 따라 Mac에서 prod스택을 올릴 수 있도록 구성했다.
- “블로그 플랫폼의 prod 모드와 dev 모드 스택을 모두 수용” 가능한 상태로 구축했다.
| 자원 | 위치 | 사용자 | 네트웍 |
|---|---|---|---|
| 블로그·n8n 공개 주소 | Cloudflare 엣지 | 방문자 | Cloudflare Tunnel(모드마다 따로) |
| LLM·임베딩 (oMLX :8000) | Mac | dev n8n, prod n8n | tailnet |
| 그림 생성 (Draw Things :7860) | Mac | dev n8n, prod n8n | tailnet |
| 메트릭 수신 (VictoriaMetrics :8428) | Mac | A1의 Alloy | tailnet |
| 대시보드 (Grafana :3000) | Mac | 노트북·휴대폰 | tailnet |
| 관리 SSH | Mac, A1 | 나 | tailnet + OCI Bastion |
첫 줄을 빼면 전부 백엔드간에 오가는 연결이다. 그런데 두 백엔드가 서로를 찾기 어려운 자리에 있다. Mac은 집 공유기 뒤에 있어 바깥에서 보이는 IP가 언제든 바뀔 수 있는 데다 노트북이라 들고 나가기도 한다. A1은 오라클 VCN 안에 있고 들어오는 포트는 Bastion용 22번 하나만 열어 두었다. 보통은 한쪽에 포트를 열어 다른 쪽이 들어오게 하는데 나는 양쪽 다 열고 싶지 않았다.

Tailscale로 네트워크 구성이 단순해졌다
Tailscale 없이 같은 일을 하려면 포기하고 싶을 정도로 손이 많이 간다. 집 공유기의 포트포워딩, 바뀌는 IP를 따라갈 DDNS, HTTPS 인증서가 필요하고 인증도 따로 만들어야 한다. Draw Things의 HTTP API에는 인증이 아예 없어서 이렇게 열어 두면 이미지 생성용 엔드포인트를 인터넷에 그대로 노출해야 한다. Cloudflare Tunnel로 Mac의 oMLX와 Draw Things API를 공개하는 방법도 생각했지만 결국 LLM API를 인터넷에 내놓는 일이라 접었다. (퍼블릭은 너무 너무 무섭다) A1의 메트릭을 Mac이 가져가는 방식(pull)이었다면 OCI 보안 규칙과 firewalld에 들어오는 포트를 또 열어야 했을 것이다.
Tailscale을 이용하니 너무나도 쉽게 근본적인 문제를 해결 할 수 있었다.
- Pubic에 포트를 열지 않아도 된다.
- 각 기계의 tailscaled가 밖으로만 연결을 맺고 양쪽이 NAT를 뚫어 WireGuard로 직접 붙는다. A1과 Mac은 OCI 보안 규칙을 하나도 더하지 않았는데 41ms 직결로 붙어 있다.
- 직결이 안 되면 Tailscale의 중계 서버(DERP)로 넘어간다. A1에서 가장 가까운 중계는 도쿄였고 9ms 남짓 거리였다.
- IP 대신 이름을 쓸 수 있다.
- tailnet MagicDNS가 기기마다
blog-a1.<tailnet>.ts.net같은 이름을 준다. Mac에서tailscale serve를 쓰면 127.0.0.1에만 열어 둔 서비스가 그 이름의 HTTPS 주소로 tailnet 안에서만 나간다. - 인증서도 Tailscale이 알아서 받아 준다.
- tailnet MagicDNS가 기기마다
- 호스트마다 데몬 하나면 된다.
- A1의 tailscaled 하나가 n8n 컨테이너의 AI 호출과 호스트에서 도는 Alloy의 메트릭 전송을 같이 처리한다.
- compose 안에 Tailscale 컨테이너를 넣지 않고 VM 데몬으로 처리했다.
- 정책을 따로 짜지 않았다.
- tailnet에는 내 기기 네 대뿐이라 기본 정책(전부 허용)을 그대로 쓰도록 했다.
- 아직은 태그나 ACL 구성이 필요없이 간단한 설정만으로 사용이 가능했다.
- 돈이 들지 않는다.
- 개인용으로 Free 제한은 최대 6명이며, 연결 기기는 무제한이다.
- 다만 요금제 설명에 비상업 용도라고 적혀 있으니 쓰기 전에 한 번 읽어 보는 게 좋다.

(우) Tailscale 로 바뀐 것.
처음에는 compose에 Tailscale 사이드카 컨테이너를 붙이는 방법도 검토했다. 그런데 n8n을 그 컨테이너의 네트워크에 묶으면 nginx가 이름으로 n8n을 찾는 설정, 서비스 기동 순서, CI가 컨테이너 수를 세는 검사와 줄줄이 부딪혔다. 호스트에 tailscaled 하나를 두는 쪽이 훨씬 덜 번거로웠다. Mac과 A1 사이에 Python으로 FastAPI 게이트웨이를 둘까도 생각했는데, 지금은 n8n이 oMLX와 Draw Things를 직접 부르는 것으로 충분해서 미뤄 두었다.

① 개인 요금제는 $0
② 기기 수 제한 없음
③ 사용자 6명까지.
OCI A1에 붙이기 — Oracle Linux 10
Oracle Linux 10.2(aarch64)인 A1에서는 설치를 cloud-init에 맡기고 tailnet 가입만 사람이 한 번 한다.
저장소와 설치는 cloud-init이 맡는다
Tailscale 공식 저장소에 Oracle Linux 10용 패키지가 있다. 공식 안내는 dnf config-manager --add-repo로 저장소를 더하라고 하지만, 나는 인스턴스를 처음 만들 때 한 번에 끝내려고 cloud-init의 write_files로 저장소 파일을 직접 두었다.

– ① 저장소 ② 설치 ③ 데몬 기동 ④ 가입.
– ①은 cloud-init 파일로, ②·③은 bootstrap.sh로 대신했고
– ④만 수동으로 진행 한다.
# cloud-init.yaml.tftpl (발췌)
write_files:
- path: /etc/yum.repos.d/tailscale-stable.repo
permissions: '0644'
content: |
[tailscale-stable]
name=Tailscale stable
baseurl=https://pkgs.tailscale.com/stable/oracle/${os_version}/$basearch
enabled=1
type=rpm
repo_gpgcheck=1
gpgcheck=1
gpgkey=https://pkgs.tailscale.com/stable/oracle/${os_version}/repo.gpg
skip_if_unavailable=1
Code language: YAML (yaml)
설치는 A1 VM 인스턴스 생성시의 cloud-init이 부르는 bootstrap.sh가 한다.
# bootstrap.sh (발췌)
retry dnf -y install tailscale ||
die "tailscale 설치 실패 (dnf repolist --enabled 에 tailscale-stable 이 있는지 보라)"
systemctl enable --now tailscaled
Code language: Bash (bash)
cloud-init의 packages: 목록에 tailscale을 넣지 않은 데는 이유가 있다. 그 목록은 Podman 같은 다른 패키지와 한 번의 dnf 작업으로 묶여 설치된다. 외부 저장소인 pkgs.tailscale.com이 잠깐 응답하지 않으면 Podman 설치까지 같이 실패한다. 그래서 설치를 따로 떼고 저장소에는 skip_if_unavailable=1을 달아 그 저장소가 죽어 있어도 OS 자동 업데이트가 함께 멈추지 않게 했다.

① Oracle Linux 10 저장소
② 저장소가 응답하지 않아도 dnf가 멈추지 않게 하는 설정
③ 설치된 1.104.1과 실행 중인 tailscaled
④ Tailnet machine의 키 만료를 끄면 KeyExpiry가 null로 나온다.
가입은 Bastion으로 들어가 수동으로 진행한다
가입용 auth key를 cloud-init에 넣으면 사람 손 없이 끝낼 수 있다. 하지만 cloud-init 내용(user_data)은 OCI 콘솔과 인스턴스 메타데이터에서 그대로 볼 수 있다. 그래서 키를 넣지 않고 인스턴스가 뜬 뒤 Bastion으로 들어가 대화형으로 가입하는 방식을 택했다.
scripts/bastion.sh ssh -- 'sudo tailscale up --hostname=blog-a1 --timeout=8s; tailscale status --json | jq -r .AuthURL'
# 나온 주소를 브라우저에서 열어 승인한다
Code language: Bash (bash)
tailscale up이 승인 주소를 찍지 않고 멈출 때가 있었다. 그럴 때도 tailscale status --json의 AuthURL에는 값이 들어 있다. 승인하고 나면 관리 콘솔의 Machines 목록에 blog-a1이 생긴다.
키 만료는 콘솔에서 끈다
Tailscale은 기기 키에 만료 기간을 두는데 기본값인 180일이 지나면 그 기기는 아무 알림 없이 tailnet에서 빠진다. 반년 뒤 어느 날 n8n의 AI 호출이 전부 timeout으로 끝나도 원인을 바로 떠올리기 어렵다. 나는 가입하자마자 Machines 목록에서 기기 오른쪽 메뉴를 열어 키 만료를 껐다. Mac과 휴대폰도 똑같이 했다.

① 세 기기 모두 키 만료를 껐다(Expiry disabled)
② A1은 OS 자동 업데이트로 1.104.1
③ Homebrew로 깐 Mac은 1.102.4라 새 버전 표시가 떠 있다.
방화벽은 건드리지 않는다
컨테이너가 호스트의 tailnet 방향으로 나갈때 firewalld를 고칠 필요는 없었다. Podman 컨테이너 대역이 trusted 존의 출발지로 들어가 있어서 바깥으로 나가는 패킷은 그대로 통과한다. 컨테이너의 출발지 주소는 Podman의 netavark가 A1의 tailnet 주소로 바꿔 내보내 준다. 반대로 tailscale0 인터페이스는 어느 존에도 속하지 않았다. 그래서 tailnet에서 A1으로 들어오는 연결은 public 존이 막는다. A1은 Mac으로 내보내기만 하면 되니 이편이 관리가 편하다. 관리 SSH도 지금처럼 OCI Bastion으로만 들어갈 수 있다.

① Mac과 직결(direct) 상태
② MagicDNS가 resolv.conf를 덮어써서 생긴 경고
③ tailscale0는 forwarding=1이고 어느 존에도 묶이지 않았다
④ trusted 존의 출발지는 Podman 대역뿐이다.
OCI 보안 규칙에는 SSH 22번(Bastion 서브넷) 말고 아무것도 손댈 필요가 없었다. Tailscale 연결용으로 쓰는 UDP 41641을 열어야 하나 싶었는데, 양쪽이 동시에 밖으로 UDP를 보내 NAT를 뚫는 방식이라 포워딩 규칙을 필요로 하지 않았다.

② 연결이 되지 않을때 사용 가능한 중계 서버는 도쿄였으며 지연시간은 9.5ms 였다.
인스턴스를 지우고 다시 만들 때는 콘솔에서 옛 blog-a1을 먼저 지워야 한다. 지우지 않고 가입하면 새 기기 이름이 blog-a1-1이 된다. (깜빡하기 쉽다)
Mac에 붙이기 — 서비스는 127.0.0.1에, 바깥으로는 serve로
Homebrew의 tailscaled를 쓰는 이유
Tailscale에서 가이드 하는 macOS용 설치 버전은 App Store 설치버전, 단독 설치 버전, 오픈소스 tailscaled 버전까지 세 종류인데 앞의 둘은 macOS 로그인 상태에서 설치가 가능하다. tailscaled는 시스템 데몬으로 떠서 로그인 전에도 tailnet에 붙을 수 있다. 내 Mac은 FileVault를 켜 둔 노트북이라 재부팅 뒤 누군가 로그인하기 전까지는 사용자 세션이 없다. 그 사이에도 tailnet은 살아 있어야 해서 Homebrew 설치버전을 골랐다. 대신 메뉴 막대 아이콘 같은 화면은 없고 모든 걸 터미널에서 작업해야 한다. 앱 자체의 자동 업데이트도 없어서 brew upgrade로 직접 버전을 올려야 한다. dnf 자동 업데이트를 받는 A1은 1.104.1인데 Mac은 아직 1.102.4다.

① tailscaled만 로그인 전에 뜬다
② 대신 화면(GUI)이 없고
③ 앱 자체의 자동 업데이트도 없다.
brew install tailscale
sudo brew services start tailscale # /Library/LaunchDaemons 에 등록 — 로그인 전에 뜬다
tailscale up # 브라우저 승인 후 Machines 에서 키 만료 끄기
# 127.0.0.1 에만 열린 서비스를 tailnet 전용 주소로 내보낸다 (설정은 저장되어 재부팅 뒤에도 유지)
tailscale serve --bg --https=8000 http://127.0.0.1:8000 # oMLX
tailscale serve --bg --https=7860 http://127.0.0.1:7860 # Draw Things
tailscale serve --bg --https=8428 http://127.0.0.1:8428 # VictoriaMetrics (A1 메트릭 수신)
tailscale serve --bg --http=3000 http://127.0.0.1:3000 # Grafana
Code language: Bash (bash)
tailscaled는 로그인 전에 뜨지만 Draw Things는 화면이 있는 앱이라 로그인해야 뜬다. 이 Mac은 자동 로그인을 켜 두었다. 이걸 빼먹으면 재부팅 뒤 LLM은 되는데 그림만 안 되는 애매한 상태가 된다.

① Homebrew가 등록한 시스템 데몬으로 돌고 있다
② A1과의 연결이 활성화 되어 있다
③ 키 만료를 꺼서 KeyExpiry가 null이다.
Mac의 서비스는 All Open으로 열지 않는다
oMLX, Draw Things, VictoriaMetrics, Grafana는 모두 127.0.0.1에만 열려 있다. 같은 와이파이에 붙은 다른 기기도 이 포트에는 들어오지 못한다. 바깥에서 들어오는 길은 tailscale serve 하나뿐이고 serve가 내놓은 주소는 tailnet 안에서만 열린다.

② oMLX는 https로 tailnet에만(tailnet only) 내보냈다
③ 실제 서비스는 모두 127.0.0.1로만 수신하고 있다
HTTPS 주소는 반드시 이름으로 불러야 한다. serve가 받아 둔 인증서는 m4max-….ts.net이라는 이름에 발급된 것이라 https://100.87.218.2:8000처럼 IP로 부르면 TLS 확인에서 실패한다. Grafana는 http로 내보냈지만 tailnet 구간은 WireGuard로 암호화되니 걱정할 필요가 없다
컨테이너 안에서는 이름을 고정한다
n8n 컨테이너가 Mac의 이름을 찾는 방법은 하나 더 손봤다. 컨테이너의 DNS는 Podman의 DNS 서버를 거쳐 호스트의 /etc/resolv.conf를 따라가고, A1에서는 그 파일을 tailscaled가 MagicDNS로 덮어쓴다. tailscaled가 업데이트로 재시작되는 DNS Lookup이 실패할 수 있다. 이걸 막으려고 compose의 extra_hosts로 이름과 tailnet IP를 컨테이너의 /etc/hosts에 명시해 두었다.
# compose.yaml — n8n 서비스 (발췌)
extra_hosts:
- "${AI_TAILNET_HOST}:${AI_TAILNET_IP}" # m4max-….ts.net : 100.87.218.2 — dev·prod 같은 값
Code language: YAML (yaml)
이름은 그대로 두고 IP만 고정하는 방식이라 TLS 확인은 여전히 이름으로 이뤄진다. dev와 prod 설정 파일에 같은 값이 들어간다.
Tailscale로 Private 네트워크를 구성하고
dev와 prod가 같은 주소를 이용할 수 있게 되었다
n8n 워크플로우에 적힌 LLM 주소는 https://m4max-….ts.net:8000/v1 하나다. dev(Mac)의 n8n이든 prod(A1)의 n8n이든 이 주소를 부르면 같은 Mac의 oMLX가 받는다. 컨테이너에서 나간 네트웍 경로만 다르다. Mac에서는 Podman의 가상 머신(gvproxy)을 거쳐 자기 자신에게 돌아오고, A1에서는 tailscale0을 타고 WireGuard로 오사카에서 Mac까지 올 수 있다. 워크플로우는 그 차이를 알 필요가 없게 된다.

경로를 확인하는 스크립트(ai-check.sh)로 확인할 수 있도록 했다. 호스트의 tailnet 상태, 컨테이너 안의 DNS Lookup, oMLX 인증, Draw Things 응답을 차례대로 확인한다. 아래는 A1에서 돌린 결과다. Mac에서 dev로 돌려도 n8n 컨테이너 안의 네 줄은 똑같이 PASS가 나온다.

① 호스트 쪽 — tailscale0 forwarding, compose에 박은 IP와 실제 tailnet IP 대조, 직결 확인
② n8n 컨테이너 안에서 Mac 이름이 고정한 IP로 풀린다
③ oMLX는 키 없이 401, 키를 주면 200, Draw Things는 200
④ All PASS!!
서버를 옮겨도 AI 연결은 그대로다
prod는 처음에 Mac에서 돌다가 9월 27일 A1 이 생성된 이후 OCI로 이전 시켰다. 그 뒤에도 A1이 죽은 상황을 가정해 Mac이 prod를 넘겨받고 A1을 새로 만들어 다시 돌려받는 왕복 전환 시험을 두세 번 테스트 했다. Mac이 비상으로 넘겨받는 데 8분 44초, 새 A1으로 전환하는 데 3분 20초가 걸렸다. 그동안 워크플로우의 AI 주소는 한 글자도 고치지 않았다. 어느 쪽이 prod를 맡든 ai-check.sh는 모두 PASS였다. 새로 만든 A1에서 손으로 tailnet 가입을 한 번 승인하는 과정만 있으면 되었다.

AI 주소를 host.containers.internal처럼 “같은 기계” 전제로 적어 두었다면 서버를 옮길 때마다, 그것도 다운타임 중에 워크플로우를 고쳐야 했을 것이다.
prod 모니터링을 Private 네트워크를 통해 Mac에 전송한다
A1에는 Grafana Alloy와 Podman 메트릭 수집기를 깔았다. Alloy가 30초마다 tailnet으로 Mac의 VictoriaMetrics에 메트릭을 밀어 넣는다(push). A1은 내보내기만 하니 들어오는 포트를 열 필요가 없다. Mac 노트북이 꺼져 있거나 이동중이라 Alloy가 보내지 못하더라도 최대 7일까지 A1의 디스크(WAL)에 쌓아 두었다가 Mac이 연결되면 밀린 구간을 그대로 채운다.(Backfill) VictoriaMetrics은 프로메테우스 보다 늦게 도착한 메트릭도 백필이 가능하기 때문에 사용하고 있다. 메트릭 수신기를 6분 동안 멈췄다 켜는 테스트에서 빈 구간이 그대로 메워지는 것을 확인했다.

① A1의 마지막 메트릭이 9초 전에 도착했다
② A1 tailscale0의 받기·보내기 — 보내기는 대부분 30초마다 밀어 넣는 메트릭이다
③ 24시간 그래프가 끊김 없이 이어진다.
핸드폰으로 백엔드를 늘 들여다 볼 수 있게 되었다
최고에 이점중 하나는 핸드폰으로도 Tailscale 연결이 가능하다는 것이다. 아이폰에 Tailscale 앱을 깔고 같은 tailnet에 넣고 외부에 있어도 블로그 백엔드를 점검할 수 있게 되었다.
- Grafana:
http://m4max-….ts.net:3000을 열면 A1 요약 대시보드가 바로 뜬다. 공개 터널에는 올리지 않은 주소다. - oMLX 관리자:
https://m4max-….ts.net:8000/admin에서 올라가 있는 모델을 보고 내리거나 바꾼다. - SSH: 아이폰 iSSH 앱으로 tailnet 이름의 Mac에 로그인(macOS 원격 로그인) ->
stack.sh로 dev를 만지고bastion.sh로 A1의 prod까지 들어간다.

글 승인은 텔레그램으로 받고 있었지만 무언가 이상할 때 확인하려면 결국 노트북을 열어야 했다. 지금은 휴대폰만 있으면 그래프를 보고 모델을 내리고 컨테이너 로그도 볼 수 있다. 이 모든 주소가 인터넷에 공개하지 않아도 된다는 점도 마음이 편하다.

① Grafana A1 요약
② oMLX 관리자 로그인.
생각해봐야할 것들, Tailscale로 얻은 것들
- 키 만료 180일.
- 보안을 위해 Machine키를 Rotation하는 것이 좋지만 관리 편의성을 위해 만료되지 않도록 했다.
- 내부호출도 HTTPS를 사용, DNS Name으로 호출한다.
- Tailscale 에서 TLS 인증서를 관리해준다. n8n 자격증명의 Base URL도 이것을 이용한다.
- 호스트 DNS가 tailscaled에 묶인다.
- A1의
/etc/resolv.conf는 tailscaled가 MagicDNS(100.100.100.100)로 덮어쓴다. 지금 A1의tailscale status에도 “System DNS config not ideal”이라는 경고가 떠 있다. - 컨테이너가 Mac을 찾는 일은
extra_hosts로 고정해 두어서 영향이 없다. 언젠가 DNS 장애가 tailscaled로 전파 된다면sudo tailscale set --accept-dns=false한 줄로 제거할 생각이다.
- A1의
- 기기를 지웠다 다시 넣으면 IP가 바뀐다.
extra_hosts에 명시한 IP도 같이 고쳐야 한다. 그래서ai-check.sh가 실제 tailnet IP와 설정값을 맞춰주도록 했다. Mac 기기는 지우지 않는다.
- Mac이 꺼져도 블로그는 멈추지 않는다.
- AI 호출을 tailnet에 맡겼어도 Mac이 응답하지 않으면 그 호출만 실패하고 다음 차례로 넘어간다. n8n의 LLM 체인은 다음 모델(무료·유료 API)을 사용하도록 해두었다. 임베딩은 OpenRouter가, 그림은 Cloudflare의 이미지 모델이나 pdf 컨티에너의 글자 카드가 역할을 하도록 했다.
정리
공개 트래픽은 Cloudflare Tunnel에 맡기고 백엔드 트래픽은 Tailscale로 보냈다. 이렇게 둘로 나누니 블로그 플랫폼 어디에서도 Public 포트를 열어둘 필요가 없었다. A1에서는 tailscaled 하나를 깔고 한 번 가입했을 뿐이고 Mac은 serve 네 줄로 끝났다. 그 덕에 dev와 prod는 서버를 옮겨도 바뀌지 않는 같은 주소로 Mac의 AI를 부르고 A1의 상태와 백엔드 관리 화면은 휴대폰에서도 프라이빗 네트워크를 이용 가능하게 되었다.
A1 안에서 컨테이너가 바깥으로 나가지 못하던 문제는 Podman forwarding=0 글에 따로 적었다. tailnet으로 나가는 길도 같은 문제에 걸릴 수 있어서 ai-check.sh가 제일 먼저 그 값을 확인하도록 해두었다.