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.
- package/README.md +172 -71
- package/dist/commands/deploy.js +251 -37
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/down.js +179 -0
- package/dist/commands/down.js.map +1 -0
- package/dist/commands/redeploy.js +59 -0
- package/dist/commands/redeploy.js.map +1 -0
- package/dist/commands/up.js +213 -0
- package/dist/commands/up.js.map +1 -0
- package/dist/index.js +36 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/certbot.js +40 -3
- package/dist/utils/certbot.js.map +1 -1
- package/dist/utils/config-loader.js +30 -0
- package/dist/utils/config-loader.js.map +1 -1
- package/dist/utils/deployment.js +49 -16
- package/dist/utils/deployment.js.map +1 -1
- package/dist/utils/docker.js +396 -25
- package/dist/utils/docker.js.map +1 -1
- package/dist/utils/nginx.js +167 -8
- package/dist/utils/nginx.js.map +1 -1
- package/docs/README.md +25 -49
- package/docs/english/01-introduction.md +84 -0
- package/docs/english/02-installation.md +200 -0
- package/docs/english/03-quick-start.md +256 -0
- package/docs/english/04-configuration.md +358 -0
- package/docs/english/05-commands.md +363 -0
- package/docs/english/06-examples.md +456 -0
- package/docs/english/07-troubleshooting.md +417 -0
- package/docs/english/08-advanced.md +411 -0
- package/docs/english/README.md +48 -0
- package/docs/thai/01-introduction.md +84 -0
- package/docs/thai/02-installation.md +200 -0
- package/docs/thai/03-quick-start.md +256 -0
- package/docs/thai/04-configuration.md +358 -0
- package/docs/thai/05-commands.md +363 -0
- package/docs/thai/06-examples.md +456 -0
- package/docs/thai/07-troubleshooting.md +417 -0
- package/docs/thai/08-advanced.md +411 -0
- package/docs/thai/README.md +48 -0
- package/example/README.md +286 -53
- package/example/suthep-complete.yml +103 -0
- package/example/suthep-docker-only.yml +71 -0
- package/example/suthep-no-docker.yml +51 -0
- package/example/suthep-path-routing.yml +62 -0
- package/example/suthep.example.yml +89 -0
- package/package.json +1 -1
- package/src/commands/deploy.ts +322 -50
- package/src/commands/down.ts +240 -0
- package/src/commands/redeploy.ts +78 -0
- package/src/commands/up.ts +271 -0
- package/src/index.ts +62 -1
- package/src/types/config.ts +25 -24
- package/src/utils/certbot.ts +68 -6
- package/src/utils/config-loader.ts +40 -0
- package/src/utils/deployment.ts +61 -36
- package/src/utils/docker.ts +634 -30
- package/src/utils/nginx.ts +187 -4
- package/suthep-0.1.0-beta.1.tgz +0 -0
- package/suthep-0.1.1.tgz +0 -0
- package/suthep.example.yml +34 -0
- package/suthep.yml +39 -0
- package/test +0 -0
- package/docs/api-reference.md +0 -545
- package/docs/architecture.md +0 -367
- package/docs/commands.md +0 -273
- package/docs/configuration.md +0 -347
- package/docs/examples.md +0 -537
- package/docs/getting-started.md +0 -197
- package/docs/troubleshooting.md +0 -441
- package/example/docker-compose.yml +0 -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
|
|
8
|
-
- ✅ Automatic HTTPS with Certbot
|
|
9
|
-
- ✅ Zero-
|
|
10
|
-
- ✅ Docker
|
|
11
|
-
- ✅ Multiple
|
|
12
|
-
- ✅ Health check
|
|
13
|
-
- ✅ YAML-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
48
|
+
suthep init
|
|
26
49
|
```
|
|
27
50
|
|
|
28
|
-
|
|
51
|
+
This will create a `suthep.yml` file with interactive prompts, or you can use an example configuration:
|
|
29
52
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```
|
|
62
|
+
```bash
|
|
63
|
+
suthep setup
|
|
64
|
+
```
|
|
46
65
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 (`
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
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
|
-
### `
|
|
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
|
-
|
|
158
|
+
suthep init [-f suthep.yml]
|
|
120
159
|
```
|
|
121
160
|
|
|
122
|
-
|
|
161
|
+
**Options:**
|
|
162
|
+
- `-f, --file`: Configuration file path (default: `suthep.yml`)
|
|
163
|
+
|
|
164
|
+
### `suthep setup`
|
|
123
165
|
|
|
124
|
-
|
|
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
|
-
|
|
169
|
+
suthep setup [--nginx-only] [--certbot-only]
|
|
128
170
|
```
|
|
129
171
|
|
|
130
|
-
|
|
172
|
+
**Options:**
|
|
173
|
+
- `--nginx-only`: Only install and configure Nginx
|
|
174
|
+
- `--certbot-only`: Only install and configure Certbot
|
|
131
175
|
|
|
132
|
-
|
|
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
|
-
|
|
184
|
+
suthep deploy [-f suthep.yml] [--no-https] [--no-nginx]
|
|
136
185
|
```
|
|
137
186
|
|
|
138
|
-
Options
|
|
139
|
-
- `-f, --file`: Configuration file path (default: `
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
-
|
|
201
|
-
-
|
|
202
|
-
-
|
|
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
|
-
|
|
306
|
+
### Developer Experience
|
|
205
307
|
|
|
206
|
-
|
|
207
|
-
-
|
|
208
|
-
- Automatic
|
|
209
|
-
-
|
|
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
|
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
|
-
import
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
46
|
-
`
|
|
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
|
-
|
|
52
|
-
|
|
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:${
|
|
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
|
-
|
|
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();
|