suthep 0.1.0 → 0.2.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.
Files changed (72) hide show
  1. package/README.md +172 -71
  2. package/dist/commands/deploy.js +251 -37
  3. package/dist/commands/deploy.js.map +1 -1
  4. package/dist/commands/down.js +179 -0
  5. package/dist/commands/down.js.map +1 -0
  6. package/dist/commands/redeploy.js +59 -0
  7. package/dist/commands/redeploy.js.map +1 -0
  8. package/dist/commands/up.js +213 -0
  9. package/dist/commands/up.js.map +1 -0
  10. package/dist/index.js +36 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/utils/certbot.js +40 -3
  13. package/dist/utils/certbot.js.map +1 -1
  14. package/dist/utils/config-loader.js +30 -0
  15. package/dist/utils/config-loader.js.map +1 -1
  16. package/dist/utils/deployment.js +49 -16
  17. package/dist/utils/deployment.js.map +1 -1
  18. package/dist/utils/docker.js +396 -25
  19. package/dist/utils/docker.js.map +1 -1
  20. package/dist/utils/nginx.js +167 -8
  21. package/dist/utils/nginx.js.map +1 -1
  22. package/docs/README.md +25 -49
  23. package/docs/english/01-introduction.md +84 -0
  24. package/docs/english/02-installation.md +200 -0
  25. package/docs/english/03-quick-start.md +256 -0
  26. package/docs/english/04-configuration.md +358 -0
  27. package/docs/english/05-commands.md +363 -0
  28. package/docs/english/06-examples.md +456 -0
  29. package/docs/english/07-troubleshooting.md +417 -0
  30. package/docs/english/08-advanced.md +411 -0
  31. package/docs/english/README.md +48 -0
  32. package/docs/thai/01-introduction.md +84 -0
  33. package/docs/thai/02-installation.md +200 -0
  34. package/docs/thai/03-quick-start.md +256 -0
  35. package/docs/thai/04-configuration.md +358 -0
  36. package/docs/thai/05-commands.md +363 -0
  37. package/docs/thai/06-examples.md +456 -0
  38. package/docs/thai/07-troubleshooting.md +417 -0
  39. package/docs/thai/08-advanced.md +411 -0
  40. package/docs/thai/README.md +48 -0
  41. package/example/README.md +286 -53
  42. package/example/suthep-complete.yml +103 -0
  43. package/example/suthep-docker-only.yml +71 -0
  44. package/example/suthep-no-docker.yml +51 -0
  45. package/example/suthep-path-routing.yml +62 -0
  46. package/example/suthep.example.yml +89 -0
  47. package/package.json +1 -1
  48. package/src/commands/deploy.ts +322 -50
  49. package/src/commands/down.ts +240 -0
  50. package/src/commands/redeploy.ts +78 -0
  51. package/src/commands/up.ts +271 -0
  52. package/src/index.ts +62 -1
  53. package/src/types/config.ts +25 -24
  54. package/src/utils/certbot.ts +68 -6
  55. package/src/utils/config-loader.ts +40 -0
  56. package/src/utils/deployment.ts +61 -36
  57. package/src/utils/docker.ts +634 -30
  58. package/src/utils/nginx.ts +187 -4
  59. package/suthep-0.1.0-beta.1.tgz +0 -0
  60. package/suthep-0.1.1.tgz +0 -0
  61. package/suthep.example.yml +34 -0
  62. package/suthep.yml +39 -0
  63. package/test +0 -0
  64. package/docs/api-reference.md +0 -545
  65. package/docs/architecture.md +0 -367
  66. package/docs/commands.md +0 -273
  67. package/docs/configuration.md +0 -347
  68. package/docs/examples.md +0 -537
  69. package/docs/getting-started.md +0 -197
  70. package/docs/troubleshooting.md +0 -441
  71. package/example/docker-compose.yml +0 -72
  72. package/example/suthep.yml +0 -31
package/README.md CHANGED
@@ -2,56 +2,83 @@
2
2
 
3
3
  A powerful CLI tool for deploying projects with automatic Nginx reverse proxy setup, HTTPS with Certbot, and zero-downtime deployments.
