suthep 0.1.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.
- 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 +214 -0
- package/dist/commands/deploy.js +104 -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 +12 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/certbot.js +27 -0
- package/dist/utils/certbot.js.map +1 -0
- package/dist/utils/config-loader.js +65 -0
- package/dist/utils/config-loader.js.map +1 -0
- package/dist/utils/deployment.js +52 -0
- package/dist/utils/deployment.js.map +1 -0
- package/dist/utils/docker.js +57 -0
- package/dist/utils/docker.js.map +1 -0
- package/dist/utils/nginx.js +154 -0
- package/dist/utils/nginx.js.map +1 -0
- package/docs/README.md +62 -0
- package/docs/api-reference.md +545 -0
- package/docs/architecture.md +367 -0
- package/docs/commands.md +273 -0
- package/docs/configuration.md +347 -0
- package/docs/examples.md +537 -0
- package/docs/getting-started.md +197 -0
- package/docs/troubleshooting.md +441 -0
- package/example/README.md +81 -0
- package/example/docker-compose.yml +72 -0
- package/example/suthep.yml +31 -0
- package/package.json +45 -0
- package/src/commands/deploy.ts +133 -0
- package/src/commands/init.ts +214 -0
- package/src/commands/setup.ts +112 -0
- package/src/index.ts +34 -0
- package/src/types/config.ts +51 -0
- package/src/utils/certbot.ts +82 -0
- package/src/utils/config-loader.ts +81 -0
- package/src/utils/deployment.ts +132 -0
- package/src/utils/docker.ts +151 -0
- package/src/utils/nginx.ts +143 -0
- package/suthep.example.yml +69 -0
- package/todo.md +6 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +46 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'fs-extra'
|
|
2
|
+
import yaml from 'js-yaml'
|
|
3
|
+
import type { DeployConfig } from '../types/config'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load and parse a YAML configuration file
|
|
7
|
+
*/
|
|
8
|
+
export async function loadConfig(filePath: string): Promise<DeployConfig> {
|
|
9
|
+
try {
|
|
10
|
+
const fileContent = await fs.readFile(filePath, 'utf8')
|
|
11
|
+
const config = yaml.load(fileContent) as DeployConfig
|
|
12
|
+
|
|
13
|
+
validateConfig(config)
|
|
14
|
+
|
|
15
|
+
return config
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error instanceof Error) {
|
|
18
|
+
throw new Error(`Failed to load configuration from ${filePath}: ${error.message}`)
|
|
19
|
+
}
|
|
20
|
+
throw error
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate the configuration object
|
|
26
|
+
*/
|
|
27
|
+
function validateConfig(config: any): asserts config is DeployConfig {
|
|
28
|
+
if (!config.project || !config.project.name) {
|
|
29
|
+
throw new Error('Configuration must include project.name')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!config.services || !Array.isArray(config.services) || config.services.length === 0) {
|
|
33
|
+
throw new Error('Configuration must include at least one service')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const service of config.services) {
|
|
37
|
+
if (!service.name) {
|
|
38
|
+
throw new Error('Each service must have a name')
|
|
39
|
+
}
|
|
40
|
+
if (!service.port) {
|
|
41
|
+
throw new Error(`Service ${service.name} must have a port`)
|
|
42
|
+
}
|
|
43
|
+
if (!service.domains || !Array.isArray(service.domains) || service.domains.length === 0) {
|
|
44
|
+
throw new Error(`Service ${service.name} must have at least one domain`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!config.nginx) {
|
|
49
|
+
config.nginx = {
|
|
50
|
+
configPath: '/etc/nginx/sites-available',
|
|
51
|
+
reloadCommand: 'sudo nginx -t && sudo systemctl reload nginx',
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!config.certbot) {
|
|
56
|
+
config.certbot = {
|
|
57
|
+
email: '',
|
|
58
|
+
staging: false,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!config.deployment) {
|
|
63
|
+
config.deployment = {
|
|
64
|
+
strategy: 'rolling',
|
|
65
|
+
healthCheckTimeout: 30000,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Save configuration to a YAML file
|
|
72
|
+
*/
|
|
73
|
+
export async function saveConfig(filePath: string, config: DeployConfig): Promise<void> {
|
|
74
|
+
const yamlContent = yaml.dump(config, {
|
|
75
|
+
indent: 2,
|
|
76
|
+
lineWidth: 120,
|
|
77
|
+
noRefs: true,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
await fs.writeFile(filePath, yamlContent, 'utf8')
|
|
81
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { DeploymentConfig, ServiceConfig } from '../types/config'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Perform a health check on a service endpoint
|
|
5
|
+
*/
|
|
6
|
+
export async function performHealthCheck(url: string, timeout: number = 30000): Promise<boolean> {
|
|
7
|
+
const startTime = Date.now()
|
|
8
|
+
const interval = 2000 // Check every 2 seconds
|
|
9
|
+
|
|
10
|
+
while (Date.now() - startTime < timeout) {
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch(url, {
|
|
13
|
+
method: 'GET',
|
|
14
|
+
signal: AbortSignal.timeout(5000), // 5 second timeout per request
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
if (response.ok) {
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
} catch (error) {
|
|
21
|
+
// Endpoint not ready yet, continue waiting
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Wait before next check
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, interval))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Deploy a service with zero-downtime strategy
|
|
33
|
+
*/
|
|
34
|
+
export async function deployService(
|
|
35
|
+
service: ServiceConfig,
|
|
36
|
+
deploymentConfig: DeploymentConfig
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
if (deploymentConfig.strategy === 'rolling') {
|
|
39
|
+
await rollingDeploy(service, deploymentConfig)
|
|
40
|
+
} else if (deploymentConfig.strategy === 'blue-green') {
|
|
41
|
+
await blueGreenDeploy(service, deploymentConfig)
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error(`Unknown deployment strategy: ${deploymentConfig.strategy}`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Rolling deployment strategy
|
|
49
|
+
* Gradually replaces old instances with new ones
|
|
50
|
+
*/
|
|
51
|
+
async function rollingDeploy(
|
|
52
|
+
service: ServiceConfig,
|
|
53
|
+
deploymentConfig: DeploymentConfig
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
// For rolling deployment:
|
|
56
|
+
// 1. Start new service instance
|
|
57
|
+
// 2. Wait for health check
|
|
58
|
+
// 3. Switch traffic to new instance
|
|
59
|
+
// 4. Stop old instance (if applicable)
|
|
60
|
+
|
|
61
|
+
// In this implementation, we assume the service is already running
|
|
62
|
+
// and we're just verifying it's healthy before proceeding
|
|
63
|
+
|
|
64
|
+
if (service.healthCheck) {
|
|
65
|
+
const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`
|
|
66
|
+
const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)
|
|
67
|
+
|
|
68
|
+
if (!isHealthy) {
|
|
69
|
+
throw new Error(`Service ${service.name} failed health check during rolling deployment`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Add a small delay to ensure service is fully ready
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Blue-green deployment strategy
|
|
79
|
+
* Maintains two identical environments and switches between them
|
|
80
|
+
*/
|
|
81
|
+
async function blueGreenDeploy(
|
|
82
|
+
service: ServiceConfig,
|
|
83
|
+
deploymentConfig: DeploymentConfig
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
// For blue-green deployment:
|
|
86
|
+
// 1. Deploy to "green" environment
|
|
87
|
+
// 2. Run health checks on green
|
|
88
|
+
// 3. Switch router/load balancer to green
|
|
89
|
+
// 4. Keep blue as backup
|
|
90
|
+
|
|
91
|
+
// This is a simplified implementation
|
|
92
|
+
// In production, you'd manage multiple service instances
|
|
93
|
+
|
|
94
|
+
if (service.healthCheck) {
|
|
95
|
+
const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`
|
|
96
|
+
const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)
|
|
97
|
+
|
|
98
|
+
if (!isHealthy) {
|
|
99
|
+
throw new Error(`Service ${service.name} failed health check during blue-green deployment`)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Wait for a service to become healthy
|
|
106
|
+
*/
|
|
107
|
+
export async function waitForService(
|
|
108
|
+
service: ServiceConfig,
|
|
109
|
+
timeout: number = 60000
|
|
110
|
+
): Promise<boolean> {
|
|
111
|
+
if (!service.healthCheck) {
|
|
112
|
+
// No health check configured, assume service is ready after a short delay
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, 5000))
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`
|
|
118
|
+
return await performHealthCheck(healthUrl, timeout)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Gracefully shutdown a service
|
|
123
|
+
*/
|
|
124
|
+
export async function gracefulShutdown(
|
|
125
|
+
_service: ServiceConfig,
|
|
126
|
+
timeout: number = 30000
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
// Send shutdown signal and wait for graceful termination
|
|
129
|
+
// This is a placeholder - actual implementation would depend on how services are managed
|
|
130
|
+
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(timeout, 5000)))
|
|
132
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import type { ServiceConfig } from '../types/config'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Start or connect to a Docker container for a service
|
|
6
|
+
*/
|
|
7
|
+
export async function startDockerContainer(service: ServiceConfig): Promise<void> {
|
|
8
|
+
if (!service.docker) {
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { image, container, port } = service.docker
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Check if container exists
|
|
16
|
+
const { stdout: existingContainers } = await execa('docker', [
|
|
17
|
+
'ps',
|
|
18
|
+
'-a',
|
|
19
|
+
'--filter',
|
|
20
|
+
`name=${container}`,
|
|
21
|
+
'--format',
|
|
22
|
+
'{{.Names}}',
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
if (existingContainers.includes(container)) {
|
|
26
|
+
// Container exists, check if it's running
|
|
27
|
+
const { stdout: runningContainers } = await execa('docker', [
|
|
28
|
+
'ps',
|
|
29
|
+
'--filter',
|
|
30
|
+
`name=${container}`,
|
|
31
|
+
'--format',
|
|
32
|
+
'{{.Names}}',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
if (!runningContainers.includes(container)) {
|
|
36
|
+
// Container exists but not running, start it
|
|
37
|
+
await execa('docker', ['start', container])
|
|
38
|
+
}
|
|
39
|
+
} else if (image) {
|
|
40
|
+
// Container doesn't exist and image is provided, create and run it
|
|
41
|
+
const args = [
|
|
42
|
+
'run',
|
|
43
|
+
'-d',
|
|
44
|
+
'--name',
|
|
45
|
+
container,
|
|
46
|
+
'-p',
|
|
47
|
+
`${service.port}:${port}`,
|
|
48
|
+
'--restart',
|
|
49
|
+
'unless-stopped',
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
// Add environment variables if configured
|
|
53
|
+
if (service.environment) {
|
|
54
|
+
for (const [key, value] of Object.entries(service.environment)) {
|
|
55
|
+
args.push('-e', `${key}=${value}`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
args.push(image)
|
|
60
|
+
|
|
61
|
+
await execa('docker', args)
|
|
62
|
+
} else {
|
|
63
|
+
throw new Error(`Container ${container} not found and no image specified`)
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Failed to start Docker container: ${error instanceof Error ? error.message : error}`
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Stop a Docker container
|
|
74
|
+
*/
|
|
75
|
+
export async function stopDockerContainer(containerName: string): Promise<void> {
|
|
76
|
+
try {
|
|
77
|
+
await execa('docker', ['stop', containerName])
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Failed to stop container ${containerName}: ${error instanceof Error ? error.message : error}`
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Remove a Docker container
|
|
87
|
+
*/
|
|
88
|
+
export async function removeDockerContainer(containerName: string): Promise<void> {
|
|
89
|
+
try {
|
|
90
|
+
await execa('docker', ['rm', '-f', containerName])
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Failed to remove container ${containerName}: ${
|
|
94
|
+
error instanceof Error ? error.message : error
|
|
95
|
+
}`
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if a Docker container is running
|
|
102
|
+
*/
|
|
103
|
+
export async function isContainerRunning(containerName: string): Promise<boolean> {
|
|
104
|
+
try {
|
|
105
|
+
const { stdout } = await execa('docker', [
|
|
106
|
+
'ps',
|
|
107
|
+
'--filter',
|
|
108
|
+
`name=${containerName}`,
|
|
109
|
+
'--format',
|
|
110
|
+
'{{.Names}}',
|
|
111
|
+
])
|
|
112
|
+
return stdout.includes(containerName)
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get container logs
|
|
120
|
+
*/
|
|
121
|
+
export async function getContainerLogs(
|
|
122
|
+
containerName: string,
|
|
123
|
+
lines: number = 100
|
|
124
|
+
): Promise<string> {
|
|
125
|
+
try {
|
|
126
|
+
const { stdout } = await execa('docker', ['logs', '--tail', lines.toString(), containerName])
|
|
127
|
+
return stdout
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Failed to get logs for container ${containerName}: ${
|
|
131
|
+
error instanceof Error ? error.message : error
|
|
132
|
+
}`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Inspect a Docker container
|
|
139
|
+
*/
|
|
140
|
+
export async function inspectContainer(containerName: string): Promise<any> {
|
|
141
|
+
try {
|
|
142
|
+
const { stdout } = await execa('docker', ['inspect', containerName])
|
|
143
|
+
return JSON.parse(stdout)[0]
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Failed to inspect container ${containerName}: ${
|
|
147
|
+
error instanceof Error ? error.message : error
|
|
148
|
+
}`
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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(service: ServiceConfig, withHttps: boolean): string {
|
|
10
|
+
const serverNames = service.domains.join(' ')
|
|
11
|
+
const upstreamName = `${service.name}_backend`
|
|
12
|
+
|
|
13
|
+
let config = `# Nginx configuration for ${service.name}\n\n`
|
|
14
|
+
|
|
15
|
+
// Upstream configuration
|
|
16
|
+
config += `upstream ${upstreamName} {\n`
|
|
17
|
+
config += ` server localhost:${service.port} max_fails=3 fail_timeout=30s;\n`
|
|
18
|
+
config += ` keepalive 32;\n`
|
|
19
|
+
config += `}\n\n`
|
|
20
|
+
|
|
21
|
+
if (withHttps) {
|
|
22
|
+
// HTTP server - redirect to HTTPS
|
|
23
|
+
config += `server {\n`
|
|
24
|
+
config += ` listen 80;\n`
|
|
25
|
+
config += ` listen [::]:80;\n`
|
|
26
|
+
config += ` server_name ${serverNames};\n\n`
|
|
27
|
+
config += ` # Redirect all HTTP to HTTPS\n`
|
|
28
|
+
config += ` return 301 https://$server_name$request_uri;\n`
|
|
29
|
+
config += `}\n\n`
|
|
30
|
+
|
|
31
|
+
// HTTPS server
|
|
32
|
+
config += `server {\n`
|
|
33
|
+
config += ` listen 443 ssl http2;\n`
|
|
34
|
+
config += ` listen [::]:443 ssl http2;\n`
|
|
35
|
+
config += ` server_name ${serverNames};\n\n`
|
|
36
|
+
|
|
37
|
+
// SSL configuration
|
|
38
|
+
const primaryDomain = service.domains[0]
|
|
39
|
+
config += ` # SSL Configuration\n`
|
|
40
|
+
config += ` ssl_certificate /etc/letsencrypt/live/${primaryDomain}/fullchain.pem;\n`
|
|
41
|
+
config += ` ssl_certificate_key /etc/letsencrypt/live/${primaryDomain}/privkey.pem;\n`
|
|
42
|
+
config += ` ssl_protocols TLSv1.2 TLSv1.3;\n`
|
|
43
|
+
config += ` ssl_ciphers HIGH:!aNULL:!MD5;\n`
|
|
44
|
+
config += ` ssl_prefer_server_ciphers on;\n\n`
|
|
45
|
+
} else {
|
|
46
|
+
// HTTP only server
|
|
47
|
+
config += `server {\n`
|
|
48
|
+
config += ` listen 80;\n`
|
|
49
|
+
config += ` listen [::]:80;\n`
|
|
50
|
+
config += ` server_name ${serverNames};\n\n`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Logging
|
|
54
|
+
config += ` # Logging\n`
|
|
55
|
+
config += ` access_log /var/log/nginx/${service.name}_access.log;\n`
|
|
56
|
+
config += ` error_log /var/log/nginx/${service.name}_error.log;\n\n`
|
|
57
|
+
|
|
58
|
+
// Client settings
|
|
59
|
+
config += ` # Client settings\n`
|
|
60
|
+
config += ` client_max_body_size 100M;\n\n`
|
|
61
|
+
|
|
62
|
+
// Proxy settings
|
|
63
|
+
config += ` # Proxy settings\n`
|
|
64
|
+
config += ` location / {\n`
|
|
65
|
+
config += ` proxy_pass http://${upstreamName};\n`
|
|
66
|
+
config += ` proxy_http_version 1.1;\n`
|
|
67
|
+
config += ` proxy_set_header Upgrade $http_upgrade;\n`
|
|
68
|
+
config += ` proxy_set_header Connection 'upgrade';\n`
|
|
69
|
+
config += ` proxy_set_header Host $host;\n`
|
|
70
|
+
config += ` proxy_set_header X-Real-IP $remote_addr;\n`
|
|
71
|
+
config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n`
|
|
72
|
+
config += ` proxy_set_header X-Forwarded-Proto $scheme;\n`
|
|
73
|
+
config += ` proxy_cache_bypass $http_upgrade;\n`
|
|
74
|
+
config += ` proxy_connect_timeout 60s;\n`
|
|
75
|
+
config += ` proxy_send_timeout 60s;\n`
|
|
76
|
+
config += ` proxy_read_timeout 60s;\n`
|
|
77
|
+
config += ` }\n`
|
|
78
|
+
|
|
79
|
+
// Health check endpoint (if configured)
|
|
80
|
+
if (service.healthCheck) {
|
|
81
|
+
config += `\n # Health check endpoint\n`
|
|
82
|
+
config += ` location ${service.healthCheck.path} {\n`
|
|
83
|
+
config += ` proxy_pass http://${upstreamName};\n`
|
|
84
|
+
config += ` access_log off;\n`
|
|
85
|
+
config += ` }\n`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
config += `}\n`
|
|
89
|
+
|
|
90
|
+
return config
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Enable an Nginx site by creating a symbolic link
|
|
95
|
+
*/
|
|
96
|
+
export async function enableSite(siteName: string, configPath: string): Promise<void> {
|
|
97
|
+
const availablePath = path.join(configPath, `${siteName}.conf`)
|
|
98
|
+
const enabledPath = availablePath.replace('sites-available', 'sites-enabled')
|
|
99
|
+
|
|
100
|
+
// Create sites-enabled directory if it doesn't exist
|
|
101
|
+
await fs.ensureDir(path.dirname(enabledPath))
|
|
102
|
+
|
|
103
|
+
// Remove existing symlink if present
|
|
104
|
+
if (await fs.pathExists(enabledPath)) {
|
|
105
|
+
await fs.remove(enabledPath)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Create symlink
|
|
109
|
+
await execa('sudo', ['ln', '-sf', availablePath, enabledPath])
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Test and reload Nginx configuration
|
|
114
|
+
*/
|
|
115
|
+
export async function reloadNginx(reloadCommand: string): Promise<void> {
|
|
116
|
+
try {
|
|
117
|
+
// Test configuration first
|
|
118
|
+
await execa('sudo', ['nginx', '-t'])
|
|
119
|
+
|
|
120
|
+
// Reload Nginx
|
|
121
|
+
const parts = reloadCommand.split(' ')
|
|
122
|
+
if (parts.length > 0) {
|
|
123
|
+
// Simple execution of provided command
|
|
124
|
+
await execa(parts[0], parts.slice(1), { shell: true })
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new Error(`Failed to reload Nginx: ${error instanceof Error ? error.message : error}`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Disable an Nginx site
|
|
133
|
+
*/
|
|
134
|
+
export async function disableSite(siteName: string, configPath: string): Promise<void> {
|
|
135
|
+
const enabledPath = path.join(
|
|
136
|
+
configPath.replace('sites-available', 'sites-enabled'),
|
|
137
|
+
`${siteName}.conf`
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if (await fs.pathExists(enabledPath)) {
|
|
141
|
+
await fs.remove(enabledPath)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
nginx:
|
|
60
|
+
configPath: /etc/nginx/sites-available
|
|
61
|
+
reloadCommand: sudo nginx -t && sudo systemctl reload nginx
|
|
62
|
+
|
|
63
|
+
certbot:
|
|
64
|
+
email: admin@example.com
|
|
65
|
+
staging: false # Set to true for testing
|
|
66
|
+
|
|
67
|
+
deployment:
|
|
68
|
+
strategy: rolling # Options: rolling, blue-green
|
|
69
|
+
healthCheckTimeout: 30000 # milliseconds
|
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
|
+
})
|