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,303 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import path from "path";
|
|
4
|
+
function generateNginxConfig(service, withHttps, portOverride) {
|
|
5
|
+
const serverNames = service.domains.join(" ");
|
|
6
|
+
const primaryDomain = service.domains[0];
|
|
7
|
+
const domainSafe = primaryDomain.replace(/\./g, "_").replace(/[^a-zA-Z0-9_]/g, "_");
|
|
8
|
+
const upstreamName = `${domainSafe}_${service.name}`;
|
|
9
|
+
const servicePath = service.path || "/";
|
|
10
|
+
const port = portOverride || service.port;
|
|
11
|
+
let config = `# Nginx configuration for ${service.name}
|
|
12
|
+
|
|
13
|
+
`;
|
|
14
|
+
config += `upstream ${upstreamName} {
|
|
15
|
+
`;
|
|
16
|
+
config += ` server localhost:${port} max_fails=3 fail_timeout=30s;
|
|
17
|
+
`;
|
|
18
|
+
config += ` keepalive 32;
|
|
19
|
+
`;
|
|
20
|
+
config += `}
|
|
21
|
+
|
|
22
|
+
`;
|
|
23
|
+
if (withHttps) {
|
|
24
|
+
config += `server {
|
|
25
|
+
`;
|
|
26
|
+
config += ` listen 80;
|
|
27
|
+
`;
|
|
28
|
+
config += ` listen [::]:80;
|
|
29
|
+
`;
|
|
30
|
+
config += ` server_name ${serverNames};
|
|
31
|
+
|
|
32
|
+
`;
|
|
33
|
+
config += ` # Redirect all HTTP to HTTPS
|
|
34
|
+
`;
|
|
35
|
+
config += ` return 301 https://$server_name$request_uri;
|
|
36
|
+
`;
|
|
37
|
+
config += `}
|
|
38
|
+
|
|
39
|
+
`;
|
|
40
|
+
config += `server {
|
|
41
|
+
`;
|
|
42
|
+
config += ` listen 443 ssl http2;
|
|
43
|
+
`;
|
|
44
|
+
config += ` listen [::]:443 ssl http2;
|
|
45
|
+
`;
|
|
46
|
+
config += ` server_name ${serverNames};
|
|
47
|
+
|
|
48
|
+
`;
|
|
49
|
+
const primaryDomain2 = service.domains[0];
|
|
50
|
+
config += ` # SSL Configuration
|
|
51
|
+
`;
|
|
52
|
+
config += ` ssl_certificate /etc/letsencrypt/live/${primaryDomain2}/fullchain.pem;
|
|
53
|
+
`;
|
|
54
|
+
config += ` ssl_certificate_key /etc/letsencrypt/live/${primaryDomain2}/privkey.pem;
|
|
55
|
+
`;
|
|
56
|
+
config += ` ssl_protocols TLSv1.2 TLSv1.3;
|
|
57
|
+
`;
|
|
58
|
+
config += ` ssl_ciphers HIGH:!aNULL:!MD5;
|
|
59
|
+
`;
|
|
60
|
+
config += ` ssl_prefer_server_ciphers on;
|
|
61
|
+
|
|
62
|
+
`;
|
|
63
|
+
} else {
|
|
64
|
+
config += `server {
|
|
65
|
+
`;
|
|
66
|
+
config += ` listen 80;
|
|
67
|
+
`;
|
|
68
|
+
config += ` listen [::]:80;
|
|
69
|
+
`;
|
|
70
|
+
config += ` server_name ${serverNames};
|
|
71
|
+
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
config += ` # Logging
|
|
75
|
+
`;
|
|
76
|
+
config += ` access_log /var/log/nginx/${service.name}_access.log;
|
|
77
|
+
`;
|
|
78
|
+
config += ` error_log /var/log/nginx/${service.name}_error.log;
|
|
79
|
+
|
|
80
|
+
`;
|
|
81
|
+
config += ` # Client settings
|
|
82
|
+
`;
|
|
83
|
+
config += ` client_max_body_size 100M;
|
|
84
|
+
|
|
85
|
+
`;
|
|
86
|
+
config += ` # Proxy settings
|
|
87
|
+
`;
|
|
88
|
+
config += ` location ${servicePath} {
|
|
89
|
+
`;
|
|
90
|
+
config += ` proxy_pass http://${upstreamName};
|
|
91
|
+
`;
|
|
92
|
+
config += ` proxy_http_version 1.1;
|
|
93
|
+
`;
|
|
94
|
+
config += ` proxy_set_header Upgrade $http_upgrade;
|
|
95
|
+
`;
|
|
96
|
+
config += ` proxy_set_header Connection 'upgrade';
|
|
97
|
+
`;
|
|
98
|
+
config += ` proxy_set_header Host $host;
|
|
99
|
+
`;
|
|
100
|
+
config += ` proxy_set_header X-Real-IP $remote_addr;
|
|
101
|
+
`;
|
|
102
|
+
config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
103
|
+
`;
|
|
104
|
+
config += ` proxy_set_header X-Forwarded-Proto $scheme;
|
|
105
|
+
`;
|
|
106
|
+
config += ` proxy_cache_bypass $http_upgrade;
|
|
107
|
+
`;
|
|
108
|
+
config += ` proxy_connect_timeout 60s;
|
|
109
|
+
`;
|
|
110
|
+
config += ` proxy_send_timeout 60s;
|
|
111
|
+
`;
|
|
112
|
+
config += ` proxy_read_timeout 60s;
|
|
113
|
+
`;
|
|
114
|
+
config += ` }
|
|
115
|
+
`;
|
|
116
|
+
if (service.healthCheck) {
|
|
117
|
+
config += `
|
|
118
|
+
# Health check endpoint
|
|
119
|
+
`;
|
|
120
|
+
config += ` location ${service.healthCheck.path} {
|
|
121
|
+
`;
|
|
122
|
+
config += ` proxy_pass http://${upstreamName};
|
|
123
|
+
`;
|
|
124
|
+
config += ` access_log off;
|
|
125
|
+
`;
|
|
126
|
+
config += ` }
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
config += `}
|
|
130
|
+
`;
|
|
131
|
+
return config;
|
|
132
|
+
}
|
|
133
|
+
function generateMultiServiceNginxConfig(services, domain, withHttps, portOverrides) {
|
|
134
|
+
const upstreams = [];
|
|
135
|
+
const locations = [];
|
|
136
|
+
const healthChecks = [];
|
|
137
|
+
const sortedServices = [...services].sort((a, b) => {
|
|
138
|
+
const pathA = (a.path || "/").length;
|
|
139
|
+
const pathB = (b.path || "/").length;
|
|
140
|
+
return pathB - pathA;
|
|
141
|
+
});
|
|
142
|
+
const usedUpstreamNames = /* @__PURE__ */ new Set();
|
|
143
|
+
for (const service of sortedServices) {
|
|
144
|
+
const domainSafe = domain.replace(/\./g, "_").replace(/[^a-zA-Z0-9_]/g, "_");
|
|
145
|
+
let upstreamName = `${domainSafe}_${service.name}`;
|
|
146
|
+
let counter = 1;
|
|
147
|
+
const originalUpstreamName = upstreamName;
|
|
148
|
+
while (usedUpstreamNames.has(upstreamName)) {
|
|
149
|
+
upstreamName = `${originalUpstreamName}_${counter}`;
|
|
150
|
+
counter++;
|
|
151
|
+
}
|
|
152
|
+
usedUpstreamNames.add(upstreamName);
|
|
153
|
+
const servicePath = service.path || "/";
|
|
154
|
+
const port = portOverrides?.get(service.name) || service.port;
|
|
155
|
+
upstreams.push(`upstream ${upstreamName} {`);
|
|
156
|
+
upstreams.push(` server localhost:${port} max_fails=3 fail_timeout=30s;`);
|
|
157
|
+
upstreams.push(` keepalive 32;`);
|
|
158
|
+
upstreams.push(`}`);
|
|
159
|
+
if (servicePath === "/") {
|
|
160
|
+
locations.push(` # Service: ${service.name}`);
|
|
161
|
+
locations.push(` location / {`);
|
|
162
|
+
} else {
|
|
163
|
+
locations.push(` # Service: ${service.name}`);
|
|
164
|
+
locations.push(` location ${servicePath} {`);
|
|
165
|
+
}
|
|
166
|
+
locations.push(` proxy_pass http://${upstreamName};`);
|
|
167
|
+
locations.push(` proxy_http_version 1.1;`);
|
|
168
|
+
locations.push(` proxy_set_header Upgrade $http_upgrade;`);
|
|
169
|
+
locations.push(` proxy_set_header Connection 'upgrade';`);
|
|
170
|
+
locations.push(` proxy_set_header Host $host;`);
|
|
171
|
+
locations.push(` proxy_set_header X-Real-IP $remote_addr;`);
|
|
172
|
+
locations.push(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`);
|
|
173
|
+
locations.push(` proxy_set_header X-Forwarded-Proto $scheme;`);
|
|
174
|
+
locations.push(` proxy_cache_bypass $http_upgrade;`);
|
|
175
|
+
locations.push(` proxy_connect_timeout 60s;`);
|
|
176
|
+
locations.push(` proxy_send_timeout 60s;`);
|
|
177
|
+
locations.push(` proxy_read_timeout 60s;`);
|
|
178
|
+
locations.push(` }`);
|
|
179
|
+
if (service.healthCheck) {
|
|
180
|
+
healthChecks.push(` # Health check for ${service.name}`);
|
|
181
|
+
healthChecks.push(` location ${service.healthCheck.path} {`);
|
|
182
|
+
healthChecks.push(` proxy_pass http://${upstreamName};`);
|
|
183
|
+
healthChecks.push(` access_log off;`);
|
|
184
|
+
healthChecks.push(` }`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
let config = `# Nginx configuration for ${domain}
|
|
188
|
+
`;
|
|
189
|
+
config += `# Multiple services on the same domain
|
|
190
|
+
|
|
191
|
+
`;
|
|
192
|
+
config += upstreams.join("\n") + "\n\n";
|
|
193
|
+
if (withHttps) {
|
|
194
|
+
config += `server {
|
|
195
|
+
`;
|
|
196
|
+
config += ` listen 80;
|
|
197
|
+
`;
|
|
198
|
+
config += ` listen [::]:80;
|
|
199
|
+
`;
|
|
200
|
+
config += ` server_name ${domain};
|
|
201
|
+
|
|
202
|
+
`;
|
|
203
|
+
config += ` # Redirect all HTTP to HTTPS
|
|
204
|
+
`;
|
|
205
|
+
config += ` return 301 https://$server_name$request_uri;
|
|
206
|
+
`;
|
|
207
|
+
config += `}
|
|
208
|
+
|
|
209
|
+
`;
|
|
210
|
+
config += `server {
|
|
211
|
+
`;
|
|
212
|
+
config += ` listen 443 ssl http2;
|
|
213
|
+
`;
|
|
214
|
+
config += ` listen [::]:443 ssl http2;
|
|
215
|
+
`;
|
|
216
|
+
config += ` server_name ${domain};
|
|
217
|
+
|
|
218
|
+
`;
|
|
219
|
+
config += ` # SSL Configuration
|
|
220
|
+
`;
|
|
221
|
+
config += ` ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
|
|
222
|
+
`;
|
|
223
|
+
config += ` ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
|
|
224
|
+
`;
|
|
225
|
+
config += ` ssl_protocols TLSv1.2 TLSv1.3;
|
|
226
|
+
`;
|
|
227
|
+
config += ` ssl_ciphers HIGH:!aNULL:!MD5;
|
|
228
|
+
`;
|
|
229
|
+
config += ` ssl_prefer_server_ciphers on;
|
|
230
|
+
|
|
231
|
+
`;
|
|
232
|
+
} else {
|
|
233
|
+
config += `server {
|
|
234
|
+
`;
|
|
235
|
+
config += ` listen 80;
|
|
236
|
+
`;
|
|
237
|
+
config += ` listen [::]:80;
|
|
238
|
+
`;
|
|
239
|
+
config += ` server_name ${domain};
|
|
240
|
+
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
config += ` # Logging
|
|
244
|
+
`;
|
|
245
|
+
config += ` access_log /var/log/nginx/${domain}_access.log;
|
|
246
|
+
`;
|
|
247
|
+
config += ` error_log /var/log/nginx/${domain}_error.log;
|
|
248
|
+
|
|
249
|
+
`;
|
|
250
|
+
config += ` # Client settings
|
|
251
|
+
`;
|
|
252
|
+
config += ` client_max_body_size 100M;
|
|
253
|
+
|
|
254
|
+
`;
|
|
255
|
+
config += ` # Service locations
|
|
256
|
+
`;
|
|
257
|
+
config += locations.join("\n") + "\n\n";
|
|
258
|
+
if (healthChecks.length > 0) {
|
|
259
|
+
config += ` # Health check endpoints
|
|
260
|
+
`;
|
|
261
|
+
config += healthChecks.join("\n") + "\n";
|
|
262
|
+
}
|
|
263
|
+
config += `}
|
|
264
|
+
`;
|
|
265
|
+
return config;
|
|
266
|
+
}
|
|
267
|
+
async function writeNginxConfig(configName, configPath, configContent) {
|
|
268
|
+
const configFilePath = path.join(configPath, `${configName}.conf`);
|
|
269
|
+
const exists = await fs.pathExists(configFilePath);
|
|
270
|
+
if (exists) {
|
|
271
|
+
await fs.remove(configFilePath);
|
|
272
|
+
}
|
|
273
|
+
await fs.writeFile(configFilePath, configContent);
|
|
274
|
+
return exists;
|
|
275
|
+
}
|
|
276
|
+
async function enableSite(siteName, configPath) {
|
|
277
|
+
const availablePath = path.join(configPath, `${siteName}.conf`);
|
|
278
|
+
const enabledPath = availablePath.replace("sites-available", "sites-enabled");
|
|
279
|
+
await fs.ensureDir(path.dirname(enabledPath));
|
|
280
|
+
if (await fs.pathExists(enabledPath)) {
|
|
281
|
+
await fs.remove(enabledPath);
|
|
282
|
+
}
|
|
283
|
+
await execa("sudo", ["ln", "-sf", availablePath, enabledPath]);
|
|
284
|
+
}
|
|
285
|
+
async function reloadNginx(reloadCommand) {
|
|
286
|
+
try {
|
|
287
|
+
await execa("sudo", ["nginx", "-t"]);
|
|
288
|
+
const parts = reloadCommand.split(" ");
|
|
289
|
+
if (parts.length > 0) {
|
|
290
|
+
await execa(parts[0], parts.slice(1), { shell: true });
|
|
291
|
+
}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
throw new Error(`Failed to reload Nginx: ${error instanceof Error ? error.message : error}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
export {
|
|
297
|
+
enableSite,
|
|
298
|
+
generateMultiServiceNginxConfig,
|
|
299
|
+
generateNginxConfig,
|
|
300
|
+
reloadNginx,
|
|
301
|
+
writeNginxConfig
|
|
302
|
+
};
|
|
303
|
+
//# sourceMappingURL=nginx.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nginx.js","sources":["../../src/utils/nginx.ts"],"sourcesContent":["import { execa } from 'execa'\nimport fs from 'fs-extra'\nimport path from 'path'\nimport type { ServiceConfig } from '../types/config'\n\n/**\n * Generate Nginx server block configuration for a service\n */\nexport function generateNginxConfig(\n service: ServiceConfig,\n withHttps: boolean,\n portOverride?: number\n): string {\n const serverNames = service.domains.join(' ')\n // Use primary domain for upstream naming to ensure uniqueness\n const primaryDomain = service.domains[0]\n const domainSafe = primaryDomain.replace(/\\./g, '_').replace(/[^a-zA-Z0-9_]/g, '_')\n const upstreamName = `${domainSafe}_${service.name}`\n const servicePath = service.path || '/'\n const port = portOverride || service.port\n\n let config = `# Nginx configuration for ${service.name}\\n\\n`\n\n // Upstream configuration\n config += `upstream ${upstreamName} {\\n`\n config += ` server localhost:${port} max_fails=3 fail_timeout=30s;\\n`\n config += ` keepalive 32;\\n`\n config += `}\\n\\n`\n\n if (withHttps) {\n // HTTP server - redirect to HTTPS\n config += `server {\\n`\n config += ` listen 80;\\n`\n config += ` listen [::]:80;\\n`\n config += ` server_name ${serverNames};\\n\\n`\n config += ` # Redirect all HTTP to HTTPS\\n`\n config += ` return 301 https://$server_name$request_uri;\\n`\n config += `}\\n\\n`\n\n // HTTPS server\n config += `server {\\n`\n config += ` listen 443 ssl http2;\\n`\n config += ` listen [::]:443 ssl http2;\\n`\n config += ` server_name ${serverNames};\\n\\n`\n\n // SSL configuration\n const primaryDomain = service.domains[0]\n config += ` # SSL Configuration\\n`\n config += ` ssl_certificate /etc/letsencrypt/live/${primaryDomain}/fullchain.pem;\\n`\n config += ` ssl_certificate_key /etc/letsencrypt/live/${primaryDomain}/privkey.pem;\\n`\n config += ` ssl_protocols TLSv1.2 TLSv1.3;\\n`\n config += ` ssl_ciphers HIGH:!aNULL:!MD5;\\n`\n config += ` ssl_prefer_server_ciphers on;\\n\\n`\n } else {\n // HTTP only server\n config += `server {\\n`\n config += ` listen 80;\\n`\n config += ` listen [::]:80;\\n`\n config += ` server_name ${serverNames};\\n\\n`\n }\n\n // Logging\n config += ` # Logging\\n`\n config += ` access_log /var/log/nginx/${service.name}_access.log;\\n`\n config += ` error_log /var/log/nginx/${service.name}_error.log;\\n\\n`\n\n // Client settings\n config += ` # Client settings\\n`\n config += ` client_max_body_size 100M;\\n\\n`\n\n // Proxy settings\n config += ` # Proxy settings\\n`\n config += ` location ${servicePath} {\\n`\n config += ` proxy_pass http://${upstreamName};\\n`\n config += ` proxy_http_version 1.1;\\n`\n config += ` proxy_set_header Upgrade $http_upgrade;\\n`\n config += ` proxy_set_header Connection 'upgrade';\\n`\n config += ` proxy_set_header Host $host;\\n`\n config += ` proxy_set_header X-Real-IP $remote_addr;\\n`\n config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\\n`\n config += ` proxy_set_header X-Forwarded-Proto $scheme;\\n`\n config += ` proxy_cache_bypass $http_upgrade;\\n`\n config += ` proxy_connect_timeout 60s;\\n`\n config += ` proxy_send_timeout 60s;\\n`\n config += ` proxy_read_timeout 60s;\\n`\n config += ` }\\n`\n\n // Health check endpoint (if configured)\n if (service.healthCheck) {\n config += `\\n # Health check endpoint\\n`\n config += ` location ${service.healthCheck.path} {\\n`\n config += ` proxy_pass http://${upstreamName};\\n`\n config += ` access_log off;\\n`\n config += ` }\\n`\n }\n\n config += `}\\n`\n\n return config\n}\n\n/**\n * Generate Nginx configuration for multiple services on the same domain\n * Groups services by domain and creates location blocks for each service path\n */\nexport function generateMultiServiceNginxConfig(\n services: ServiceConfig[],\n domain: string,\n withHttps: boolean,\n portOverrides?: Map<string, number>\n): string {\n const upstreams: string[] = []\n const locations: string[] = []\n const healthChecks: string[] = []\n\n // Sort services by path length (longest first) to ensure specific paths are matched before general ones\n const sortedServices = [...services].sort((a, b) => {\n const pathA = (a.path || '/').length\n const pathB = (b.path || '/').length\n return pathB - pathA\n })\n\n // Track upstream names to ensure uniqueness within the same domain config\n const usedUpstreamNames = new Set<string>()\n\n for (const service of sortedServices) {\n // Create unique upstream name: domain_service_name to avoid conflicts\n // Replace dots and special chars in domain for valid nginx upstream name\n const domainSafe = domain.replace(/\\./g, '_').replace(/[^a-zA-Z0-9_]/g, '_')\n let upstreamName = `${domainSafe}_${service.name}`\n\n // Ensure uniqueness (in case same service name appears multiple times)\n let counter = 1\n const originalUpstreamName = upstreamName\n while (usedUpstreamNames.has(upstreamName)) {\n upstreamName = `${originalUpstreamName}_${counter}`\n counter++\n }\n usedUpstreamNames.add(upstreamName)\n\n const servicePath = service.path || '/'\n const port = portOverrides?.get(service.name) || service.port\n\n // Generate upstream for each service on the same domain\n upstreams.push(`upstream ${upstreamName} {`)\n upstreams.push(` server localhost:${port} max_fails=3 fail_timeout=30s;`)\n upstreams.push(` keepalive 32;`)\n upstreams.push(`}`)\n\n // Generate location block\n if (servicePath === '/') {\n // Root path - use exact match or default\n locations.push(` # Service: ${service.name}`)\n locations.push(` location / {`)\n } else {\n // Specific path - use prefix match\n locations.push(` # Service: ${service.name}`)\n locations.push(` location ${servicePath} {`)\n }\n\n locations.push(` proxy_pass http://${upstreamName};`)\n locations.push(` proxy_http_version 1.1;`)\n locations.push(` proxy_set_header Upgrade $http_upgrade;`)\n locations.push(` proxy_set_header Connection 'upgrade';`)\n locations.push(` proxy_set_header Host $host;`)\n locations.push(` proxy_set_header X-Real-IP $remote_addr;`)\n locations.push(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`)\n locations.push(` proxy_set_header X-Forwarded-Proto $scheme;`)\n locations.push(` proxy_cache_bypass $http_upgrade;`)\n locations.push(` proxy_connect_timeout 60s;`)\n locations.push(` proxy_send_timeout 60s;`)\n locations.push(` proxy_read_timeout 60s;`)\n locations.push(` }`)\n\n // Health check endpoint\n if (service.healthCheck) {\n healthChecks.push(` # Health check for ${service.name}`)\n healthChecks.push(` location ${service.healthCheck.path} {`)\n healthChecks.push(` proxy_pass http://${upstreamName};`)\n healthChecks.push(` access_log off;`)\n healthChecks.push(` }`)\n }\n }\n\n let config = `# Nginx configuration for ${domain}\\n`\n config += `# Multiple services on the same domain\\n\\n`\n\n // Add upstreams\n config += upstreams.join('\\n') + '\\n\\n'\n\n if (withHttps) {\n // HTTP server - redirect to HTTPS\n config += `server {\\n`\n config += ` listen 80;\\n`\n config += ` listen [::]:80;\\n`\n config += ` server_name ${domain};\\n\\n`\n config += ` # Redirect all HTTP to HTTPS\\n`\n config += ` return 301 https://$server_name$request_uri;\\n`\n config += `}\\n\\n`\n\n // HTTPS server\n config += `server {\\n`\n config += ` listen 443 ssl http2;\\n`\n config += ` listen [::]:443 ssl http2;\\n`\n config += ` server_name ${domain};\\n\\n`\n\n // SSL configuration\n config += ` # SSL Configuration\\n`\n config += ` ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;\\n`\n config += ` ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;\\n`\n config += ` ssl_protocols TLSv1.2 TLSv1.3;\\n`\n config += ` ssl_ciphers HIGH:!aNULL:!MD5;\\n`\n config += ` ssl_prefer_server_ciphers on;\\n\\n`\n } else {\n // HTTP only server\n config += `server {\\n`\n config += ` listen 80;\\n`\n config += ` listen [::]:80;\\n`\n config += ` server_name ${domain};\\n\\n`\n }\n\n // Logging\n config += ` # Logging\\n`\n config += ` access_log /var/log/nginx/${domain}_access.log;\\n`\n config += ` error_log /var/log/nginx/${domain}_error.log;\\n\\n`\n\n // Client settings\n config += ` # Client settings\\n`\n config += ` client_max_body_size 100M;\\n\\n`\n\n // Location blocks\n config += ` # Service locations\\n`\n config += locations.join('\\n') + '\\n\\n'\n\n // Health check endpoints\n if (healthChecks.length > 0) {\n config += ` # Health check endpoints\\n`\n config += healthChecks.join('\\n') + '\\n'\n }\n\n config += `}\\n`\n\n return config\n}\n\n/**\n * Check if an Nginx config file exists\n */\nexport async function configExists(configName: string, configPath: string): Promise<boolean> {\n const configFilePath = path.join(configPath, `${configName}.conf`)\n return await fs.pathExists(configFilePath)\n}\n\n/**\n * Write Nginx configuration file, deleting existing file if it exists and creating new one\n */\nexport async function writeNginxConfig(\n configName: string,\n configPath: string,\n configContent: string\n): Promise<boolean> {\n const configFilePath = path.join(configPath, `${configName}.conf`)\n const exists = await fs.pathExists(configFilePath)\n\n // If config file exists, delete it first\n if (exists) {\n await fs.remove(configFilePath)\n }\n\n // Create new config file\n await fs.writeFile(configFilePath, configContent)\n\n return exists // Return true if file existed (was deleted and recreated)\n}\n\n/**\n * Enable an Nginx site by creating a symbolic link\n */\nexport async function enableSite(siteName: string, configPath: string): Promise<void> {\n const availablePath = path.join(configPath, `${siteName}.conf`)\n const enabledPath = availablePath.replace('sites-available', 'sites-enabled')\n\n // Create sites-enabled directory if it doesn't exist\n await fs.ensureDir(path.dirname(enabledPath))\n\n // Remove existing symlink if present\n if (await fs.pathExists(enabledPath)) {\n await fs.remove(enabledPath)\n }\n\n // Create symlink\n await execa('sudo', ['ln', '-sf', availablePath, enabledPath])\n}\n\n/**\n * Test and reload Nginx configuration\n */\nexport async function reloadNginx(reloadCommand: string): Promise<void> {\n try {\n // Test configuration first\n await execa('sudo', ['nginx', '-t'])\n\n // Reload Nginx\n const parts = reloadCommand.split(' ')\n if (parts.length > 0) {\n // Simple execution of provided command\n await execa(parts[0], parts.slice(1), { shell: true })\n }\n } catch (error) {\n throw new Error(`Failed to reload Nginx: ${error instanceof Error ? error.message : error}`)\n }\n}\n\n/**\n * Disable an Nginx site\n */\nexport async function disableSite(siteName: string, configPath: string): Promise<void> {\n const enabledPath = path.join(\n configPath.replace('sites-available', 'sites-enabled'),\n `${siteName}.conf`\n )\n\n if (await fs.pathExists(enabledPath)) {\n await fs.remove(enabledPath)\n }\n}\n"],"names":["primaryDomain"],"mappings":"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"}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Suthep Documentation
|
|
2
|
+
|
|
3
|
+
Welcome to the Suthep documentation! This guide will help you understand and use Suthep, a powerful CLI tool for deploying projects with automatic Nginx reverse proxy setup, HTTPS with Certbot, and zero-downtime deployments.
|
|
4
|
+
|
|
5
|
+
## Choose Your Language / เลือกภาษาของคุณ
|
|
6
|
+
|
|
7
|
+
- 🇺🇸 [English Documentation](./en/) - English documentation
|
|
8
|
+
- 🇹🇭 [เอกสารภาษาไทย](./th/) - Thai documentation
|
|
9
|
+
|
|
10
|
+
## Quick Navigation
|
|
11
|
+
|
|
12
|
+
### English / ภาษาอังกฤษ
|
|
13
|
+
|
|
14
|
+
- [Getting Started](./en/getting-started.md) - Installation and quick start guide
|
|
15
|
+
- [Configuration Reference](./en/configuration.md) - Complete configuration file reference
|
|
16
|
+
- [Commands Reference](./en/commands.md) - Detailed command documentation
|
|
17
|
+
- [Examples](./en/examples.md) - Real-world deployment examples
|
|
18
|
+
- [Troubleshooting](./en/troubleshooting.md) - Common issues and solutions
|
|
19
|
+
- [Developer Guide](./en/developer-guide.md) - Complete guide for developers
|
|
20
|
+
|
|
21
|
+
### ไทย / Thai
|
|
22
|
+
|
|
23
|
+
- [เริ่มต้นใช้งาน](./th/getting-started.md) - คู่มือการติดตั้งและเริ่มต้นใช้งาน
|
|
24
|
+
- [คู่มือการตั้งค่า](./th/configuration.md) - คู่มืออ้างอิงไฟล์ configuration ฉบับสมบูรณ์
|
|
25
|
+
- [คู่มือคำสั่ง](./th/commands.md) - เอกสารคำสั่งแบบละเอียด
|
|
26
|
+
|
|
27
|
+
## What is Suthep?
|
|
28
|
+
|
|
29
|
+
Suthep is a deployment automation tool that simplifies the process of deploying web services with:
|
|
30
|
+
|
|
31
|
+
- ✅ **Automatic Nginx reverse proxy setup** - No manual Nginx configuration needed
|
|
32
|
+
- ✅ **Automatic HTTPS with Certbot** - Free SSL certificates from Let's Encrypt
|
|
33
|
+
- ✅ **Zero-downtime deployment** - Rolling and blue-green deployment strategies
|
|
34
|
+
- ✅ **Docker container support** - Deploy containerized applications easily
|
|
35
|
+
- ✅ **Multiple domain/subdomain support** - One service, multiple domains
|
|
36
|
+
- ✅ **Health check integration** - Ensure services are healthy before going live
|
|
37
|
+
- ✅ **YAML-based configuration** - Simple, declarative configuration
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Install dependencies
|
|
43
|
+
npm install
|
|
44
|
+
npm link
|
|
45
|
+
|
|
46
|
+
# Initialize configuration
|
|
47
|
+
suthep init
|
|
48
|
+
|
|
49
|
+
# Setup prerequisites
|
|
50
|
+
suthep setup
|
|
51
|
+
|
|
52
|
+
# Deploy your services
|
|
53
|
+
suthep deploy
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Requirements
|
|
57
|
+
|
|
58
|
+
- Node.js 16+
|
|
59
|
+
- Nginx (installed via `suthep setup`)
|
|
60
|
+
- Certbot (installed via `suthep setup`)
|
|
61
|
+
- Docker (optional, for Docker-based services)
|
|
62
|
+
- sudo access (for Nginx and Certbot operations)
|
|
63
|
+
|
|
64
|
+
## Documentation Structure
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
docs/
|
|
68
|
+
├── README.md (this file)
|
|
69
|
+
├── en/ # English documentation
|
|
70
|
+
│ ├── README.md
|
|
71
|
+
│ ├── getting-started.md
|
|
72
|
+
│ ├── configuration.md
|
|
73
|
+
│ ├── commands.md
|
|
74
|
+
│ └── ...
|
|
75
|
+
└── th/ # Thai documentation
|
|
76
|
+
├── README.md
|
|
77
|
+
├── getting-started.md
|
|
78
|
+
├── configuration.md
|
|
79
|
+
└── commands.md
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Contributing Translations
|
|
83
|
+
|
|
84
|
+
We welcome contributions to translate documentation into more languages!
|
|
85
|
+
|
|
86
|
+
To contribute:
|
|
87
|
+
1. Create a new language folder (e.g., `ja/` for Japanese)
|
|
88
|
+
2. Translate the documentation files
|
|
89
|
+
3. Update this README with links to the new language
|
|
90
|
+
4. Submit a pull request
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
[MIT](../LICENSE)
|
|
95
|
+
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Translations Guide
|
|
2
|
+
|
|
3
|
+
This document describes the translation structure and how to contribute translations for the Suthep documentation.
|
|
4
|
+
|
|
5
|
+
## Directory Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
docs/
|
|
9
|
+
├── README.md # Main documentation index with language selection
|
|
10
|
+
├── TRANSLATIONS.md # This file - translation guide
|
|
11
|
+
├── en/ # English documentation (default)
|
|
12
|
+
│ ├── README.md
|
|
13
|
+
│ ├── getting-started.md
|
|
14
|
+
│ ├── configuration.md
|
|
15
|
+
│ ├── commands.md
|
|
16
|
+
│ ├── port-binding.md
|
|
17
|
+
│ ├── docker-ports-config.md
|
|
18
|
+
│ ├── examples.md
|
|
19
|
+
│ ├── troubleshooting.md
|
|
20
|
+
│ ├── architecture.md
|
|
21
|
+
│ ├── developer-guide.md
|
|
22
|
+
│ └── api-reference.md
|
|
23
|
+
└── th/ # Thai documentation
|
|
24
|
+
├── README.md
|
|
25
|
+
├── getting-started.md
|
|
26
|
+
├── configuration.md
|
|
27
|
+
└── commands.md
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Translation Status
|
|
31
|
+
|
|
32
|
+
| Document | English | Thai | Status |
|
|
33
|
+
|----------|---------|------|--------|
|
|
34
|
+
| README | ✅ | ✅ | Complete |
|
|
35
|
+
| Getting Started | ✅ | ✅ | Complete |
|
|
36
|
+
| Configuration | ✅ | ✅ | Complete |
|
|
37
|
+
| Commands | ✅ | ✅ | Complete |
|
|
38
|
+
| Port Binding | ✅ | ⏳ | English only |
|
|
39
|
+
| Docker Ports Config | ✅ | ⏳ | English only |
|
|
40
|
+
| Examples | ✅ | ⏳ | English only |
|
|
41
|
+
| Troubleshooting | ✅ | ⏳ | English only |
|
|
42
|
+
| Architecture | ✅ | ⏳ | English only |
|
|
43
|
+
| Developer Guide | ✅ | ⏳ | English only |
|
|
44
|
+
| API Reference | ✅ | ⏳ | English only |
|
|
45
|
+
|
|
46
|
+
**Legend:**
|
|
47
|
+
- ✅ Available
|
|
48
|
+
- ⏳ Coming soon / In progress
|
|
49
|
+
- ❌ Not started
|
|
50
|
+
|
|
51
|
+
## Adding a New Translation
|
|
52
|
+
|
|
53
|
+
### Step 1: Create Language Folder
|
|
54
|
+
|
|
55
|
+
Create a new folder for your language using the ISO 639-1 language code:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
mkdir docs/ja # For Japanese
|
|
59
|
+
mkdir docs/es # For Spanish
|
|
60
|
+
mkdir docs/fr # For French
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Step 2: Create README.md
|
|
64
|
+
|
|
65
|
+
Create a `README.md` file in the new language folder that:
|
|
66
|
+
- Welcomes users in the target language
|
|
67
|
+
- Lists all available documents
|
|
68
|
+
- Links to English versions for untranslated documents
|
|
69
|
+
- Links back to the main documentation index
|
|
70
|
+
|
|
71
|
+
Example structure:
|
|
72
|
+
|
|
73
|
+
```markdown
|
|
74
|
+
# Suthep Documentation
|
|
75
|
+
|
|
76
|
+
[Welcome message in target language]
|
|
77
|
+
|
|
78
|
+
## Table of Contents
|
|
79
|
+
|
|
80
|
+
1. [Getting Started](./getting-started.md)
|
|
81
|
+
2. [Configuration](./configuration.md)
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
## Other Languages
|
|
85
|
+
|
|
86
|
+
- [English](../en/README.md)
|
|
87
|
+
- [Your Language](./README.md) (current)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Step 3: Translate Documents
|
|
91
|
+
|
|
92
|
+
Translate documents in priority order:
|
|
93
|
+
|
|
94
|
+
1. **README.md** - Main index
|
|
95
|
+
2. **getting-started.md** - Most important for new users
|
|
96
|
+
3. **configuration.md** - Essential reference
|
|
97
|
+
4. **commands.md** - User-facing documentation
|
|
98
|
+
5. Other documents as needed
|
|
99
|
+
|
|
100
|
+
### Step 4: Update Links
|
|
101
|
+
|
|
102
|
+
- Update internal links to use relative paths within the language folder
|
|
103
|
+
- Link to English versions (`../en/filename.md`) for untranslated documents
|
|
104
|
+
- Update the main `docs/README.md` to include your language
|
|
105
|
+
|
|
106
|
+
### Step 5: Update Translation Status
|
|
107
|
+
|
|
108
|
+
Update this file (`TRANSLATIONS.md`) to reflect the new translation status.
|
|
109
|
+
|
|
110
|
+
## Translation Guidelines
|
|
111
|
+
|
|
112
|
+
### File Naming
|
|
113
|
+
|
|
114
|
+
- Use the same filenames as English versions
|
|
115
|
+
- Keep file extensions: `.md`
|
|
116
|
+
- No language suffix needed (language is determined by folder)
|
|
117
|
+
|
|
118
|
+
### Link Structure
|
|
119
|
+
|
|
120
|
+
- **Within same language**: `./filename.md`
|
|
121
|
+
- **To English version**: `../en/filename.md`
|
|
122
|
+
- **To main index**: `../README.md`
|
|
123
|
+
- **To other languages**: `../lang-code/README.md`
|
|
124
|
+
|
|
125
|
+
### Code Examples
|
|
126
|
+
|
|
127
|
+
- Keep code examples in English (they're universal)
|
|
128
|
+
- Translate comments in code examples if appropriate
|
|
129
|
+
- Keep command-line examples in English
|
|
130
|
+
|
|
131
|
+
### Technical Terms
|
|
132
|
+
|
|
133
|
+
- Keep technical terms in English when there's no common translation
|
|
134
|
+
- Use English terms for: Docker, Nginx, Certbot, SSL, HTTPS, etc.
|
|
135
|
+
- Provide explanations in the target language
|
|
136
|
+
|
|
137
|
+
### Formatting
|
|
138
|
+
|
|
139
|
+
- Maintain the same Markdown structure
|
|
140
|
+
- Keep the same heading hierarchy
|
|
141
|
+
- Preserve code blocks and syntax highlighting
|
|
142
|
+
- Maintain table structures
|
|
143
|
+
|
|
144
|
+
## Website Integration
|
|
145
|
+
|
|
146
|
+
This structure is designed to work with documentation websites:
|
|
147
|
+
|
|
148
|
+
### Static Site Generators
|
|
149
|
+
|
|
150
|
+
Works well with:
|
|
151
|
+
- **Docusaurus** - Multi-language support built-in
|
|
152
|
+
- **VitePress** - Supports i18n
|
|
153
|
+
- **MkDocs** - With i18n plugin
|
|
154
|
+
- **GitBook** - Multi-language support
|
|
155
|
+
|
|
156
|
+
### URL Structure
|
|
157
|
+
|
|
158
|
+
For websites, URLs would be:
|
|
159
|
+
- `/en/getting-started` - English
|
|
160
|
+
- `/th/getting-started` - Thai
|
|
161
|
+
- `/` - Language selection
|
|
162
|
+
|
|
163
|
+
### Configuration Example (Docusaurus)
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
// docusaurus.config.js
|
|
167
|
+
module.exports = {
|
|
168
|
+
i18n: {
|
|
169
|
+
defaultLocale: 'en',
|
|
170
|
+
locales: ['en', 'th'],
|
|
171
|
+
localeConfigs: {
|
|
172
|
+
en: {
|
|
173
|
+
label: 'English',
|
|
174
|
+
},
|
|
175
|
+
th: {
|
|
176
|
+
label: 'ไทย',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Contributing
|
|
184
|
+
|
|
185
|
+
1. **Fork the repository**
|
|
186
|
+
2. **Create a language folder** (if new language)
|
|
187
|
+
3. **Translate documents** following the guidelines
|
|
188
|
+
4. **Update this file** with translation status
|
|
189
|
+
5. **Submit a pull request**
|
|
190
|
+
|
|
191
|
+
## Quality Checklist
|
|
192
|
+
|
|
193
|
+
Before submitting a translation:
|
|
194
|
+
|
|
195
|
+
- [ ] All internal links work correctly
|
|
196
|
+
- [ ] Code examples are preserved
|
|
197
|
+
- [ ] Technical terms are handled appropriately
|
|
198
|
+
- [ ] Formatting matches English version
|
|
199
|
+
- [ ] README.md includes language selection
|
|
200
|
+
- [ ] Translation status is updated
|
|
201
|
+
- [ ] Main README.md is updated
|
|
202
|
+
|
|
203
|
+
## Questions?
|
|
204
|
+
|
|
205
|
+
If you have questions about translations:
|
|
206
|
+
- Open an issue on GitHub
|
|
207
|
+
- Check existing translations for examples
|
|
208
|
+
- Review the English documentation for context
|
|
209
|
+
|
|
210
|
+
Thank you for contributing to Suthep documentation! 🎉
|
|
211
|
+
|