4
4
 
5
+ ## Table of Contents
6
+
7
+ - [Features](#features)
8
+ - [Installation](#installation)
9
+ - [Quick Start](#quick-start)
10
+ - [Configuration](#configuration)
11
+ - [Commands](#commands)
12
+ - [Examples](#examples)
13
+ - [Requirements](#requirements)
14
+ - [Benefits](#benefits)
15
+ - [License](#license)
16
+
5
17
  ## Features
6
18
 
7
- - ✅ Automatic Nginx reverse proxy setup
8
- - ✅ Automatic HTTPS with Certbot
9
- - ✅ Zero-downtime deployment
10
- - ✅ Docker container support
11
- - ✅ Multiple domain/subdomain support
12
- - ✅ Health check integration
13
- - ✅ YAML-based configuration
19
+ - ✅ **Automatic Nginx Reverse Proxy** - Configures Nginx automatically for your services
20
+ - ✅ **Automatic HTTPS** - Sets up SSL/TLS certificates with Let's Encrypt via Certbot
21
+ - ✅ **Zero-Downtime Deployment** - Rolling deployment strategy ensures continuous availability
22
+ - ✅ **Docker Support** - Deploy Docker containers or connect to existing containers
23
+ - ✅ **Multiple Domains** - Support for multiple domains and subdomains per service
24
+ - ✅ **Health Checks** - Built-in health check monitoring for service reliability
25
+ - ✅ **YAML Configuration** - Simple, declarative configuration file format
14
26
 
15
27
  ## Installation
16
28
 
29
+ Install Suthep globally using your preferred package manager:
30
+
17
31
  ```bash
18
- npm install
19
- npm link # Makes 'deploy' command available globally
32
+ npm install -g suthep
33
+ # or
34
+ yarn global add suthep
35
+ # or
36
+ pnpm add -g suthep
20
37
  ```
21
38
 
22
- The tool uses `tsx` to run TypeScript directly, so no build step is required for development. For production, you can still build:
39
+ > **Note:** The tool uses `tsx` to run TypeScript directly, so no build step is required for development. For production builds, you can optionally compile to JavaScript using `npm run build`.
40
+
41
+ ## Quick Start
42
+
43
+ ### 1. Initialize Configuration
44
+
45
+ Create a new configuration file interactively:
23
46
 
24
47
  ```bash
25
- npm run build # Optional: compile to JavaScript
48
+ suthep init
26
49
  ```
27
50
 
28
- ## Quick Start
51
+ This will create a `suthep.yml` file with interactive prompts, or you can use an example configuration:
29
52
 
30
- 1. **Initialize a configuration file:**
31
- ```bash
32
- deploy init
33
- ```
34
- This will create a `deploy.yml` file with interactive prompts.
53
+ ```bash
54
+ cp example/suthep.yml suthep.yml
55
+ # Edit suthep.yml with your settings
56
+ ```
57
+
58
+ ### 2. Setup Prerequisites
35
59
 
36
- 2. **Or use the example configuration:**
37
- ```bash
38
- cp example.yml deploy.yml
39
- # Edit deploy.yml with your settings
40
- ```
60
+ Install and configure Nginx and Certbot on your system:
41
61
 
42
- 3. **Setup prerequisites (Nginx and Certbot):**
43
- ```bash
44
- deploy setup
45
- ```
62
+ ```bash
63
+ suthep setup
64
+ ```
46
65
 
47
- 4. **Deploy your project:**
48
- ```bash
49
- deploy deploy
50
- ```
66
+ This command will:
67
+ - Install Nginx (if not already installed)
68
+ - Install Certbot (if not already installed)
69
+ - Configure system dependencies
70
+
71
+ ### 3. Deploy Your Project
72
+
73
+ Deploy your project using the configuration file:
74
+
75
+ ```bash
76
+ suthep deploy
77
+ ```
51
78
 
52
79
  ## Configuration
53
80
 
54
- The configuration file (`deploy.yml`) supports the following structure:
81
+ The configuration file (`suthep.yml`) uses a YAML format to define your deployment. Here's the complete structure:
55
82
 
56
83
  ```yaml
57
84
  project:
@@ -95,56 +122,84 @@ deployment:
95
122
 
96
123
  ### Service Configuration
97
124
 
98
- Each service can have:
125
+ Each service in the `services` array can have the following properties:
126
+
127
+ | Property | Required | Description |
128
+ |----------|----------|-------------|
129
+ | `name` | ✅ Yes | Unique identifier for the service |
130
+ | `port` | ✅ Yes | Port number the service runs on (host port) |
131
+ | `domains` | ✅ Yes | Array of domain names or subdomains to route to this service |
132
+ | `docker` | ❌ No | Docker configuration (see below) |
133
+ | `healthCheck` | ❌ No | Health check configuration (see below) |
134
+ | `environment` | ❌ No | Environment variables as key-value pairs |
135
+
136
+ #### Docker Configuration
137
+
138
+ When deploying Docker containers, use the `docker` object:
139
+
140
+ - **`image`**: Docker image to pull and run (e.g., `nginx:latest`)
141
+ - **`container`**: Name for the Docker container
142
+ - **`port`**: Internal port the container listens on (mapped to the service `port`)
143
+
144
+ #### Health Check Configuration
99
145
 
100
- - **name**: Unique service name
101
- - **port**: Port the service runs on
102
- - **domains**: Array of domain names or subdomains
103
- - **docker** (optional):
104
- - **image**: Docker image to use
105
- - **container**: Container name
106
- - **port**: Container's internal port
107
- - **healthCheck** (optional):
108
- - **path**: Health check endpoint path
109
- - **interval**: Check interval in seconds
110
- - **environment** (optional): Environment variables
146
+ Configure health monitoring for your service:
147
+
148
+ - **`path`**: HTTP endpoint path for health checks (e.g., `/health`)
149
+ - **`interval`**: Time between health checks in seconds (default: 30)
111
150
 
112
151
  ## Commands
113
152
 
114
- ### `deploy init`
153
+ ### `suthep init`
115
154
 
116
- Initialize a new deployment configuration file.
155
+ Initialize a new deployment configuration file with interactive prompts.
117
156
 
118
157
  ```bash
119
- deploy init [-f deploy.yml]
158
+ suthep init [-f suthep.yml]
120
159
  ```
121
160
 
122
- ### `deploy setup`
161
+ **Options:**
162
+ - `-f, --file`: Configuration file path (default: `suthep.yml`)
163
+
164
+ ### `suthep setup`
123
165
 
124
- Setup Nginx and Certbot on the system.
166
+ Install and configure Nginx and Certbot on your system. This command checks for existing installations and sets up any missing components.
125
167
 
126
168
  ```bash
127
- deploy setup [--nginx-only] [--certbot-only]
169
+ suthep setup [--nginx-only] [--certbot-only]
128
170
  ```
129
171
 
130
- ### `deploy deploy`
172
+ **Options:**
173
+ - `--nginx-only`: Only install and configure Nginx
174
+ - `--certbot-only`: Only install and configure Certbot
131
175
 
132
- Deploy a project using the configuration file.
176
+ ### `suthep deploy`
177
+
178
+ Deploy your project using the configuration file. This command will:
179
+ 1. Configure Nginx reverse proxy for all services
180
+ 2. Obtain SSL certificates via Certbot (if enabled)
181
+ 3. Deploy services with zero-downtime strategy
133
182
 
134
183
  ```bash
135
- deploy deploy [-f deploy.yml] [--no-https] [--no-nginx]
184
+ suthep deploy [-f suthep.yml] [--no-https] [--no-nginx]
136
185
  ```
137
186
 
138
- Options:
139
- - `-f, --file`: Configuration file path (default: `deploy.yml`)
140
- - `--no-https`: Skip HTTPS setup
141
- - `--no-nginx`: Skip Nginx configuration
187
+ **Options:**
188
+ - `-f, --file`: Configuration file path (default: `suthep.yml`)
189
+ - `--no-https`: Skip HTTPS/SSL certificate setup
190
+ - `--no-nginx`: Skip Nginx configuration (useful for testing)
142
191
 
143
192
  ## Examples
144
193
 
145
- ### Simple Node.js Service
194
+ ### Example 1: Simple Node.js API Service
195
+
196
+ Deploy a Node.js API service with health checks:
146
197
 
147
198
  ```yaml
199
+ project:
200
+ name: my-api
201
+ version: 1.0.0
202
+
148
203
  services:
149
204
  - name: api
150
205
  port: 3000
@@ -152,9 +207,26 @@ services:
152
207
  - api.example.com
153
208
  healthCheck:
154
209
  path: /health
210
+ interval: 30
211
+ environment:
212
+ NODE_ENV: production
213
+
214
+ nginx:
215
+ configPath: /etc/nginx/sites-available
216
+ reloadCommand: sudo nginx -t && sudo systemctl reload nginx
217
+
218
+ certbot:
219
+ email: admin@example.com
220
+ staging: false
221
+
222
+ deployment:
223
+ strategy: rolling
224
+ healthCheckTimeout: 30000
155
225
  ```
156
226
 
157
- ### Docker Container Service
227
+ ### Example 2: Docker Container Service
228
+
229
+ Deploy a web application using a Docker container:
158
230
 
159
231
  ```yaml
160
232
  services:
@@ -166,9 +238,12 @@ services:
166
238
  port: 80
167
239
  domains:
168
240
  - example.com
241
+ - www.example.com
169
242
  ```
170
243
 
171
- ### Multiple Domains
244
+ ### Example 3: Multiple Domains for Single Service
245
+
246
+ Route multiple domains to the same service:
172
247
 
173
248
  ```yaml
174
249
  services:
@@ -178,9 +253,13 @@ services:
178
253
  - dashboard.example.com
179
254
  - admin.example.com
180
255
  - app.example.com
256
+ healthCheck:
257
+ path: /api/health
181
258
  ```
182
259
 
183
- ### Connect to Existing Docker Container
260
+ ### Example 4: Connect to Existing Docker Container
261
+
262
+ Connect to an already running Docker container:
184
263
 
185
264
  ```yaml
186
265
  services:
@@ -193,21 +272,43 @@ services:
193
272
  - db.example.com
194
273
  ```
195
274
 
275
+ > **Note:** When connecting to an existing container, omit the `image` field. The tool will use the existing container.
276
+
196
277
  ## Requirements
197
278
 
198
- - Node.js 16+
199
- - Nginx (installed via `deploy setup`)
200
- - Certbot (installed via `deploy setup`)
201
- - Docker (optional, for Docker-based services)
202
- - sudo access (for Nginx and Certbot operations)
279
+ ### System Requirements
280
+
281
+ - **Node.js** 16 or higher
282
+ - **Nginx** (installed automatically via `suthep setup`)
283
+ - **Certbot** (installed automatically via `suthep setup`)
284
+ - **Docker** (optional, required only for Docker-based services)
285
+ - **sudo access** (required for Nginx and Certbot operations)
286
+
287
+ ### Permissions
288
+
289
+ Suthep requires sudo privileges to:
290
+ - Install system packages (Nginx, Certbot)
291
+ - Modify Nginx configuration files
292
+ - Reload Nginx service
293
+ - Obtain SSL certificates from Let's Encrypt
294
+
295
+ ## Benefits
296
+
297
+ ### Cost Optimization
298
+
299
+ Suthep helps optimize infrastructure costs by:
300
+
301
+ - **Multi-Service Management** - Run multiple services on a single server efficiently
302
+ - **Automated Configuration** - Eliminates manual Nginx and SSL setup, saving time and reducing errors
303
+ - **Zero-Downtime Deployments** - Rolling deployment strategy ensures continuous service availability
304
+ - **Health Monitoring** - Built-in health checks help maintain service reliability and catch issues early
203
305
 
204
- ## Cost Optimization
306
+ ### Developer Experience
205
307
 
206
- This tool helps save costs on VMs by:
207
- - Efficiently managing multiple services on a single server
208
- - Automatic reverse proxy setup reduces manual configuration
209
- - Zero-downtime deployments reduce service interruptions
210
- - Health checks ensure service reliability
308
+ - **Simple Configuration** - YAML-based configuration is easy to understand and maintain
309
+ - **One Command Deployment** - Deploy entire infrastructure with a single command
310
+ - **Automatic SSL** - HTTPS certificates are obtained and renewed automatically
311
+ - **Docker Integration** - Seamless support for containerized applications
211
312
 
212
313
  ## License
213
314
 
@@ -1,11 +1,10 @@
1
1
  import chalk from "chalk";
2
2
  import fs from "fs-extra";
3
- import path from "path";
4
- import { requestCertificate } from "../utils/certbot.js";
3
+ import { certificateExists, requestCertificate } from "../utils/certbot.js";
5
4
  import { loadConfig } from "../utils/config-loader.js";
6
5
  import { deployService, performHealthCheck } from "../utils/deployment.js";
7
- import { startDockerContainer } from "../utils/docker.js";
8
- import { generateNginxConfig, enableSite, reloadNginx } from "../utils/nginx.js";
6
+ import { startDockerContainerZeroDowntime, startDockerContainer, swapContainersForZeroDowntime, cleanupTempContainer } from "../utils/docker.js";
7
+ import { generateNginxConfig, generateMultiServiceNginxConfig, writeNginxConfig, enableSite, reloadNginx } from "../utils/nginx.js";
9
8
  async function deployCommand(options) {
10
9
  console.log(chalk.blue.bold("\n🚀 Deploying Services\n"));
11
10
  try {
@@ -17,51 +16,48 @@ async function deployCommand(options) {
17
16
  console.log(chalk.green(`✅ Configuration loaded for project: ${config.project.name}`));
18
17
  console.log(chalk.dim(` Services: ${config.services.map((s) => s.name).join(", ")}
19
18
  `));
19
+ const domainToServices = /* @__PURE__ */ new Map();
20
+ const allDomains = /* @__PURE__ */ new Set();
21
+ for (const service of config.services) {
22
+ for (const domain of service.domains) {
23
+ allDomains.add(domain);
24
+ if (!domainToServices.has(domain)) {
25
+ domainToServices.set(domain, []);
26
+ }
27
+ domainToServices.get(domain).push(service);
28
+ }
29
+ }
30
+ const serviceTempInfo = /* @__PURE__ */ new Map();
20
31
  for (const service of config.services) {
21
32
  console.log(chalk.cyan(`
22
33
  📦 Deploying service: ${service.name}`));
23
34
  try {
24
35
  if (service.docker) {
25
36
  console.log(chalk.dim(" 🐳 Managing Docker container..."));
26
- await startDockerContainer(service);
27
- }
28
- await deployService(service, config.deployment);
29
- if (options.nginx) {
30
- console.log(chalk.dim(" ⚙️ Configuring Nginx reverse proxy..."));
31
- const nginxConfigContent = generateNginxConfig(service, false);
32
- const nginxConfigPath = path.join(config.nginx.configPath, `${service.name}.conf`);
33
- await fs.writeFile(nginxConfigPath, nginxConfigContent);
34
- await enableSite(service.name, config.nginx.configPath);
35
- console.log(chalk.green(` ✅ Nginx configured for ${service.domains.join(", ")}`));
36
- }
37
- if (options.https && service.domains.length > 0) {
38
- console.log(chalk.dim(" 🔐 Setting up HTTPS certificates..."));
39
- for (const domain of service.domains) {
40
- try {
41
- await requestCertificate(domain, config.certbot.email, config.certbot.staging);
42
- console.log(chalk.green(` ✅ SSL certificate obtained for ${domain}`));
43
- } catch (error) {
37
+ if (config.deployment.strategy === "blue-green" || config.deployment.strategy === "rolling") {
38
+ const tempInfo2 = await startDockerContainerZeroDowntime(service);
39
+ serviceTempInfo.set(service.name, tempInfo2);
40
+ if (tempInfo2 && tempInfo2.oldContainerExists) {
44
41
  console.log(
45
- chalk.yellow(
46
- ` ⚠️ Failed to obtain SSL for ${domain}: ${error instanceof Error ? error.message : error}`
42
+ chalk.cyan(
43
+ ` 🔄 Zero-downtime deployment: new container on port ${tempInfo2.tempPort}`
47
44
  )
48
45
  );
49
46
  }
47
+ } else {
48
+ await startDockerContainer(service);
49
+ serviceTempInfo.set(service.name, null);
50
50
  }
51
- if (options.nginx) {
52
- const nginxConfigContent = generateNginxConfig(service, true);
53
- const nginxConfigPath = path.join(config.nginx.configPath, `${service.name}.conf`);
54
- await fs.writeFile(nginxConfigPath, nginxConfigContent);
55
- }
56
- }
57
- if (options.nginx) {
58
- console.log(chalk.dim(" 🔄 Reloading Nginx..."));
59
- await reloadNginx(config.nginx.reloadCommand);
51
+ } else {
52
+ serviceTempInfo.set(service.name, null);
60
53
  }
54
+ const tempInfo = serviceTempInfo.get(service.name) || null;
55
+ await deployService(service, config.deployment, tempInfo);
61
56
  if (service.healthCheck) {
62
57
  console.log(chalk.dim(` 🏥 Performing health check...`));
58
+ const checkPort = tempInfo && tempInfo.oldContainerExists ? tempInfo.tempPort : service.port;
63
59
  const isHealthy = await performHealthCheck(
64
- `http://localhost:${service.port}${service.healthCheck.path}`,
60
+ `http://localhost:${checkPort}${service.healthCheck.path}`,
65
61
  config.deployment.healthCheckTimeout
66
62
  );
67
63
  if (isHealthy) {
@@ -70,8 +66,7 @@ async function deployCommand(options) {
70
66
  throw new Error(`Health check failed for service ${service.name}`);
71
67
  }
72
68
  }
73
- console.log(chalk.green.bold(`
74
- ✨ Service ${service.name} deployed successfully!`));
69
+ console.log(chalk.green.bold(`✨ Service ${service.name} deployed successfully!`));
75
70
  } catch (error) {
76
71
  console.error(
77
72
  chalk.red(`
@@ -81,12 +76,231 @@ async function deployCommand(options) {
81
76
  throw error;
82
77
  }
83
78
  }
79
+ const generateNginxConfigsForDomain = (domain, withHttps, portOverrides) => {
80
+ const servicesForDomain = domainToServices.get(domain);
81
+ if (servicesForDomain.length === 1) {
82
+ const service = servicesForDomain[0];
83
+ const portOverride = portOverrides?.get(service.name);
84
+ return generateNginxConfig(service, withHttps, portOverride);
85
+ } else {
86
+ return generateMultiServiceNginxConfig(servicesForDomain, domain, withHttps, portOverrides);
87
+ }
88
+ };
89
+ const needsZeroDowntimeNginx = Array.from(serviceTempInfo.values()).some(
90
+ (info) => info !== null && info.oldContainerExists
91
+ );
92
+ if (options.nginx) {
93
+ if (needsZeroDowntimeNginx) {
94
+ console.log(chalk.cyan(`
95
+ ⚙️ Updating Nginx for zero-downtime deployment...`));
96
+ const tempPortOverrides = /* @__PURE__ */ new Map();
97
+ for (const service of config.services) {
98
+ const tempInfo = serviceTempInfo.get(service.name);
99
+ if (tempInfo && tempInfo.oldContainerExists) {
100
+ tempPortOverrides.set(service.name, tempInfo.tempPort);
101
+ }
102
+ }
103
+ for (const domain of allDomains) {
104
+ const configName = domain.replace(/\./g, "_");
105
+ try {
106
+ const nginxConfigContent = generateNginxConfigsForDomain(
107
+ domain,
108
+ false,
109
+ tempPortOverrides
110
+ );
111
+ await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent);
112
+ await enableSite(configName, config.nginx.configPath);
113
+ console.log(chalk.green(` ✅ Nginx updated for ${domain} (temporary ports)`));
114
+ } catch (error) {
115
+ console.error(
116
+ chalk.red(` ❌ Failed to update Nginx for ${domain}:`),
117
+ error instanceof Error ? error.message : error
118
+ );
119
+ throw error;
120
+ }
121
+ }
122
+ console.log(chalk.cyan(`
123
+ 🔄 Reloading Nginx to switch to new containers...`));
124
+ await reloadNginx(config.nginx.reloadCommand);
125
+ console.log(chalk.green(` ✅ Nginx reloaded, traffic now routed to new containers`));
126
+ console.log(chalk.cyan(`
127
+ 🔄 Swapping containers for zero-downtime...`));
128
+ for (const service of config.services) {
129
+ const tempInfo = serviceTempInfo.get(service.name);
130
+ if (tempInfo && tempInfo.oldContainerExists && service.docker) {
131
+ try {
132
+ await swapContainersForZeroDowntime(service, tempInfo);
133
+ console.log(chalk.green(` ✅ Container swapped for ${service.name}`));
134
+ } catch (error) {
135
+ console.error(
136
+ chalk.red(` ❌ Failed to swap container for ${service.name}:`),
137
+ error instanceof Error ? error.message : error
138
+ );
139
+ throw error;
140
+ }
141
+ }
142
+ }
143
+ console.log(chalk.cyan(`
144
+ ⚙️ Updating Nginx back to production ports...`));
145
+ for (const domain of allDomains) {
146
+ const configName = domain.replace(/\./g, "_");
147
+ try {
148
+ const nginxConfigContent = generateNginxConfigsForDomain(domain, false);
149
+ await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent);
150
+ await enableSite(configName, config.nginx.configPath);
151
+ console.log(chalk.green(` ✅ Nginx updated for ${domain} (production ports)`));
152
+ } catch (error) {
153
+ console.error(
154
+ chalk.red(` ❌ Failed to update Nginx for ${domain}:`),
155
+ error instanceof Error ? error.message : error
156
+ );
157
+ throw error;
158
+ }
159
+ }
160
+ console.log(chalk.cyan(`
161
+ 🔄 Reloading Nginx to switch to production ports...`));
162
+ await reloadNginx(config.nginx.reloadCommand);
163
+ console.log(chalk.green(` ✅ Nginx reloaded, traffic now routed to production containers`));
164
+ console.log(chalk.cyan(`
165
+ 🧹 Cleaning up temporary containers...`));
166
+ for (const service of config.services) {
167
+ const tempInfo = serviceTempInfo.get(service.name);
168
+ if (tempInfo && tempInfo.oldContainerExists) {
169
+ await cleanupTempContainer(tempInfo.tempContainerName);
170
+ }
171
+ }
172
+ } else {
173
+ console.log(chalk.cyan(`
174
+ ⚙️ Configuring Nginx reverse proxy...`));
175
+ for (const domain of allDomains) {
176
+ const servicesForDomain = domainToServices.get(domain);
177
+ const configName = domain.replace(/\./g, "_");
178
+ try {
179
+ if (servicesForDomain.length > 1) {
180
+ console.log(
181
+ chalk.cyan(
182
+ ` 📋 Configuring ${domain} with ${servicesForDomain.length} services: ${servicesForDomain.map((s) => s.name).join(", ")}`
183
+ )
184
+ );
185
+ console.log(
186
+ chalk.dim(
187
+ ` All services will share the same nginx config file: ${configName}.conf`
188
+ )
189
+ );
190
+ }
191
+ const nginxConfigContent = generateNginxConfigsForDomain(domain, false);
192
+ const wasOverridden = await writeNginxConfig(
193
+ configName,
194
+ config.nginx.configPath,
195
+ nginxConfigContent
196
+ );
197
+ if (wasOverridden) {
198
+ console.log(
199
+ chalk.yellow(
200
+ ` 🔄 Nginx config "${configName}.conf" already exists, deleting and recreating with new configuration...`
201
+ )
202
+ );
203
+ }
204
+ await enableSite(configName, config.nginx.configPath);
205
+ console.log(chalk.green(` ✅ Nginx configured for ${domain}`));
206
+ } catch (error) {
207
+ console.error(
208
+ chalk.red(` ❌ Failed to configure Nginx for ${domain}:`),
209
+ error instanceof Error ? error.message : error
210
+ );
211
+ throw error;
212
+ }
213
+ }
214
+ }
215
+ }
216
+ if (options.https && allDomains.size > 0) {
217
+ console.log(chalk.cyan(`
218
+ 🔐 Setting up HTTPS certificates...`));
219
+ for (const domain of allDomains) {
220
+ try {
221
+ const exists = await certificateExists(domain);
222
+ if (exists) {
223
+ console.log(
224
+ chalk.green(
225
+ ` ✅ SSL certificate already exists for ${domain}, skipping certificate creation`
226
+ )
227
+ );
228
+ console.log(
229
+ chalk.dim(` Using existing certificate from /etc/letsencrypt/live/${domain}/`)
230
+ );
231
+ } else {
232
+ console.log(chalk.cyan(` 📜 Requesting SSL certificate for ${domain}...`));
233
+ try {
234
+ await requestCertificate(domain, config.certbot.email, config.certbot.staging);
235
+ console.log(chalk.green(` ✅ SSL certificate obtained for ${domain}`));
236
+ } catch (error) {
237
+ const errorMessage = error?.message || String(error) || "";
238
+ if (errorMessage.includes("already exists") || errorMessage.includes("Skipping certificate creation")) {
239
+ console.log(
240
+ chalk.green(
241
+ ` ✅ SSL certificate already exists for ${domain} (detected during request), skipping...`
242
+ )
243
+ );
244
+ } else {
245
+ throw error;
246
+ }
247
+ }
248
+ }
249
+ } catch (error) {
250
+ console.log(
251
+ chalk.yellow(
252
+ ` ⚠️ Failed to obtain SSL for ${domain}: ${error instanceof Error ? error.message : error}`
253
+ )
254
+ );
255
+ }
256
+ }
257
+ if (options.nginx) {
258
+ console.log(chalk.cyan(`
259
+ 🔄 Updating Nginx configs with HTTPS...`));
260
+ for (const domain of allDomains) {
261
+ const configName = domain.replace(/\./g, "_");
262
+ try {
263
+ const nginxConfigContent = generateNginxConfigsForDomain(domain, true);
264
+ const wasOverridden = await writeNginxConfig(
265
+ configName,
266
+ config.nginx.configPath,
267
+ nginxConfigContent
268
+ );
269
+ if (wasOverridden) {
270
+ console.log(
271
+ chalk.yellow(
272
+ ` 🔄 Nginx config "${configName}.conf" already exists, deleting and recreating with new HTTPS configuration...`
273
+ )
274
+ );
275
+ }
276
+ console.log(chalk.green(` ✅ HTTPS config updated for ${domain}`));
277
+ } catch (error) {
278
+ console.error(
279
+ chalk.red(` ❌ Failed to update HTTPS config for ${domain}:`),
280
+ error instanceof Error ? error.message : error
281
+ );
282
+ throw error;
283
+ }
284
+ }
285
+ }
286
+ }
287
+ if (options.nginx && !needsZeroDowntimeNginx) {
288
+ console.log(chalk.cyan(`
289
+ 🔄 Reloading Nginx...`));
290
+ await reloadNginx(config.nginx.reloadCommand);
291
+ } else if (options.nginx && needsZeroDowntimeNginx) {
292
+ console.log(chalk.cyan(`
293
+ 🔄 Final Nginx reload with HTTPS...`));
294
+ await reloadNginx(config.nginx.reloadCommand);
295
+ }
84
296
  console.log(chalk.green.bold("\n🎉 All services deployed successfully!\n"));
85
297
  console.log(chalk.cyan("📋 Service URLs:"));
86
298
  for (const service of config.services) {
87
299
  for (const domain of service.domains) {
88
300
  const protocol = options.https ? "https" : "http";
89
- console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}`));
301
+ const servicePath = service.path || "/";
302
+ const fullPath = servicePath === "/" ? "" : servicePath;
303
+ console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}${fullPath}`));
90
304
  }
91
305
  }
92
306
  console.log();