tayo 0.1.13 β 0.2.2
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 +67 -75
- data/README.md +77 -68
- data/lib/tayo/cli.rb +4 -4
- data/lib/tayo/commands/cf.rb +196 -221
- data/lib/tayo/commands/gh.rb +0 -9
- data/lib/tayo/commands/init.rb +35 -35
- data/lib/tayo/commands/proxy.rb +97 -0
- data/lib/tayo/proxy/cloudflare_client.rb +323 -0
- data/lib/tayo/proxy/docker_manager.rb +150 -0
- data/lib/tayo/proxy/network_config.rb +147 -0
- data/lib/tayo/proxy/traefik_config.rb +303 -0
- data/lib/tayo/proxy/welcome_service.rb +337 -0
- data/lib/tayo/version.rb +1 -1
- data/lib/templates/welcome/Dockerfile +14 -0
- data/lib/templates/welcome/index.html +173 -0
- metadata +24 -6
- data/CLAUDE.md +0 -58
- data/lib/tayo/commands/base.rb +0 -13
- data/lib/tayo/commands/sqlite.rb +0 -413
- data/scripts/setup_rubygems_key.sh +0 -60
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "colorize"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Tayo
|
|
7
|
+
module Proxy
|
|
8
|
+
class DockerManager
|
|
9
|
+
def check_containers
|
|
10
|
+
puts "\nπ³ Docker 컨ν
μ΄λ μνλ₯Ό νμΈν©λλ€...".colorize(:yellow)
|
|
11
|
+
|
|
12
|
+
unless docker_installed?
|
|
13
|
+
puts "β Dockerκ° μ€μΉλμ΄ μμ§ μμ΅λλ€.".colorize(:red)
|
|
14
|
+
puts "https://www.docker.com/get-started μμ Dockerλ₯Ό μ€μΉν΄μ£ΌμΈμ.".colorize(:cyan)
|
|
15
|
+
exit 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
unless docker_running?
|
|
19
|
+
puts "β Dockerκ° μ€νλκ³ μμ§ μμ΅λλ€.".colorize(:red)
|
|
20
|
+
puts "Docker Desktopμ μ€νν΄μ£ΌμΈμ.".colorize(:cyan)
|
|
21
|
+
exit 1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Kamal Proxy μν νμΈ
|
|
25
|
+
check_kamal_proxy_status
|
|
26
|
+
|
|
27
|
+
# Caddy μν νμΈ
|
|
28
|
+
check_caddy_status
|
|
29
|
+
|
|
30
|
+
puts ""
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def container_exists?(name)
|
|
34
|
+
output = `docker ps -a --filter "name=^#{name}$" --format "{{.Names}}" 2>/dev/null`.strip
|
|
35
|
+
!output.empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def container_running?(name)
|
|
39
|
+
output = `docker ps --filter "name=^#{name}$" --format "{{.Names}}" 2>/dev/null`.strip
|
|
40
|
+
!output.empty?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def check_port_binding(container, ports)
|
|
44
|
+
return false unless container_running?(container)
|
|
45
|
+
|
|
46
|
+
ports.all? do |port|
|
|
47
|
+
output = `docker port #{container} #{port} 2>/dev/null`.strip
|
|
48
|
+
!output.empty?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def port_in_use?(port)
|
|
53
|
+
# Docker 컨ν
μ΄λκ° ν¬νΈλ₯Ό μ¬μ© μ€μΈμ§ νμΈ
|
|
54
|
+
docker_using = `docker ps --format "table {{.Names}}\t{{.Ports}}" | grep -E "0\\.0\\.0\\.0:#{port}->|\\*:#{port}->" 2>/dev/null`.strip
|
|
55
|
+
return true unless docker_using.empty?
|
|
56
|
+
|
|
57
|
+
# μμ€ν
ν¬νΈ νμΈ
|
|
58
|
+
if RUBY_PLATFORM.include?("darwin")
|
|
59
|
+
# macOS
|
|
60
|
+
output = `lsof -iTCP:#{port} -sTCP:LISTEN 2>/dev/null`.strip
|
|
61
|
+
else
|
|
62
|
+
# Linux
|
|
63
|
+
output = `netstat -tln 2>/dev/null | grep ":#{port}"`.strip
|
|
64
|
+
output = `ss -tln 2>/dev/null | grep ":#{port}"`.strip if output.empty?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
!output.empty?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def stop_container(name)
|
|
71
|
+
return unless container_exists?(name)
|
|
72
|
+
|
|
73
|
+
puts "π #{name} 컨ν
μ΄λλ₯Ό μ€μ§ν©λλ€...".colorize(:yellow)
|
|
74
|
+
system("docker stop #{name} >/dev/null 2>&1")
|
|
75
|
+
system("docker rm #{name} >/dev/null 2>&1")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def get_container_network(name)
|
|
79
|
+
return nil unless container_running?(name)
|
|
80
|
+
|
|
81
|
+
output = `docker inspect #{name} --format '{{range .NetworkSettings.Networks}}{{.NetworkID}}{{end}}' 2>/dev/null`.strip
|
|
82
|
+
output.empty? ? nil : output
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def create_network_if_not_exists(network_name = "tayo-proxy")
|
|
86
|
+
existing = `docker network ls --filter "name=^#{network_name}$" --format "{{.Name}}" 2>/dev/null`.strip
|
|
87
|
+
|
|
88
|
+
if existing.empty?
|
|
89
|
+
puts "π‘ Docker λ€νΈμν¬ '#{network_name}'λ₯Ό μμ±ν©λλ€...".colorize(:yellow)
|
|
90
|
+
system("docker network create #{network_name} >/dev/null 2>&1")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
network_name
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def docker_installed?
|
|
99
|
+
system("which docker >/dev/null 2>&1")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def docker_running?
|
|
103
|
+
system("docker info >/dev/null 2>&1")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def check_kamal_proxy_status
|
|
107
|
+
# TraefikμΌλ‘ κ΅μ²΄λμ΄ λ μ΄μ μ¬μ©νμ§ μμ
|
|
108
|
+
check_traefik_status
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def check_traefik_status
|
|
112
|
+
if container_running?("traefik")
|
|
113
|
+
if check_port_binding("traefik", [80, 443])
|
|
114
|
+
puts "β
Traefik: μ€ν μ€ (80, 443 ν¬νΈ μ¬μ©)".colorize(:green)
|
|
115
|
+
else
|
|
116
|
+
puts "β οΈ Traefik: μ€ν μ€μ΄μ§λ§ ν¬νΈκ° μ¬λ°λ₯΄κ² λ°μΈλ©λμ§ μμ".colorize(:yellow)
|
|
117
|
+
show_port_conflicts
|
|
118
|
+
end
|
|
119
|
+
elsif container_exists?("traefik")
|
|
120
|
+
puts "β οΈ Traefik: μ€μ§λ¨".colorize(:yellow)
|
|
121
|
+
else
|
|
122
|
+
puts "βΉοΈ Traefik: μ€μΉλμ§ μμ".colorize(:gray)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def check_caddy_status
|
|
127
|
+
# Caddyλ λ μ΄μ μ¬μ©νμ§ μμ
|
|
128
|
+
# μ΄ λ©μλλ νμ νΈνμ±μ μν΄ μ μ§νμ§λ§ μ무κ²λ νμ§ μμ
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def show_port_conflicts
|
|
132
|
+
[80, 443].each do |port|
|
|
133
|
+
if port_in_use?(port)
|
|
134
|
+
# ν¬νΈλ₯Ό μ¬μ© μ€μΈ νλ‘μΈμ€ μ°ΎκΈ°
|
|
135
|
+
if RUBY_PLATFORM.include?("darwin")
|
|
136
|
+
process = `lsof -iTCP:#{port} -sTCP:LISTEN 2>/dev/null | grep LISTEN | head -1`.strip
|
|
137
|
+
else
|
|
138
|
+
process = `netstat -tlnp 2>/dev/null | grep ":#{port}" | head -1`.strip
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
unless process.empty?
|
|
142
|
+
puts " β οΈ ν¬νΈ #{port}κ° λ€λ₯Έ νλ‘μΈμ€μμ μ¬μ© μ€μ
λλ€:".colorize(:yellow)
|
|
143
|
+
puts " #{process}".colorize(:gray)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "colorize"
|
|
4
|
+
require "tty-prompt"
|
|
5
|
+
|
|
6
|
+
module Tayo
|
|
7
|
+
module Proxy
|
|
8
|
+
class NetworkConfig
|
|
9
|
+
attr_reader :public_ip, :internal_ip, :external_http, :external_https
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@prompt = TTY::Prompt.new
|
|
13
|
+
@use_custom_ports = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def detect_ips
|
|
17
|
+
puts "\nπ λ€νΈμν¬ μ 보λ₯Ό νμΈν©λλ€...".colorize(:yellow)
|
|
18
|
+
|
|
19
|
+
# κ³΅μΈ IP κ°μ§
|
|
20
|
+
print "κ³΅μΈ IP νμΈ μ€... "
|
|
21
|
+
@public_ip = detect_public_ip
|
|
22
|
+
puts "#{@public_ip}".colorize(:green)
|
|
23
|
+
|
|
24
|
+
# λ΄λΆ IP κ°μ§
|
|
25
|
+
print "λ΄λΆ IP νμΈ μ€... "
|
|
26
|
+
@internal_ip = detect_internal_ip
|
|
27
|
+
puts "#{@internal_ip}".colorize(:green)
|
|
28
|
+
|
|
29
|
+
puts ""
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def configure_ports
|
|
33
|
+
puts "\nπ μΈλΆ μ μ ν¬νΈλ₯Ό μ€μ ν©λλ€.".colorize(:yellow)
|
|
34
|
+
puts "Kamal Proxyλ νμ 80, 443 ν¬νΈλ₯Ό μ¬μ©ν©λλ€.".colorize(:gray)
|
|
35
|
+
puts ""
|
|
36
|
+
|
|
37
|
+
choices = [
|
|
38
|
+
{ name: "곡μ κΈ°μμ 80, 443μ μ§μ ν¬μλ© (κΈ°λ³Έ)", value: :direct },
|
|
39
|
+
{ name: "λ€λ₯Έ ν¬νΈλ₯Ό μ¬μ©νμ¬ ν¬μλ© (μ: 8080β80, 8443β443)", value: :custom }
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
choice = @prompt.select("ν¬νΈ μ€μ λ°©μμ μ ννμΈμ:", choices)
|
|
43
|
+
|
|
44
|
+
if choice == :custom
|
|
45
|
+
@use_custom_ports = true
|
|
46
|
+
|
|
47
|
+
@external_http = @prompt.ask("HTTP μΈλΆ ν¬νΈ (κΈ°λ³Έ: 8080):", default: "8080")
|
|
48
|
+
@external_https = @prompt.ask("HTTPS μΈλΆ ν¬νΈ (κΈ°λ³Έ: 8443):", default: "8443")
|
|
49
|
+
|
|
50
|
+
puts "\nβ
μΈλΆ ν¬νΈκ° μ€μ λμμ΅λλ€:".colorize(:green)
|
|
51
|
+
puts " HTTP: #{@external_http}".colorize(:gray)
|
|
52
|
+
puts " HTTPS: #{@external_https}".colorize(:gray)
|
|
53
|
+
else
|
|
54
|
+
@external_http = "80"
|
|
55
|
+
@external_https = "443"
|
|
56
|
+
|
|
57
|
+
puts "\nβ
νμ€ ν¬νΈ(80, 443)λ₯Ό μ¬μ©ν©λλ€.".colorize(:green)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def use_custom_ports?
|
|
62
|
+
@use_custom_ports
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def show_port_forwarding_guide
|
|
66
|
+
return unless use_custom_ports?
|
|
67
|
+
|
|
68
|
+
puts "\nπ‘ 곡μ κΈ° ν¬νΈν¬μλ© μ€μ μλ΄:".colorize(:yellow)
|
|
69
|
+
puts "β" * 50
|
|
70
|
+
puts "μΈλΆ ν¬νΈ #{@external_http} β #{@internal_ip}:80".colorize(:white)
|
|
71
|
+
puts "μΈλΆ ν¬νΈ #{@external_https} β #{@internal_ip}:443".colorize(:white)
|
|
72
|
+
puts "β" * 50
|
|
73
|
+
puts ""
|
|
74
|
+
puts "μ μ€μ μ 곡μ κΈ° κ΄λ¦¬ νμ΄μ§μμ μλ£ν΄μ£ΌμΈμ.".colorize(:cyan)
|
|
75
|
+
puts "μΌλ°μ μΈ μ μ μ£Όμ: http://192.168.1.1".colorize(:gray)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def detect_public_ip
|
|
81
|
+
# curlμ μ¬μ©ν κ³΅μΈ IP κ°μ§
|
|
82
|
+
ip = `curl -s ifconfig.me 2>/dev/null`.strip
|
|
83
|
+
|
|
84
|
+
# λ체 λ°©λ²λ€
|
|
85
|
+
if ip.empty? || !valid_ip?(ip)
|
|
86
|
+
ip = `curl -s ipecho.net/plain 2>/dev/null`.strip
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if ip.empty? || !valid_ip?(ip)
|
|
90
|
+
ip = `curl -s icanhazip.com 2>/dev/null`.strip
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if ip.empty? || !valid_ip?(ip)
|
|
94
|
+
puts "\nβ οΈ κ³΅μΈ IPλ₯Ό μλμΌλ‘ κ°μ§ν μ μμ΅λλ€.".colorize(:yellow)
|
|
95
|
+
ip = @prompt.ask("κ³΅μΈ IPλ₯Ό μ§μ μ
λ ₯ν΄μ£ΌμΈμ:") do |q|
|
|
96
|
+
q.validate(/\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/, "μ¬λ°λ₯Έ IP νμμ μ
λ ₯ν΄μ£ΌμΈμ")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
ip
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def detect_internal_ip
|
|
104
|
+
# macOS
|
|
105
|
+
if RUBY_PLATFORM.include?("darwin")
|
|
106
|
+
# en0 (Wi-Fi) λλ en1 (Ethernet) μΈν°νμ΄μ€μμ IP μΆμΆ
|
|
107
|
+
ip = `ifconfig en0 2>/dev/null | grep 'inet ' | grep -v 127.0.0.1 | awk '{print $2}'`.strip
|
|
108
|
+
ip = `ifconfig en1 2>/dev/null | grep 'inet ' | grep -v 127.0.0.1 | awk '{print $2}'`.strip if ip.empty?
|
|
109
|
+
|
|
110
|
+
# λ€λ₯Έ μΈν°νμ΄μ€ κ²μ
|
|
111
|
+
if ip.empty?
|
|
112
|
+
ip = `ifconfig | grep 'inet ' | grep -v 127.0.0.1 | grep -v '::1' | head -1 | awk '{print $2}'`.strip
|
|
113
|
+
end
|
|
114
|
+
else
|
|
115
|
+
# Linux
|
|
116
|
+
ip = `hostname -I 2>/dev/null | awk '{print $1}'`.strip
|
|
117
|
+
|
|
118
|
+
if ip.empty?
|
|
119
|
+
ip = `ip addr show | grep 'inet ' | grep -v 127.0.0.1 | grep -v '::1' | head -1 | awk '{print $2}' | cut -d/ -f1`.strip
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# μ¬μ ν λΉμ΄μλ€λ©΄ μλ μ
λ ₯
|
|
124
|
+
if ip.empty? || !valid_ip?(ip)
|
|
125
|
+
puts "\nβ οΈ λ΄λΆ IPλ₯Ό μλμΌλ‘ κ°μ§ν μ μμ΅λλ€.".colorize(:yellow)
|
|
126
|
+
ip = @prompt.ask("λ΄λΆ IPλ₯Ό μ§μ μ
λ ₯ν΄μ£ΌμΈμ (μ: 192.168.1.100):") do |q|
|
|
127
|
+
q.validate(/\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/, "μ¬λ°λ₯Έ IP νμμ μ
λ ₯ν΄μ£ΌμΈμ")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
ip
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def valid_ip?(ip)
|
|
135
|
+
return false if ip.nil? || ip.empty?
|
|
136
|
+
|
|
137
|
+
parts = ip.split('.')
|
|
138
|
+
return false unless parts.length == 4
|
|
139
|
+
|
|
140
|
+
parts.all? do |part|
|
|
141
|
+
num = part.to_i
|
|
142
|
+
num >= 0 && num <= 255 && part == num.to_s
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,303 @@
|
|
|
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
|