suthep 0.1.0-beta.1
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.
- package/.editorconfig +17 -0
- package/.prettierignore +6 -0
- package/.prettierrc +7 -0
- package/.vscode/settings.json +19 -0
- package/LICENSE +21 -0
- package/README.md +217 -0
- package/dist/commands/deploy.js +318 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/init.js +188 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/setup.js +90 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/certbot.js +64 -0
- package/dist/utils/certbot.js.map +1 -0
- package/dist/utils/config-loader.js +95 -0
- package/dist/utils/config-loader.js.map +1 -0
- package/dist/utils/deployment.js +76 -0
- package/dist/utils/deployment.js.map +1 -0
- package/dist/utils/docker.js +393 -0
- package/dist/utils/docker.js.map +1 -0
- package/dist/utils/nginx.js +303 -0
- package/dist/utils/nginx.js.map +1 -0
- package/docs/README.md +95 -0
- package/docs/TRANSLATIONS.md +211 -0
- package/docs/en/README.md +76 -0
- package/docs/en/api-reference.md +545 -0
- package/docs/en/architecture.md +369 -0
- package/docs/en/commands.md +273 -0
- package/docs/en/configuration.md +347 -0
- package/docs/en/developer-guide.md +588 -0
- package/docs/en/docker-ports-config.md +333 -0
- package/docs/en/examples.md +537 -0
- package/docs/en/getting-started.md +202 -0
- package/docs/en/port-binding.md +268 -0
- package/docs/en/troubleshooting.md +441 -0
- package/docs/th/README.md +64 -0
- package/docs/th/commands.md +202 -0
- package/docs/th/configuration.md +325 -0
- package/docs/th/getting-started.md +203 -0
- package/example/README.md +85 -0
- package/example/docker-compose.yml +76 -0
- package/example/docker-ports-example.yml +81 -0
- package/example/muacle.yml +47 -0
- package/example/port-binding-example.yml +45 -0
- package/example/suthep.yml +46 -0
- package/example/suthep=1.yml +46 -0
- package/package.json +45 -0
- package/src/commands/deploy.ts +405 -0
- package/src/commands/init.ts +214 -0
- package/src/commands/setup.ts +112 -0
- package/src/index.ts +42 -0
- package/src/types/config.ts +52 -0
- package/src/utils/certbot.ts +144 -0
- package/src/utils/config-loader.ts +121 -0
- package/src/utils/deployment.ts +157 -0
- package/src/utils/docker.ts +755 -0
- package/src/utils/nginx.ts +326 -0
- package/suthep-0.1.1.tgz +0 -0
- package/suthep.example.yml +98 -0
- package/test +0 -0
- package/todo.md +6 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +46 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import fs from 'fs-extra'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import type { ServiceConfig } from '../types/config'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate Nginx server block configuration for a service
|
|
8
|
+
*/
|
|
9
|
+
export function generateNginxConfig(
|
|
10
|
+
service: ServiceConfig,
|
|
11
|
+
withHttps: boolean,
|
|
12
|
+
portOverride?: number
|
|
13
|
+
): string {
|
|
14
|
+
const serverNames = service.domains.join(' ')
|
|
15
|
+
// Use primary domain for upstream naming to ensure uniqueness
|
|
16
|
+
const primaryDomain = service.domains[0]
|
|
17
|
+
const domainSafe = primaryDomain.replace(/\./g, '_').replace(/[^a-zA-Z0-9_]/g, '_')
|
|
18
|
+
const upstreamName = `${domainSafe}_${service.name}`
|
|
19
|
+
const servicePath = service.path || '/'
|
|
20
|
+
const port = portOverride || service.port
|
|
21
|
+
|
|
22
|
+
let config = `# Nginx configuration for ${service.name}\n\n`
|
|
23
|
+
|
|
24
|
+
// Upstream configuration
|
|
25
|
+
config += `upstream ${upstreamName} {\n`
|
|
26
|
+
config += ` server localhost:${port} max_fails=3 fail_timeout=30s;\n`
|
|
27
|
+
config += ` keepalive 32;\n`
|
|
28
|
+
config += `}\n\n`
|
|
29
|
+
|
|
30
|
+
if (withHttps) {
|
|
31
|
+
// HTTP server - redirect to HTTPS
|
|
32
|
+
config += `server {\n`
|
|
33
|
+
config += ` listen 80;\n`
|
|
34
|
+
config += ` listen [::]:80;\n`
|
|
35
|
+
config += ` server_name ${serverNames};\n\n`
|
|
36
|
+
config += ` # Redirect all HTTP to HTTPS\n`
|
|
37
|
+
config += ` return 301 https://$server_name$request_uri;\n`
|
|
38
|
+
config += `}\n\n`
|
|
39
|
+
|
|
40
|
+
// HTTPS server
|
|
41
|
+
config += `server {\n`
|
|
42
|
+
config += ` listen 443 ssl http2;\n`
|
|
43
|
+
config += ` listen [::]:443 ssl http2;\n`
|
|
44
|
+
config += ` server_name ${serverNames};\n\n`
|
|
45
|
+
|
|
46
|
+
// SSL configuration
|
|
47
|
+
const primaryDomain = service.domains[0]
|
|
48
|
+
config += ` # SSL Configuration\n`
|
|
49
|
+
config += ` ssl_certificate /etc/letsencrypt/live/${primaryDomain}/fullchain.pem;\n`
|
|
50
|
+
config += ` ssl_certificate_key /etc/letsencrypt/live/${primaryDomain}/privkey.pem;\n`
|
|
51
|
+
config += ` ssl_protocols TLSv1.2 TLSv1.3;\n`
|
|
52
|
+
config += ` ssl_ciphers HIGH:!aNULL:!MD5;\n`
|
|
53
|
+
config += ` ssl_prefer_server_ciphers on;\n\n`
|
|
54
|
+
} else {
|
|
55
|
+
// HTTP only server
|
|
56
|
+
config += `server {\n`
|
|
57
|
+
config += ` listen 80;\n`
|
|
58
|
+
config += ` listen [::]:80;\n`
|
|
59
|
+
config += ` server_name ${serverNames};\n\n`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Logging
|
|
63
|
+
config += ` # Logging\n`
|
|
64
|
+
config += ` access_log /var/log/nginx/${service.name}_access.log;\n`
|
|
65
|
+
config += ` error_log /var/log/nginx/${service.name}_error.log;\n\n`
|
|
66
|
+
|
|
67
|
+
// Client settings
|
|
68
|
+
config += ` # Client settings\n`
|
|
69
|
+
config += ` client_max_body_size 100M;\n\n`
|
|
70
|
+
|
|
71
|
+
// Proxy settings
|
|
72
|
+
config += ` # Proxy settings\n`
|
|
73
|
+
config += ` location ${servicePath} {\n`
|
|
74
|
+
config += ` proxy_pass http://${upstreamName};\n`
|
|
75
|
+
config += ` proxy_http_version 1.1;\n`
|
|
76
|
+
config += ` proxy_set_header Upgrade $http_upgrade;\n`
|
|
77
|
+
config += ` proxy_set_header Connection 'upgrade';\n`
|
|
78
|
+
config += ` proxy_set_header Host $host;\n`
|
|
79
|
+
config += ` proxy_set_header X-Real-IP $remote_addr;\n`
|
|
80
|
+
config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n`
|
|
81
|
+
config += ` proxy_set_header X-Forwarded-Proto $scheme;\n`
|
|
82
|
+
config += ` proxy_cache_bypass $http_upgrade;\n`
|
|
83
|
+
config += ` proxy_connect_timeout 60s;\n`
|
|
84
|
+
config += ` proxy_send_timeout 60s;\n`
|
|
85
|
+
config += ` proxy_read_timeout 60s;\n`
|
|
86
|
+
config += ` }\n`
|
|
87
|
+
|
|
88
|
+
// Health check endpoint (if configured)
|
|
89
|
+
if (service.healthCheck) {
|
|
90
|
+
config += `\n # Health check endpoint\n`
|
|
91
|
+
config += ` location ${service.healthCheck.path} {\n`
|
|
92
|
+
config += ` proxy_pass http://${upstreamName};\n`
|
|
93
|
+
config += ` access_log off;\n`
|
|
94
|
+
config += ` }\n`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
config += `}\n`
|
|
98
|
+
|
|
99
|
+
return config
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate Nginx configuration for multiple services on the same domain
|
|
104
|
+
* Groups services by domain and creates location blocks for each service path
|
|
105
|
+
*/
|
|
106
|
+
export function generateMultiServiceNginxConfig(
|
|
107
|
+
services: ServiceConfig[],
|
|
108
|
+
domain: string,
|
|
109
|
+
withHttps: boolean,
|
|
110
|
+
portOverrides?: Map<string, number>
|
|
111
|
+
): string {
|
|
112
|
+
const upstreams: string[] = []
|
|
113
|
+
const locations: string[] = []
|
|
114
|
+
const healthChecks: string[] = []
|
|
115
|
+
|
|
116
|
+
// Sort services by path length (longest first) to ensure specific paths are matched before general ones
|
|
117
|
+
const sortedServices = [...services].sort((a, b) => {
|
|
118
|
+
const pathA = (a.path || '/').length
|
|
119
|
+
const pathB = (b.path || '/').length
|
|
120
|
+
return pathB - pathA
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Track upstream names to ensure uniqueness within the same domain config
|
|
124
|
+
const usedUpstreamNames = new Set<string>()
|
|
125
|
+
|
|
126
|
+
for (const service of sortedServices) {
|
|
127
|
+
// Create unique upstream name: domain_service_name to avoid conflicts
|
|
128
|
+
// Replace dots and special chars in domain for valid nginx upstream name
|
|
129
|
+
const domainSafe = domain.replace(/\./g, '_').replace(/[^a-zA-Z0-9_]/g, '_')
|
|
130
|
+
let upstreamName = `${domainSafe}_${service.name}`
|
|
131
|
+
|
|
132
|
+
// Ensure uniqueness (in case same service name appears multiple times)
|
|
133
|
+
let counter = 1
|
|
134
|
+
const originalUpstreamName = upstreamName
|
|
135
|
+
while (usedUpstreamNames.has(upstreamName)) {
|
|
136
|
+
upstreamName = `${originalUpstreamName}_${counter}`
|
|
137
|
+
counter++
|
|
138
|
+
}
|
|
139
|
+
usedUpstreamNames.add(upstreamName)
|
|
140
|
+
|
|
141
|
+
const servicePath = service.path || '/'
|
|
142
|
+
const port = portOverrides?.get(service.name) || service.port
|
|
143
|
+
|
|
144
|
+
// Generate upstream for each service on the same domain
|
|
145
|
+
upstreams.push(`upstream ${upstreamName} {`)
|
|
146
|
+
upstreams.push(` server localhost:${port} max_fails=3 fail_timeout=30s;`)
|
|
147
|
+
upstreams.push(` keepalive 32;`)
|
|
148
|
+
upstreams.push(`}`)
|
|
149
|
+
|
|
150
|
+
// Generate location block
|
|
151
|
+
if (servicePath === '/') {
|
|
152
|
+
// Root path - use exact match or default
|
|
153
|
+
locations.push(` # Service: ${service.name}`)
|
|
154
|
+
locations.push(` location / {`)
|
|
155
|
+
} else {
|
|
156
|
+
// Specific path - use prefix match
|
|
157
|
+
locations.push(` # Service: ${service.name}`)
|
|
158
|
+
locations.push(` location ${servicePath} {`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
locations.push(` proxy_pass http://${upstreamName};`)
|
|
162
|
+
locations.push(` proxy_http_version 1.1;`)
|
|
163
|
+
locations.push(` proxy_set_header Upgrade $http_upgrade;`)
|
|
164
|
+
locations.push(` proxy_set_header Connection 'upgrade';`)
|
|
165
|
+
locations.push(` proxy_set_header Host $host;`)
|
|
166
|
+
locations.push(` proxy_set_header X-Real-IP $remote_addr;`)
|
|
167
|
+
locations.push(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`)
|
|
168
|
+
locations.push(` proxy_set_header X-Forwarded-Proto $scheme;`)
|
|
169
|
+
locations.push(` proxy_cache_bypass $http_upgrade;`)
|
|
170
|
+
locations.push(` proxy_connect_timeout 60s;`)
|
|
171
|
+
locations.push(` proxy_send_timeout 60s;`)
|
|
172
|
+
locations.push(` proxy_read_timeout 60s;`)
|
|
173
|
+
locations.push(` }`)
|
|
174
|
+
|
|
175
|
+
// Health check endpoint
|
|
176
|
+
if (service.healthCheck) {
|
|
177
|
+
healthChecks.push(` # Health check for ${service.name}`)
|
|
178
|
+
healthChecks.push(` location ${service.healthCheck.path} {`)
|
|
179
|
+
healthChecks.push(` proxy_pass http://${upstreamName};`)
|
|
180
|
+
healthChecks.push(` access_log off;`)
|
|
181
|
+
healthChecks.push(` }`)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let config = `# Nginx configuration for ${domain}\n`
|
|
186
|
+
config += `# Multiple services on the same domain\n\n`
|
|
187
|
+
|
|
188
|
+
// Add upstreams
|
|
189
|
+
config += upstreams.join('\n') + '\n\n'
|
|
190
|
+
|
|
191
|
+
if (withHttps) {
|
|
192
|
+
// HTTP server - redirect to HTTPS
|
|
193
|
+
config += `server {\n`
|
|
194
|
+
config += ` listen 80;\n`
|
|
195
|
+
config += ` listen [::]:80;\n`
|
|
196
|
+
config += ` server_name ${domain};\n\n`
|
|
197
|
+
config += ` # Redirect all HTTP to HTTPS\n`
|
|
198
|
+
config += ` return 301 https://$server_name$request_uri;\n`
|
|
199
|
+
config += `}\n\n`
|
|
200
|
+
|
|
201
|
+
// HTTPS server
|
|
202
|
+
config += `server {\n`
|
|
203
|
+
config += ` listen 443 ssl http2;\n`
|
|
204
|
+
config += ` listen [::]:443 ssl http2;\n`
|
|
205
|
+
config += ` server_name ${domain};\n\n`
|
|
206
|
+
|
|
207
|
+
// SSL configuration
|
|
208
|
+
config += ` # SSL Configuration\n`
|
|
209
|
+
config += ` ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;\n`
|
|
210
|
+
config += ` ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;\n`
|
|
211
|
+
config += ` ssl_protocols TLSv1.2 TLSv1.3;\n`
|
|
212
|
+
config += ` ssl_ciphers HIGH:!aNULL:!MD5;\n`
|
|
213
|
+
config += ` ssl_prefer_server_ciphers on;\n\n`
|
|
214
|
+
} else {
|
|
215
|
+
// HTTP only server
|
|
216
|
+
config += `server {\n`
|
|
217
|
+
config += ` listen 80;\n`
|
|
218
|
+
config += ` listen [::]:80;\n`
|
|
219
|
+
config += ` server_name ${domain};\n\n`
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Logging
|
|
223
|
+
config += ` # Logging\n`
|
|
224
|
+
config += ` access_log /var/log/nginx/${domain}_access.log;\n`
|
|
225
|
+
config += ` error_log /var/log/nginx/${domain}_error.log;\n\n`
|
|
226
|
+
|
|
227
|
+
// Client settings
|
|
228
|
+
config += ` # Client settings\n`
|
|
229
|
+
config += ` client_max_body_size 100M;\n\n`
|
|
230
|
+
|
|
231
|
+
// Location blocks
|
|
232
|
+
config += ` # Service locations\n`
|
|
233
|
+
config += locations.join('\n') + '\n\n'
|
|
234
|
+
|
|
235
|
+
// Health check endpoints
|
|
236
|
+
if (healthChecks.length > 0) {
|
|
237
|
+
config += ` # Health check endpoints\n`
|
|
238
|
+
config += healthChecks.join('\n') + '\n'
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
config += `}\n`
|
|
242
|
+
|
|
243
|
+
return config
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check if an Nginx config file exists
|
|
248
|
+
*/
|
|
249
|
+
export async function configExists(configName: string, configPath: string): Promise<boolean> {
|
|
250
|
+
const configFilePath = path.join(configPath, `${configName}.conf`)
|
|
251
|
+
return await fs.pathExists(configFilePath)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Write Nginx configuration file, deleting existing file if it exists and creating new one
|
|
256
|
+
*/
|
|
257
|
+
export async function writeNginxConfig(
|
|
258
|
+
configName: string,
|
|
259
|
+
configPath: string,
|
|
260
|
+
configContent: string
|
|
261
|
+
): Promise<boolean> {
|
|
262
|
+
const configFilePath = path.join(configPath, `${configName}.conf`)
|
|
263
|
+
const exists = await fs.pathExists(configFilePath)
|
|
264
|
+
|
|
265
|
+
// If config file exists, delete it first
|
|
266
|
+
if (exists) {
|
|
267
|
+
await fs.remove(configFilePath)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Create new config file
|
|
271
|
+
await fs.writeFile(configFilePath, configContent)
|
|
272
|
+
|
|
273
|
+
return exists // Return true if file existed (was deleted and recreated)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Enable an Nginx site by creating a symbolic link
|
|
278
|
+
*/
|
|
279
|
+
export async function enableSite(siteName: string, configPath: string): Promise<void> {
|
|
280
|
+
const availablePath = path.join(configPath, `${siteName}.conf`)
|
|
281
|
+
const enabledPath = availablePath.replace('sites-available', 'sites-enabled')
|
|
282
|
+
|
|
283
|
+
// Create sites-enabled directory if it doesn't exist
|
|
284
|
+
await fs.ensureDir(path.dirname(enabledPath))
|
|
285
|
+
|
|
286
|
+
// Remove existing symlink if present
|
|
287
|
+
if (await fs.pathExists(enabledPath)) {
|
|
288
|
+
await fs.remove(enabledPath)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Create symlink
|
|
292
|
+
await execa('sudo', ['ln', '-sf', availablePath, enabledPath])
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Test and reload Nginx configuration
|
|
297
|
+
*/
|
|
298
|
+
export async function reloadNginx(reloadCommand: string): Promise<void> {
|
|
299
|
+
try {
|
|
300
|
+
// Test configuration first
|
|
301
|
+
await execa('sudo', ['nginx', '-t'])
|
|
302
|
+
|
|
303
|
+
// Reload Nginx
|
|
304
|
+
const parts = reloadCommand.split(' ')
|
|
305
|
+
if (parts.length > 0) {
|
|
306
|
+
// Simple execution of provided command
|
|
307
|
+
await execa(parts[0], parts.slice(1), { shell: true })
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
throw new Error(`Failed to reload Nginx: ${error instanceof Error ? error.message : error}`)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Disable an Nginx site
|
|
316
|
+
*/
|
|
317
|
+
export async function disableSite(siteName: string, configPath: string): Promise<void> {
|
|
318
|
+
const enabledPath = path.join(
|
|
319
|
+
configPath.replace('sites-available', 'sites-enabled'),
|
|
320
|
+
`${siteName}.conf`
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if (await fs.pathExists(enabledPath)) {
|
|
324
|
+
await fs.remove(enabledPath)
|
|
325
|
+
}
|
|
326
|
+
}
|
package/suthep-0.1.1.tgz
ADDED
|
Binary file
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
project:
|
|
2
|
+
name: my-app
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
|
|
5
|
+
services:
|
|
6
|
+
# Example 1: Simple Node.js service
|
|
7
|
+
- name: api
|
|
8
|
+
port: 3000
|
|
9
|
+
domains:
|
|
10
|
+
- api.example.com
|
|
11
|
+
- www.api.example.com
|
|
12
|
+
healthCheck:
|
|
13
|
+
path: /health
|
|
14
|
+
interval: 30
|
|
15
|
+
environment:
|
|
16
|
+
NODE_ENV: production
|
|
17
|
+
PORT: 3000
|
|
18
|
+
|
|
19
|
+
# Example 2: Docker container service
|
|
20
|
+
- name: webapp
|
|
21
|
+
port: 8080
|
|
22
|
+
docker:
|
|
23
|
+
image: nginx:latest
|
|
24
|
+
container: webapp-container
|
|
25
|
+
port: 80
|
|
26
|
+
domains:
|
|
27
|
+
- example.com
|
|
28
|
+
- www.example.com
|
|
29
|
+
healthCheck:
|
|
30
|
+
path: /
|
|
31
|
+
interval: 30
|
|
32
|
+
|
|
33
|
+
# Example 3: Multiple subdomains with Docker
|
|
34
|
+
- name: dashboard
|
|
35
|
+
port: 5000
|
|
36
|
+
docker:
|
|
37
|
+
image: myapp/dashboard:latest
|
|
38
|
+
container: dashboard-container
|
|
39
|
+
port: 5000
|
|
40
|
+
domains:
|
|
41
|
+
- dashboard.example.com
|
|
42
|
+
- admin.example.com
|
|
43
|
+
healthCheck:
|
|
44
|
+
path: /api/health
|
|
45
|
+
interval: 60
|
|
46
|
+
environment:
|
|
47
|
+
DATABASE_URL: postgresql://localhost:5432/dashboard
|
|
48
|
+
REDIS_URL: redis://localhost:6379
|
|
49
|
+
|
|
50
|
+
# Example 4: Service connecting to existing Docker container
|
|
51
|
+
- name: database-proxy
|
|
52
|
+
port: 5432
|
|
53
|
+
docker:
|
|
54
|
+
container: postgres-container
|
|
55
|
+
port: 5432
|
|
56
|
+
domains:
|
|
57
|
+
- db.example.com
|
|
58
|
+
|
|
59
|
+
# Example 5: Multiple services on the same domain with path-based routing
|
|
60
|
+
# API service on /api path
|
|
61
|
+
- name: api
|
|
62
|
+
port: 3001
|
|
63
|
+
path: /api
|
|
64
|
+
domains:
|
|
65
|
+
- muacle.com
|
|
66
|
+
docker:
|
|
67
|
+
image: myapp/api:latest
|
|
68
|
+
container: api-container
|
|
69
|
+
port: 3001
|
|
70
|
+
healthCheck:
|
|
71
|
+
path: /health
|
|
72
|
+
interval: 30
|
|
73
|
+
|
|
74
|
+
# UI service on root path
|
|
75
|
+
- name: ui
|
|
76
|
+
port: 3000
|
|
77
|
+
path: /
|
|
78
|
+
domains:
|
|
79
|
+
- muacle.com
|
|
80
|
+
docker:
|
|
81
|
+
image: myapp/ui:latest
|
|
82
|
+
container: ui-container
|
|
83
|
+
port: 3000
|
|
84
|
+
healthCheck:
|
|
85
|
+
path: /
|
|
86
|
+
interval: 30
|
|
87
|
+
|
|
88
|
+
nginx:
|
|
89
|
+
configPath: /etc/nginx/sites-available
|
|
90
|
+
reloadCommand: sudo nginx -t && sudo systemctl reload nginx
|
|
91
|
+
|
|
92
|
+
certbot:
|
|
93
|
+
email: admin@example.com
|
|
94
|
+
staging: false # Set to true for testing
|
|
95
|
+
|
|
96
|
+
deployment:
|
|
97
|
+
strategy: rolling # Options: rolling, blue-green
|
|
98
|
+
healthCheckTimeout: 30000 # milliseconds
|
package/test
ADDED
|
File without changes
|
package/todo.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
build tool deploy and run project in yml service config service port to nginx multiple domain name or sub domain and can connect docker port
|
|
2
|
+
✔ Automatic Nginx reverse proxy setup
|
|
3
|
+
✔ Automatic HTTPS with Certbot
|
|
4
|
+
✔ Zero-downtime deploy
|
|
5
|
+
make it in cli https://www.npmjs.com/package/commander typescript to build create project and setup by yml nginx
|
|
6
|
+
in save cost in run vm
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
7
|
+
"types": ["vite/client"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["src"]
|
|
26
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { resolve } from 'path'
|
|
2
|
+
import type { Plugin } from 'vite'
|
|
3
|
+
import { defineConfig } from 'vite'
|
|
4
|
+
|
|
5
|
+
const addShebangPlugin = (): Plugin => ({
|
|
6
|
+
name: 'add-shebang',
|
|
7
|
+
generateBundle(_options, bundle) {
|
|
8
|
+
if (bundle['index.js']) {
|
|
9
|
+
const chunk = bundle['index.js']
|
|
10
|
+
if (chunk.type === 'chunk') {
|
|
11
|
+
chunk.code = '#!/usr/bin/env node\n' + chunk.code
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export default defineConfig({
|
|
18
|
+
plugins: [addShebangPlugin()],
|
|
19
|
+
build: {
|
|
20
|
+
outDir: 'dist',
|
|
21
|
+
lib: {
|
|
22
|
+
entry: resolve(__dirname, 'src/index.ts'),
|
|
23
|
+
formats: ['es'],
|
|
24
|
+
fileName: 'index',
|
|
25
|
+
},
|
|
26
|
+
rollupOptions: {
|
|
27
|
+
external: (id) => {
|
|
28
|
+
// Externalize all node_modules and Node.js built-ins
|
|
29
|
+
return (
|
|
30
|
+
!id.startsWith('.') &&
|
|
31
|
+
!id.startsWith('/') &&
|
|
32
|
+
!resolve(__dirname, id).startsWith(__dirname + '/src')
|
|
33
|
+
)
|
|
34
|
+
},
|
|
35
|
+
output: {
|
|
36
|
+
format: 'es',
|
|
37
|
+
entryFileNames: '[name].js',
|
|
38
|
+
preserveModules: true,
|
|
39
|
+
preserveModulesRoot: 'src',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
target: 'node18',
|
|
43
|
+
minify: false,
|
|
44
|
+
sourcemap: true,
|
|
45
|
+
},
|
|
46
|
+
})
|