tayo 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +38 -79
- data/lib/tayo/cli.rb +0 -6
- data/lib/tayo/commands/cf.rb +137 -30
- data/lib/tayo/commands/gh.rb +90 -55
- data/lib/tayo/commands/init.rb +15 -2
- data/lib/tayo/version.rb +1 -1
- metadata +2 -10
- data/lib/tayo/commands/proxy.rb +0 -97
- data/lib/tayo/proxy/cloudflare_client.rb +0 -323
- data/lib/tayo/proxy/docker_manager.rb +0 -150
- data/lib/tayo/proxy/network_config.rb +0 -147
- data/lib/tayo/proxy/traefik_config.rb +0 -303
- data/lib/tayo/proxy/welcome_service.rb +0 -337
- data/lib/templates/welcome/Dockerfile +0 -14
- data/lib/templates/welcome/index.html +0 -173
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "colorize"
|
|
4
|
-
require "fileutils"
|
|
5
|
-
require "yaml"
|
|
6
|
-
require "erb"
|
|
7
|
-
|
|
8
|
-
module Tayo
|
|
9
|
-
module Proxy
|
|
10
|
-
class TraefikConfig
|
|
11
|
-
TRAEFIK_CONFIG_DIR = File.expand_path("~/.tayo/traefik")
|
|
12
|
-
|
|
13
|
-
def initialize
|
|
14
|
-
@docker = DockerManager.new
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def setup(domains, email = nil)
|
|
18
|
-
puts "\n⚙️ Traefik을 설정합니다...".colorize(:yellow)
|
|
19
|
-
|
|
20
|
-
# 설정 디렉토리 생성
|
|
21
|
-
setup_directories
|
|
22
|
-
|
|
23
|
-
# 이메일 주소 입력 (Let's Encrypt용)
|
|
24
|
-
email ||= get_email_for_acme
|
|
25
|
-
|
|
26
|
-
# 설정 파일 생성
|
|
27
|
-
create_docker_compose(email)
|
|
28
|
-
create_traefik_config(email)
|
|
29
|
-
create_dynamic_config(domains)
|
|
30
|
-
|
|
31
|
-
# Traefik 시작
|
|
32
|
-
ensure_running
|
|
33
|
-
|
|
34
|
-
# 라우팅 설정
|
|
35
|
-
configure_routes(domains)
|
|
36
|
-
|
|
37
|
-
puts "✅ Traefik 설정이 완료되었습니다.".colorize(:green)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
def setup_directories
|
|
43
|
-
FileUtils.mkdir_p(TRAEFIK_CONFIG_DIR)
|
|
44
|
-
FileUtils.mkdir_p(File.join(TRAEFIK_CONFIG_DIR, "config"))
|
|
45
|
-
|
|
46
|
-
# acme.json 파일 생성 및 권한 설정
|
|
47
|
-
acme_file = File.join(TRAEFIK_CONFIG_DIR, "acme.json")
|
|
48
|
-
unless File.exist?(acme_file)
|
|
49
|
-
File.write(acme_file, "{}")
|
|
50
|
-
File.chmod(0600, acme_file)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def get_email_for_acme
|
|
55
|
-
prompt = TTY::Prompt.new
|
|
56
|
-
|
|
57
|
-
# 저장된 이메일 확인
|
|
58
|
-
email_file = File.join(TRAEFIK_CONFIG_DIR, ".email")
|
|
59
|
-
if File.exist?(email_file)
|
|
60
|
-
saved_email = File.read(email_file).strip
|
|
61
|
-
if prompt.yes?("저장된 이메일을 사용하시겠습니까? (#{saved_email})")
|
|
62
|
-
return saved_email
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# 새 이메일 입력
|
|
67
|
-
email = prompt.ask("Let's Encrypt 인증서용 이메일 주소를 입력하세요:") do |q|
|
|
68
|
-
q.validate(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i, "올바른 이메일 형식을 입력해주세요")
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# 이메일 저장
|
|
72
|
-
File.write(email_file, email)
|
|
73
|
-
email
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def create_docker_compose(email)
|
|
77
|
-
compose_content = <<~YAML
|
|
78
|
-
version: '3.8'
|
|
79
|
-
|
|
80
|
-
services:
|
|
81
|
-
traefik:
|
|
82
|
-
image: traefik:v3.0
|
|
83
|
-
container_name: traefik
|
|
84
|
-
restart: unless-stopped
|
|
85
|
-
security_opt:
|
|
86
|
-
- no-new-privileges:true
|
|
87
|
-
networks:
|
|
88
|
-
- traefik-net
|
|
89
|
-
ports:
|
|
90
|
-
- "80:80"
|
|
91
|
-
- "443:443"
|
|
92
|
-
- "8080:8080" # 대시보드
|
|
93
|
-
extra_hosts:
|
|
94
|
-
- "host.docker.internal:host-gateway"
|
|
95
|
-
volumes:
|
|
96
|
-
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
97
|
-
- #{TRAEFIK_CONFIG_DIR}/config/traefik.yml:/etc/traefik/traefik.yml:ro
|
|
98
|
-
- #{TRAEFIK_CONFIG_DIR}/config/dynamic.yml:/etc/traefik/dynamic.yml:ro
|
|
99
|
-
- #{TRAEFIK_CONFIG_DIR}/acme.json:/acme.json
|
|
100
|
-
labels:
|
|
101
|
-
- "traefik.enable=true"
|
|
102
|
-
- "traefik.http.routers.dashboard.rule=Host(`traefik.localhost`)"
|
|
103
|
-
- "traefik.http.routers.dashboard.service=api@internal"
|
|
104
|
-
- "traefik.http.routers.dashboard.middlewares=auth"
|
|
105
|
-
- "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$10$$YFPx3EmK6lN5bPG.zPNvp.UYQhkPvNnkZ7J4zYu2GODXJfHZXfYbK" # admin:admin
|
|
106
|
-
|
|
107
|
-
networks:
|
|
108
|
-
traefik-net:
|
|
109
|
-
name: traefik-net
|
|
110
|
-
driver: bridge
|
|
111
|
-
YAML
|
|
112
|
-
|
|
113
|
-
compose_file = File.join(TRAEFIK_CONFIG_DIR, "docker-compose.yml")
|
|
114
|
-
File.write(compose_file, compose_content)
|
|
115
|
-
puts "✅ Docker Compose 파일이 생성되었습니다.".colorize(:green)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def create_traefik_config(email = nil)
|
|
119
|
-
# 이메일 주소 확인
|
|
120
|
-
email_file = File.join(TRAEFIK_CONFIG_DIR, ".email")
|
|
121
|
-
email ||= File.exist?(email_file) ? File.read(email_file).strip : "admin@example.com"
|
|
122
|
-
|
|
123
|
-
config_content = <<~YAML
|
|
124
|
-
# Traefik 정적 설정
|
|
125
|
-
api:
|
|
126
|
-
dashboard: true
|
|
127
|
-
debug: false
|
|
128
|
-
|
|
129
|
-
entryPoints:
|
|
130
|
-
web:
|
|
131
|
-
address: ":80"
|
|
132
|
-
http:
|
|
133
|
-
redirections:
|
|
134
|
-
entryPoint:
|
|
135
|
-
to: websecure
|
|
136
|
-
scheme: https
|
|
137
|
-
permanent: true
|
|
138
|
-
websecure:
|
|
139
|
-
address: ":443"
|
|
140
|
-
|
|
141
|
-
providers:
|
|
142
|
-
docker:
|
|
143
|
-
endpoint: "unix:///var/run/docker.sock"
|
|
144
|
-
exposedByDefault: false
|
|
145
|
-
network: traefik-net
|
|
146
|
-
watch: true
|
|
147
|
-
file:
|
|
148
|
-
filename: /etc/traefik/dynamic.yml
|
|
149
|
-
watch: true
|
|
150
|
-
|
|
151
|
-
certificatesResolvers:
|
|
152
|
-
myresolver:
|
|
153
|
-
acme:
|
|
154
|
-
email: #{email}
|
|
155
|
-
storage: /acme.json
|
|
156
|
-
tlsChallenge: {}
|
|
157
|
-
# caServer: https://acme-staging-v02.api.letsencrypt.org/directory # 테스트용
|
|
158
|
-
|
|
159
|
-
log:
|
|
160
|
-
level: INFO
|
|
161
|
-
format: json
|
|
162
|
-
|
|
163
|
-
accessLog:
|
|
164
|
-
format: json
|
|
165
|
-
YAML
|
|
166
|
-
|
|
167
|
-
config_file = File.join(TRAEFIK_CONFIG_DIR, "config", "traefik.yml")
|
|
168
|
-
File.write(config_file, config_content)
|
|
169
|
-
puts "✅ Traefik 설정 파일이 생성되었습니다.".colorize(:green)
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def create_dynamic_config(domains)
|
|
173
|
-
routers = {}
|
|
174
|
-
services = {}
|
|
175
|
-
|
|
176
|
-
domains.each do |domain_info|
|
|
177
|
-
domain = domain_info[:domain]
|
|
178
|
-
safe_name = domain.gsub('.', '-').gsub('_', '-')
|
|
179
|
-
|
|
180
|
-
# HTTP 라우터 (리다이렉트용)
|
|
181
|
-
routers["#{safe_name}-http"] = {
|
|
182
|
-
"rule" => "Host(`#{domain}`)",
|
|
183
|
-
"entryPoints" => ["web"],
|
|
184
|
-
"middlewares" => ["redirect-to-https"],
|
|
185
|
-
"service" => "#{safe_name}-service"
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
# HTTPS 라우터
|
|
189
|
-
routers["#{safe_name}-https"] = {
|
|
190
|
-
"rule" => "Host(`#{domain}`)",
|
|
191
|
-
"entryPoints" => ["websecure"],
|
|
192
|
-
"service" => "#{safe_name}-service",
|
|
193
|
-
"tls" => {
|
|
194
|
-
"certResolver" => "myresolver"
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
# 서비스 정의 (호스트의 3000 포트로)
|
|
199
|
-
services["#{safe_name}-service"] = {
|
|
200
|
-
"loadBalancer" => {
|
|
201
|
-
"servers" => [
|
|
202
|
-
{ "url" => "http://host.docker.internal:3000" }
|
|
203
|
-
]
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# 미들웨어 정의
|
|
209
|
-
dynamic_config = {
|
|
210
|
-
"http" => {
|
|
211
|
-
"middlewares" => {
|
|
212
|
-
"redirect-to-https" => {
|
|
213
|
-
"redirectScheme" => {
|
|
214
|
-
"scheme" => "https",
|
|
215
|
-
"permanent" => true
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
},
|
|
219
|
-
"routers" => routers,
|
|
220
|
-
"services" => services
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
dynamic_file = File.join(TRAEFIK_CONFIG_DIR, "config", "dynamic.yml")
|
|
225
|
-
File.write(dynamic_file, dynamic_config.to_yaml)
|
|
226
|
-
puts "✅ 동적 라우팅 설정이 생성되었습니다.".colorize(:green)
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
def ensure_running
|
|
230
|
-
if @docker.container_running?("traefik")
|
|
231
|
-
puts "🔄 Traefik 컨테이너를 재시작합니다...".colorize(:yellow)
|
|
232
|
-
reload_traefik
|
|
233
|
-
else
|
|
234
|
-
start_container
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
def start_container
|
|
239
|
-
puts "🚀 Traefik 컨테이너를 시작합니다...".colorize(:yellow)
|
|
240
|
-
|
|
241
|
-
# 기존 컨테이너가 있다면 제거
|
|
242
|
-
if @docker.container_exists?("traefik")
|
|
243
|
-
@docker.stop_container("traefik")
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
# Docker Compose로 시작
|
|
247
|
-
Dir.chdir(TRAEFIK_CONFIG_DIR) do
|
|
248
|
-
if system("docker compose up -d")
|
|
249
|
-
puts "✅ Traefik이 시작되었습니다.".colorize(:green)
|
|
250
|
-
|
|
251
|
-
# 시작 완료 대기
|
|
252
|
-
sleep 3
|
|
253
|
-
|
|
254
|
-
# 상태 확인
|
|
255
|
-
check_traefik_status
|
|
256
|
-
else
|
|
257
|
-
puts "❌ Traefik 시작에 실패했습니다.".colorize(:red)
|
|
258
|
-
exit 1
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def reload_traefik
|
|
264
|
-
puts "🔄 Traefik 설정을 다시 로드합니다...".colorize(:yellow)
|
|
265
|
-
|
|
266
|
-
Dir.chdir(TRAEFIK_CONFIG_DIR) do
|
|
267
|
-
if system("docker compose restart")
|
|
268
|
-
puts "✅ Traefik이 재시작되었습니다.".colorize(:green)
|
|
269
|
-
else
|
|
270
|
-
puts "⚠️ Traefik 재시작에 실패했습니다.".colorize(:yellow)
|
|
271
|
-
end
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def check_traefik_status
|
|
276
|
-
# 대시보드 접근 확인
|
|
277
|
-
puts "\n📊 Traefik 대시보드: http://localhost:8080".colorize(:cyan)
|
|
278
|
-
puts " (기본 인증: admin / admin)".colorize(:gray)
|
|
279
|
-
|
|
280
|
-
# 로그 확인
|
|
281
|
-
logs = `docker logs traefik --tail 5 2>&1`.strip
|
|
282
|
-
if logs.include?("error") || logs.include?("Error")
|
|
283
|
-
puts "\n⚠️ Traefik 로그에 오류가 발견되었습니다:".colorize(:yellow)
|
|
284
|
-
puts logs.colorize(:gray)
|
|
285
|
-
end
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
def configure_routes(domains)
|
|
289
|
-
puts "\n📝 도메인 라우팅 상태:".colorize(:yellow)
|
|
290
|
-
|
|
291
|
-
domains.each do |domain_info|
|
|
292
|
-
domain = domain_info[:domain]
|
|
293
|
-
puts " • #{domain} → localhost:3000".colorize(:green)
|
|
294
|
-
puts " HTTP: http://#{domain} (→ HTTPS 리다이렉트)".colorize(:gray)
|
|
295
|
-
puts " HTTPS: https://#{domain} (Let's Encrypt 인증서)".colorize(:gray)
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
puts "\n💡 Let's Encrypt 인증서 발급 중...".colorize(:yellow)
|
|
299
|
-
puts " 첫 발급에는 1-2분이 소요될 수 있습니다.".colorize(:gray)
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
end
|
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "colorize"
|
|
4
|
-
require "fileutils"
|
|
5
|
-
|
|
6
|
-
module Tayo
|
|
7
|
-
module Proxy
|
|
8
|
-
class WelcomeService
|
|
9
|
-
def initialize
|
|
10
|
-
@docker = DockerManager.new
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def ensure_running
|
|
14
|
-
# 호스트에서 3000 포트 서비스 확인 (Docker 제외)
|
|
15
|
-
if host_port_in_use?(3000)
|
|
16
|
-
puts "✅ 3000 포트에 호스트 서비스(Rails 등)가 실행 중입니다.".colorize(:green)
|
|
17
|
-
|
|
18
|
-
# 기존 Welcome 컨테이너가 있다면 중지
|
|
19
|
-
if @docker.container_running?("tayo-welcome")
|
|
20
|
-
puts "🛑 기존 Welcome 서비스를 중지합니다...".colorize(:yellow)
|
|
21
|
-
@docker.stop_container("tayo-welcome")
|
|
22
|
-
end
|
|
23
|
-
return
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Docker 컨테이너로 3000 포트가 사용 중인 경우
|
|
27
|
-
if @docker.container_running?("tayo-welcome")
|
|
28
|
-
puts "✅ Welcome 서비스가 이미 실행 중입니다.".colorize(:green)
|
|
29
|
-
return
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
puts "\n🚀 3000 포트에 Welcome 서비스를 시작합니다...".colorize(:yellow)
|
|
33
|
-
puts " (Rails 서버를 시작하면 자동으로 중지됩니다)".colorize(:gray)
|
|
34
|
-
|
|
35
|
-
prepare_welcome_files
|
|
36
|
-
build_image
|
|
37
|
-
start_container
|
|
38
|
-
|
|
39
|
-
puts "✅ Welcome 서비스가 시작되었습니다.".colorize(:green)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
def host_port_in_use?(port)
|
|
45
|
-
# macOS와 Linux에서 호스트 포트 확인 (Docker 컨테이너 제외)
|
|
46
|
-
if RUBY_PLATFORM.include?("darwin")
|
|
47
|
-
# macOS: lsof 사용, Docker 프로세스 제외
|
|
48
|
-
output = `lsof -i :#{port} -sTCP:LISTEN 2>/dev/null | grep -v docker | grep -v com.docke | grep -v tayo-welcome`.strip
|
|
49
|
-
!output.empty?
|
|
50
|
-
else
|
|
51
|
-
# Linux: netstat 또는 ss 사용
|
|
52
|
-
output = `netstat -tln 2>/dev/null | grep ":#{port} " | grep -v docker`.strip
|
|
53
|
-
if output.empty?
|
|
54
|
-
output = `ss -tln 2>/dev/null | grep ":#{port} " | grep -v docker`.strip
|
|
55
|
-
end
|
|
56
|
-
!output.empty?
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def template_dir
|
|
61
|
-
File.expand_path("../../../templates/welcome", __FILE__)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def prepare_welcome_files
|
|
65
|
-
puts "📁 Welcome 서비스 파일을 준비합니다...".colorize(:yellow)
|
|
66
|
-
|
|
67
|
-
# 템플릿 디렉토리 생성
|
|
68
|
-
FileUtils.mkdir_p(template_dir)
|
|
69
|
-
|
|
70
|
-
# Dockerfile 생성
|
|
71
|
-
create_dockerfile
|
|
72
|
-
|
|
73
|
-
# index.html 생성
|
|
74
|
-
create_index_html
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def create_dockerfile
|
|
78
|
-
dockerfile_content = <<~DOCKERFILE
|
|
79
|
-
FROM nginx:alpine
|
|
80
|
-
|
|
81
|
-
# Nginx 설정
|
|
82
|
-
RUN echo 'server { listen 80; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ =404; } }' > /etc/nginx/conf.d/default.conf
|
|
83
|
-
|
|
84
|
-
# HTML 파일 복사
|
|
85
|
-
COPY index.html /usr/share/nginx/html/
|
|
86
|
-
|
|
87
|
-
# 헬스체크
|
|
88
|
-
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
89
|
-
CMD wget --no-verbose --tries=1 --spider http://localhost || exit 1
|
|
90
|
-
|
|
91
|
-
EXPOSE 80
|
|
92
|
-
|
|
93
|
-
CMD ["nginx", "-g", "daemon off;"]
|
|
94
|
-
DOCKERFILE
|
|
95
|
-
|
|
96
|
-
File.write(File.join(template_dir, "Dockerfile"), dockerfile_content)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def create_index_html
|
|
100
|
-
html_content = <<~HTML
|
|
101
|
-
<!DOCTYPE html>
|
|
102
|
-
<html lang="ko">
|
|
103
|
-
<head>
|
|
104
|
-
<meta charset="UTF-8">
|
|
105
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
106
|
-
<title>Tayo Proxy - Welcome</title>
|
|
107
|
-
<style>
|
|
108
|
-
* {
|
|
109
|
-
margin: 0;
|
|
110
|
-
padding: 0;
|
|
111
|
-
box-sizing: border-box;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
body {
|
|
115
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
116
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
117
|
-
min-height: 100vh;
|
|
118
|
-
display: flex;
|
|
119
|
-
align-items: center;
|
|
120
|
-
justify-content: center;
|
|
121
|
-
color: white;
|
|
122
|
-
padding: 20px;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
.container {
|
|
126
|
-
text-align: center;
|
|
127
|
-
max-width: 600px;
|
|
128
|
-
animation: fadeIn 1s ease-in;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
@keyframes fadeIn {
|
|
132
|
-
from { opacity: 0; transform: translateY(-20px); }
|
|
133
|
-
to { opacity: 1; transform: translateY(0); }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.logo {
|
|
137
|
-
font-size: 5em;
|
|
138
|
-
margin-bottom: 20px;
|
|
139
|
-
animation: bounce 2s infinite;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
@keyframes bounce {
|
|
143
|
-
0%, 100% { transform: translateY(0); }
|
|
144
|
-
50% { transform: translateY(-10px); }
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
h1 {
|
|
148
|
-
font-size: 3em;
|
|
149
|
-
margin-bottom: 20px;
|
|
150
|
-
font-weight: 700;
|
|
151
|
-
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.subtitle {
|
|
155
|
-
font-size: 1.3em;
|
|
156
|
-
opacity: 0.95;
|
|
157
|
-
margin-bottom: 30px;
|
|
158
|
-
line-height: 1.5;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.status {
|
|
162
|
-
background: rgba(255, 255, 255, 0.2);
|
|
163
|
-
border-radius: 10px;
|
|
164
|
-
padding: 20px;
|
|
165
|
-
backdrop-filter: blur(10px);
|
|
166
|
-
margin-top: 30px;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.status-item {
|
|
170
|
-
display: flex;
|
|
171
|
-
justify-content: space-between;
|
|
172
|
-
padding: 10px 0;
|
|
173
|
-
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
.status-item:last-child {
|
|
177
|
-
border-bottom: none;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
.status-label {
|
|
181
|
-
font-weight: 600;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
.status-value {
|
|
185
|
-
opacity: 0.9;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
.status-ok {
|
|
189
|
-
color: #4ade80;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
.info-box {
|
|
193
|
-
background: rgba(255, 255, 255, 0.1);
|
|
194
|
-
border-radius: 8px;
|
|
195
|
-
padding: 15px;
|
|
196
|
-
margin-top: 30px;
|
|
197
|
-
font-size: 0.9em;
|
|
198
|
-
opacity: 0.85;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
.footer {
|
|
202
|
-
margin-top: 50px;
|
|
203
|
-
font-size: 0.85em;
|
|
204
|
-
opacity: 0.7;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
.footer a {
|
|
208
|
-
color: white;
|
|
209
|
-
text-decoration: none;
|
|
210
|
-
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
|
211
|
-
transition: border-color 0.3s;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
.footer a:hover {
|
|
215
|
-
border-bottom-color: white;
|
|
216
|
-
}
|
|
217
|
-
</style>
|
|
218
|
-
</head>
|
|
219
|
-
<body>
|
|
220
|
-
<div class="container">
|
|
221
|
-
<div class="logo">🚀</div>
|
|
222
|
-
<h1>Tayo Proxy</h1>
|
|
223
|
-
<p class="subtitle">
|
|
224
|
-
홈서버 프록시 서비스가 정상적으로 작동 중입니다<br>
|
|
225
|
-
Your home server proxy is running successfully
|
|
226
|
-
</p>
|
|
227
|
-
|
|
228
|
-
<div class="status">
|
|
229
|
-
<div class="status-item">
|
|
230
|
-
<span class="status-label">프록시 상태</span>
|
|
231
|
-
<span class="status-value status-ok">✓ 활성</span>
|
|
232
|
-
</div>
|
|
233
|
-
<div class="status-item">
|
|
234
|
-
<span class="status-label">Kamal Proxy</span>
|
|
235
|
-
<span class="status-value status-ok">✓ 실행 중</span>
|
|
236
|
-
</div>
|
|
237
|
-
<div class="status-item">
|
|
238
|
-
<span class="status-label">Caddy Server</span>
|
|
239
|
-
<span class="status-value status-ok">✓ 실행 중</span>
|
|
240
|
-
</div>
|
|
241
|
-
<div class="status-item">
|
|
242
|
-
<span class="status-label">SSL/TLS</span>
|
|
243
|
-
<span class="status-value status-ok">✓ 준비됨</span>
|
|
244
|
-
</div>
|
|
245
|
-
</div>
|
|
246
|
-
|
|
247
|
-
<div class="info-box">
|
|
248
|
-
<strong>💡 다음 단계:</strong><br>
|
|
249
|
-
이제 실제 애플리케이션을 3000 포트에 배포하면<br>
|
|
250
|
-
이 페이지 대신 애플리케이션이 표시됩니다.
|
|
251
|
-
</div>
|
|
252
|
-
|
|
253
|
-
<div class="footer">
|
|
254
|
-
<p>
|
|
255
|
-
Powered by <a href="https://github.com/TeamMilestone/tayo" target="_blank">Tayo</a> |
|
|
256
|
-
<a href="https://kamal-deploy.org" target="_blank">Kamal</a> |
|
|
257
|
-
<a href="https://caddyserver.com" target="_blank">Caddy</a>
|
|
258
|
-
</p>
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
|
|
262
|
-
<script>
|
|
263
|
-
// 현재 시간 표시 (옵션)
|
|
264
|
-
const updateTime = () => {
|
|
265
|
-
const now = new Date();
|
|
266
|
-
const timeString = now.toLocaleTimeString('ko-KR');
|
|
267
|
-
// 시간 표시 기능이 필요한 경우 활성화
|
|
268
|
-
};
|
|
269
|
-
setInterval(updateTime, 1000);
|
|
270
|
-
updateTime();
|
|
271
|
-
</script>
|
|
272
|
-
</body>
|
|
273
|
-
</html>
|
|
274
|
-
HTML
|
|
275
|
-
|
|
276
|
-
File.write(File.join(template_dir, "index.html"), html_content)
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def build_image
|
|
280
|
-
puts "🔨 Docker 이미지를 빌드합니다...".colorize(:yellow)
|
|
281
|
-
|
|
282
|
-
cmd = "docker build -t tayo-welcome:latest #{template_dir}"
|
|
283
|
-
|
|
284
|
-
if system(cmd)
|
|
285
|
-
puts "✅ Docker 이미지 빌드가 완료되었습니다.".colorize(:green)
|
|
286
|
-
else
|
|
287
|
-
puts "❌ Docker 이미지 빌드에 실패했습니다.".colorize(:red)
|
|
288
|
-
exit 1
|
|
289
|
-
end
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def start_container
|
|
293
|
-
puts "🚀 Welcome 컨테이너를 시작합니다...".colorize(:yellow)
|
|
294
|
-
|
|
295
|
-
# 기존 컨테이너가 있다면 제거
|
|
296
|
-
if @docker.container_exists?("tayo-welcome")
|
|
297
|
-
@docker.stop_container("tayo-welcome")
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
# 네트워크 확인
|
|
301
|
-
network = @docker.create_network_if_not_exists("tayo-proxy")
|
|
302
|
-
|
|
303
|
-
# Welcome 컨테이너 실행
|
|
304
|
-
cmd = <<~DOCKER
|
|
305
|
-
docker run -d \
|
|
306
|
-
--name tayo-welcome \
|
|
307
|
-
--network #{network} \
|
|
308
|
-
-p 3000:80 \
|
|
309
|
-
--restart unless-stopped \
|
|
310
|
-
tayo-welcome:latest
|
|
311
|
-
DOCKER
|
|
312
|
-
|
|
313
|
-
if system(cmd)
|
|
314
|
-
puts "✅ Welcome 서비스가 포트 3000에서 시작되었습니다.".colorize(:green)
|
|
315
|
-
|
|
316
|
-
# 서비스 확인
|
|
317
|
-
sleep 2
|
|
318
|
-
check_service_health
|
|
319
|
-
else
|
|
320
|
-
puts "❌ Welcome 서비스 시작에 실패했습니다.".colorize(:red)
|
|
321
|
-
exit 1
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
def check_service_health
|
|
326
|
-
# curl로 서비스 확인
|
|
327
|
-
response = `curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 2>/dev/null`.strip
|
|
328
|
-
|
|
329
|
-
if response == "200"
|
|
330
|
-
puts "✅ Welcome 서비스가 정상적으로 응답합니다.".colorize(:green)
|
|
331
|
-
else
|
|
332
|
-
puts "⚠️ Welcome 서비스 응답 확인 중... (HTTP #{response})".colorize(:yellow)
|
|
333
|
-
end
|
|
334
|
-
end
|
|
335
|
-
end
|
|
336
|
-
end
|
|
337
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
FROM nginx:alpine
|
|
2
|
-
|
|
3
|
-
# Nginx 설정
|
|
4
|
-
RUN echo 'server { listen 80; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ =404; } }' > /etc/nginx/conf.d/default.conf
|
|
5
|
-
|
|
6
|
-
# HTML 파일 복사
|
|
7
|
-
COPY index.html /usr/share/nginx/html/
|
|
8
|
-
|
|
9
|
-
# 헬스체크
|
|
10
|
-
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD wget --no-verbose --tries=1 --spider http://localhost || exit 1
|
|
11
|
-
|
|
12
|
-
EXPOSE 80
|
|
13
|
-
|
|
14
|
-
CMD ["nginx", "-g", "daemon off;"]
|