portok 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +10 -0
- package/Dockerfile +41 -0
- package/README.md +606 -0
- package/bench/baseline.bench.mjs +73 -0
- package/bench/connections.bench.mjs +70 -0
- package/bench/keepalive.bench.mjs +248 -0
- package/bench/latency.bench.mjs +47 -0
- package/bench/run.mjs +211 -0
- package/bench/switching.bench.mjs +96 -0
- package/bench/throughput.bench.mjs +44 -0
- package/bench/validate.mjs +260 -0
- package/docker-compose.yml +62 -0
- package/examples/api.env +30 -0
- package/examples/web.env +27 -0
- package/package.json +39 -0
- package/portok.mjs +793 -0
- package/portok@.service +62 -0
- package/portokd.mjs +793 -0
- package/test/cli.test.mjs +220 -0
- package/test/drain.test.mjs +249 -0
- package/test/helpers/mock-server.mjs +305 -0
- package/test/metrics.test.mjs +328 -0
- package/test/proxy.test.mjs +223 -0
- package/test/rollback.test.mjs +344 -0
- package/test/security.test.mjs +256 -0
- package/test/switching.test.mjs +261 -0
package/.dockerignore
ADDED
package/Dockerfile
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Portok Test & Benchmark Environment
|
|
2
|
+
# Multi-stage build for development and testing on Linux
|
|
3
|
+
|
|
4
|
+
FROM node:20-alpine AS base
|
|
5
|
+
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
# Install dependencies
|
|
9
|
+
COPY package*.json ./
|
|
10
|
+
RUN npm ci
|
|
11
|
+
|
|
12
|
+
# Copy source files
|
|
13
|
+
COPY . .
|
|
14
|
+
|
|
15
|
+
# Make CLI executable
|
|
16
|
+
RUN chmod +x portok.mjs portokd.mjs
|
|
17
|
+
|
|
18
|
+
# Default command runs tests
|
|
19
|
+
CMD ["npm", "test"]
|
|
20
|
+
|
|
21
|
+
# =============================================================================
|
|
22
|
+
# Test stage
|
|
23
|
+
# =============================================================================
|
|
24
|
+
FROM base AS test
|
|
25
|
+
ENV NODE_ENV=test
|
|
26
|
+
CMD ["npm", "test"]
|
|
27
|
+
|
|
28
|
+
# =============================================================================
|
|
29
|
+
# Benchmark stage
|
|
30
|
+
# =============================================================================
|
|
31
|
+
FROM base AS bench
|
|
32
|
+
ENV NODE_ENV=production
|
|
33
|
+
CMD ["npm", "run", "bench"]
|
|
34
|
+
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# Development stage (with shell access)
|
|
37
|
+
# =============================================================================
|
|
38
|
+
FROM base AS dev
|
|
39
|
+
RUN apk add --no-cache bash curl
|
|
40
|
+
CMD ["/bin/bash"]
|
|
41
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
# Portok
|
|
2
|
+
|
|
3
|
+
A lightweight Node.js "switchboard" proxy that enables zero-downtime deployments by routing a stable public port to an internal app instance running on a random port, switching only when the new instance is healthy.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Zero-downtime switching**: Health-gated port switching with connection draining
|
|
8
|
+
- **Auto-rollback**: Automatic rollback if the new port becomes unhealthy
|
|
9
|
+
- **WebSocket support**: Full HTTP and WebSocket proxying
|
|
10
|
+
- **Lightweight metrics**: Built-in metrics without heavy dependencies
|
|
11
|
+
- **Security**: Token-based auth, IP allowlist, rate limiting
|
|
12
|
+
- **CLI**: Easy-to-use command-line interface
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Start the Daemon
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Required environment variables
|
|
26
|
+
export LISTEN_PORT=3000
|
|
27
|
+
export INITIAL_TARGET_PORT=8080
|
|
28
|
+
export ADMIN_TOKEN=your-secret-token
|
|
29
|
+
|
|
30
|
+
# Start portokd
|
|
31
|
+
node portokd.mjs
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Use the CLI
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Check status
|
|
38
|
+
./portok.mjs status --token your-secret-token
|
|
39
|
+
|
|
40
|
+
# Switch to new port
|
|
41
|
+
./portok.mjs switch 8081 --token your-secret-token
|
|
42
|
+
|
|
43
|
+
# Check metrics
|
|
44
|
+
./portok.mjs metrics --token your-secret-token
|
|
45
|
+
|
|
46
|
+
# Check health
|
|
47
|
+
./portok.mjs health --token your-secret-token
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
All configuration is via environment variables:
|
|
53
|
+
|
|
54
|
+
| Variable | Default | Description |
|
|
55
|
+
|----------|---------|-------------|
|
|
56
|
+
| `INSTANCE_NAME` | `default` | Instance identifier (for logging/state file naming) |
|
|
57
|
+
| `LISTEN_PORT` | `3000` | Port the proxy listens on |
|
|
58
|
+
| `INITIAL_TARGET_PORT` | (required) | Initial backend port to proxy to |
|
|
59
|
+
| `STATE_FILE` | `/var/lib/portok/<instance>.json` | Path to persist state |
|
|
60
|
+
| `HEALTH_PATH` | `/health` | Health check endpoint path |
|
|
61
|
+
| `HEALTH_TIMEOUT_MS` | `5000` | Health check timeout |
|
|
62
|
+
| `DRAIN_MS` | `30000` | Connection drain period after switch |
|
|
63
|
+
| `ROLLBACK_WINDOW_MS` | `60000` | Time window for auto-rollback monitoring |
|
|
64
|
+
| `ROLLBACK_CHECK_EVERY_MS` | `5000` | Health check interval during rollback window |
|
|
65
|
+
| `ROLLBACK_FAIL_THRESHOLD` | `3` | Consecutive failures before rollback |
|
|
66
|
+
| `ADMIN_TOKEN` | (required) | Token for admin endpoint authentication |
|
|
67
|
+
| `ADMIN_ALLOWLIST` | `127.0.0.1,::1` | Comma-separated list of allowed IPs |
|
|
68
|
+
| `ADMIN_UNIX_SOCKET` | (optional) | Unix socket path for admin endpoints |
|
|
69
|
+
|
|
70
|
+
### Performance Tuning
|
|
71
|
+
|
|
72
|
+
| Variable | Default | Description |
|
|
73
|
+
|----------|---------|-------------|
|
|
74
|
+
| `FAST_PATH` | `0` | Enable minimal metrics mode for maximum throughput |
|
|
75
|
+
| `UPSTREAM_KEEPALIVE` | `1` | Enable keep-alive for upstream connections (critical) |
|
|
76
|
+
| `UPSTREAM_MAX_SOCKETS` | `1024` | Maximum sockets per upstream host |
|
|
77
|
+
| `UPSTREAM_KEEPALIVE_MSECS` | `1000` | Keep-alive ping interval in ms |
|
|
78
|
+
| `SERVER_KEEPALIVE_TIMEOUT` | `5000` | Server keep-alive timeout in ms |
|
|
79
|
+
| `SERVER_HEADERS_TIMEOUT` | `6000` | Headers timeout (must be > keepAliveTimeout) |
|
|
80
|
+
| `ENABLE_XFWD` | `1` | Add X-Forwarded-* headers to proxied requests |
|
|
81
|
+
| `DEBUG_UPSTREAM` | `0` | Track upstream socket creation in /__metrics |
|
|
82
|
+
| `VERBOSE_ERRORS` | `0` | Log full error stacks (disable in production) |
|
|
83
|
+
|
|
84
|
+
## Admin Endpoints
|
|
85
|
+
|
|
86
|
+
All admin endpoints require the `x-admin-token` header.
|
|
87
|
+
|
|
88
|
+
### GET /__status
|
|
89
|
+
|
|
90
|
+
Returns current proxy status.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
curl -H "x-admin-token: your-token" http://127.0.0.1:3000/__status
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Response:
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"activePort": 8080,
|
|
100
|
+
"drainUntil": null,
|
|
101
|
+
"lastSwitch": {
|
|
102
|
+
"from": 8081,
|
|
103
|
+
"to": 8080,
|
|
104
|
+
"at": "2024-01-15T10:30:00.000Z",
|
|
105
|
+
"reason": "manual",
|
|
106
|
+
"id": "uuid-here"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### GET /__metrics
|
|
112
|
+
|
|
113
|
+
Returns proxy metrics.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
curl -H "x-admin-token: your-token" http://127.0.0.1:3000/__metrics
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Response:
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"startedAt": "2024-01-15T10:00:00.000Z",
|
|
123
|
+
"inflight": 5,
|
|
124
|
+
"inflightMax": 100,
|
|
125
|
+
"totalRequests": 50000,
|
|
126
|
+
"totalProxyErrors": 2,
|
|
127
|
+
"statusCounters": {
|
|
128
|
+
"2xx": 49500,
|
|
129
|
+
"3xx": 100,
|
|
130
|
+
"4xx": 398,
|
|
131
|
+
"5xx": 2
|
|
132
|
+
},
|
|
133
|
+
"rollingRps60": 125.5,
|
|
134
|
+
"health": {
|
|
135
|
+
"activePortOk": true,
|
|
136
|
+
"lastCheckedAt": "2024-01-15T10:29:55.000Z",
|
|
137
|
+
"consecutiveFails": 0
|
|
138
|
+
},
|
|
139
|
+
"lastProxyError": null
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### POST /__switch?port=PORT
|
|
144
|
+
|
|
145
|
+
Switch to a new target port. Performs health check before switching.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
curl -X POST -H "x-admin-token: your-token" \
|
|
149
|
+
"http://127.0.0.1:3000/__switch?port=8081"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Success response (200):
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"success": true,
|
|
156
|
+
"message": "Switched to port 8081",
|
|
157
|
+
"switch": {
|
|
158
|
+
"from": 8080,
|
|
159
|
+
"to": 8081,
|
|
160
|
+
"at": "2024-01-15T10:30:00.000Z",
|
|
161
|
+
"reason": "manual",
|
|
162
|
+
"id": "uuid-here"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Failure response (409 - health check failed):
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"error": "Health check failed",
|
|
171
|
+
"message": "Port 8081 did not respond with 2xx at /health"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### GET /__health
|
|
176
|
+
|
|
177
|
+
Check health of the current active port.
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
curl -H "x-admin-token: your-token" http://127.0.0.1:3000/__health
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## CLI Reference
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
portok <command> [options]
|
|
187
|
+
|
|
188
|
+
Management Commands:
|
|
189
|
+
init Initialize portok directories (/etc/portok, /var/lib/portok)
|
|
190
|
+
add <name> Create a new service instance
|
|
191
|
+
list List all configured instances and their status
|
|
192
|
+
|
|
193
|
+
Operational Commands:
|
|
194
|
+
status Show current proxy status
|
|
195
|
+
metrics Show proxy metrics
|
|
196
|
+
switch <port> Switch to a new target port
|
|
197
|
+
health Check health of active port
|
|
198
|
+
|
|
199
|
+
Options:
|
|
200
|
+
--url <url> Daemon URL (default: http://127.0.0.1:3000)
|
|
201
|
+
--instance <name> Target instance by name (reads /etc/portok/<name>.env)
|
|
202
|
+
--token <token> Admin token (or PORTOK_TOKEN env var)
|
|
203
|
+
--json Output as JSON
|
|
204
|
+
--help Show help
|
|
205
|
+
|
|
206
|
+
Options for 'add' command:
|
|
207
|
+
--port <port> Listen port (default: random 3000-3999)
|
|
208
|
+
--target <port> Target port (default: random 8000-8999)
|
|
209
|
+
--health <path> Health check path (default: /health)
|
|
210
|
+
--force Overwrite existing config
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Quick Start with CLI
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
# 1. Initialize portok (creates /etc/portok and /var/lib/portok)
|
|
217
|
+
sudo portok init
|
|
218
|
+
|
|
219
|
+
# 2. Create a new service
|
|
220
|
+
sudo portok add api --port 3001 --target 8001
|
|
221
|
+
|
|
222
|
+
# 3. Start the service
|
|
223
|
+
sudo systemctl start portok@api
|
|
224
|
+
|
|
225
|
+
# 4. Check status
|
|
226
|
+
portok status --instance api
|
|
227
|
+
|
|
228
|
+
# 5. List all services
|
|
229
|
+
portok list
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Environment Variables for CLI
|
|
233
|
+
|
|
234
|
+
- `PORTOK_URL`: Default daemon URL
|
|
235
|
+
- `PORTOK_TOKEN`: Admin token
|
|
236
|
+
|
|
237
|
+
### Examples
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
# Initialize portok (run once)
|
|
241
|
+
sudo portok init
|
|
242
|
+
|
|
243
|
+
# Create services with specific ports
|
|
244
|
+
sudo portok add api --port 3001 --target 8001
|
|
245
|
+
sudo portok add web --port 3002 --target 8002
|
|
246
|
+
|
|
247
|
+
# List all instances with status
|
|
248
|
+
portok list
|
|
249
|
+
|
|
250
|
+
# Check status by instance name
|
|
251
|
+
portok status --instance api
|
|
252
|
+
|
|
253
|
+
# Get metrics as JSON
|
|
254
|
+
portok metrics --instance api --json
|
|
255
|
+
|
|
256
|
+
# Switch to new port
|
|
257
|
+
portok switch 8081 --instance api
|
|
258
|
+
|
|
259
|
+
# Health check (exits 0 if healthy, 1 if unhealthy)
|
|
260
|
+
portok health --instance api && echo "OK" || echo "FAIL"
|
|
261
|
+
|
|
262
|
+
# Direct URL mode (without instance)
|
|
263
|
+
portok status --url http://127.0.0.1:3000 --token your-token
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## systemd Service
|
|
267
|
+
|
|
268
|
+
Example systemd unit file (`/etc/systemd/system/portokd.service`):
|
|
269
|
+
|
|
270
|
+
```ini
|
|
271
|
+
[Unit]
|
|
272
|
+
Description=Portok Zero-Downtime Proxy
|
|
273
|
+
After=network.target
|
|
274
|
+
|
|
275
|
+
[Service]
|
|
276
|
+
Type=simple
|
|
277
|
+
User=www-data
|
|
278
|
+
WorkingDirectory=/opt/portok
|
|
279
|
+
ExecStart=/usr/bin/node /opt/portok/portokd.mjs
|
|
280
|
+
Restart=always
|
|
281
|
+
RestartSec=5
|
|
282
|
+
|
|
283
|
+
# Environment
|
|
284
|
+
Environment=LISTEN_PORT=3000
|
|
285
|
+
Environment=INITIAL_TARGET_PORT=8080
|
|
286
|
+
Environment=STATE_FILE=/var/lib/portok/state.json
|
|
287
|
+
Environment=ADMIN_TOKEN=your-secret-token
|
|
288
|
+
Environment=HEALTH_PATH=/health
|
|
289
|
+
Environment=DRAIN_MS=30000
|
|
290
|
+
Environment=ROLLBACK_WINDOW_MS=60000
|
|
291
|
+
Environment=ROLLBACK_CHECK_EVERY_MS=5000
|
|
292
|
+
Environment=ROLLBACK_FAIL_THRESHOLD=3
|
|
293
|
+
|
|
294
|
+
# Security hardening
|
|
295
|
+
NoNewPrivileges=true
|
|
296
|
+
ProtectSystem=strict
|
|
297
|
+
ProtectHome=true
|
|
298
|
+
ReadWritePaths=/var/lib/portok
|
|
299
|
+
|
|
300
|
+
[Install]
|
|
301
|
+
WantedBy=multi-user.target
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Enable and start:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
sudo systemctl daemon-reload
|
|
308
|
+
sudo systemctl enable portokd
|
|
309
|
+
sudo systemctl start portokd
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Testing
|
|
313
|
+
|
|
314
|
+
Tests run in Docker for Linux compatibility:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
# Run all tests
|
|
318
|
+
docker compose run --rm test
|
|
319
|
+
|
|
320
|
+
# Run specific test file
|
|
321
|
+
docker compose run --rm test npm test -- --test-name-pattern="proxy"
|
|
322
|
+
|
|
323
|
+
# Development shell
|
|
324
|
+
docker compose run --rm dev
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Or run locally (requires Node.js 20+):
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
npm test
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Benchmarks
|
|
334
|
+
|
|
335
|
+
Benchmarks measure proxy performance:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
# Run all benchmarks in Docker
|
|
339
|
+
docker compose run --rm bench
|
|
340
|
+
|
|
341
|
+
# Quick benchmark (shorter duration)
|
|
342
|
+
docker compose run --rm bench npm run bench -- --quick
|
|
343
|
+
|
|
344
|
+
# Output JSON for CI
|
|
345
|
+
docker compose run --rm bench npm run bench -- --json > results.json
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Benchmark scenarios:
|
|
349
|
+
|
|
350
|
+
| Benchmark | Description |
|
|
351
|
+
|-----------|-------------|
|
|
352
|
+
| **Throughput** | Maximum requests/sec with 100 connections |
|
|
353
|
+
| **Latency** | Latency percentiles (p50, p95, p99) |
|
|
354
|
+
| **Connections** | Scaling with 10-500 concurrent connections |
|
|
355
|
+
| **Switching** | Switch latency and request loss |
|
|
356
|
+
| **Baseline** | Direct vs proxied overhead comparison |
|
|
357
|
+
| **Keep-Alive** | Validates keep-alive performance (RPS >= 70% of direct) |
|
|
358
|
+
|
|
359
|
+
## Multi-Instance Setup
|
|
360
|
+
|
|
361
|
+
Portok supports running multiple isolated instances on the same host, each managing a different application. This is the recommended approach for multi-app deployments.
|
|
362
|
+
|
|
363
|
+
### Directory Structure
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
/etc/portok/
|
|
367
|
+
├── api.env # Config for "api" instance
|
|
368
|
+
├── web.env # Config for "web" instance
|
|
369
|
+
└── worker.env # Config for "worker" instance
|
|
370
|
+
|
|
371
|
+
/var/lib/portok/
|
|
372
|
+
├── api.json # State file for "api" instance
|
|
373
|
+
├── web.json # State file for "web" instance
|
|
374
|
+
└── worker.json # State file for "worker" instance
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Instance Configuration
|
|
378
|
+
|
|
379
|
+
Each instance has its own env file at `/etc/portok/<instance>.env`:
|
|
380
|
+
|
|
381
|
+
**Example: `/etc/portok/api.env`**
|
|
382
|
+
```bash
|
|
383
|
+
# Required
|
|
384
|
+
LISTEN_PORT=3001
|
|
385
|
+
INITIAL_TARGET_PORT=8001
|
|
386
|
+
ADMIN_TOKEN=api-secret-token-change-me
|
|
387
|
+
|
|
388
|
+
# Optional (defaults shown)
|
|
389
|
+
HEALTH_PATH=/health
|
|
390
|
+
HEALTH_TIMEOUT_MS=5000
|
|
391
|
+
DRAIN_MS=30000
|
|
392
|
+
ROLLBACK_WINDOW_MS=60000
|
|
393
|
+
ROLLBACK_CHECK_EVERY_MS=5000
|
|
394
|
+
ROLLBACK_FAIL_THRESHOLD=3
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Example: `/etc/portok/web.env`**
|
|
398
|
+
```bash
|
|
399
|
+
LISTEN_PORT=3002
|
|
400
|
+
INITIAL_TARGET_PORT=8002
|
|
401
|
+
ADMIN_TOKEN=web-secret-token-change-me
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### systemd Template Unit
|
|
405
|
+
|
|
406
|
+
Use the template unit `portok@.service` for managing instances:
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
# Copy the template
|
|
410
|
+
sudo cp portok@.service /etc/systemd/system/
|
|
411
|
+
|
|
412
|
+
# Create config directory and state directory
|
|
413
|
+
sudo mkdir -p /etc/portok /var/lib/portok
|
|
414
|
+
sudo chown www-data:www-data /var/lib/portok
|
|
415
|
+
|
|
416
|
+
# Create instance configs
|
|
417
|
+
sudo cp examples/api.env /etc/portok/api.env
|
|
418
|
+
sudo cp examples/web.env /etc/portok/web.env
|
|
419
|
+
|
|
420
|
+
# Edit tokens and ports
|
|
421
|
+
sudo nano /etc/portok/api.env
|
|
422
|
+
sudo nano /etc/portok/web.env
|
|
423
|
+
|
|
424
|
+
# Reload systemd
|
|
425
|
+
sudo systemctl daemon-reload
|
|
426
|
+
|
|
427
|
+
# Start instances
|
|
428
|
+
sudo systemctl start portok@api
|
|
429
|
+
sudo systemctl start portok@web
|
|
430
|
+
|
|
431
|
+
# Enable at boot
|
|
432
|
+
sudo systemctl enable portok@api
|
|
433
|
+
sudo systemctl enable portok@web
|
|
434
|
+
|
|
435
|
+
# Check status
|
|
436
|
+
sudo systemctl status portok@api
|
|
437
|
+
sudo systemctl status portok@web
|
|
438
|
+
|
|
439
|
+
# View logs
|
|
440
|
+
journalctl -u portok@api -f
|
|
441
|
+
journalctl -u portok@web -f
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### CLI with Multi-Instance
|
|
445
|
+
|
|
446
|
+
Use `--instance` to target a specific instance:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
# Target by instance name (reads /etc/portok/<name>.env)
|
|
450
|
+
portok status --instance api
|
|
451
|
+
portok metrics --instance web
|
|
452
|
+
portok switch 8081 --instance api
|
|
453
|
+
portok health --instance web --json
|
|
454
|
+
|
|
455
|
+
# Explicit URL/token still works (and overrides env file)
|
|
456
|
+
portok status --url http://127.0.0.1:3001 --token api-secret-token
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Instance Isolation
|
|
460
|
+
|
|
461
|
+
Each instance is fully isolated:
|
|
462
|
+
- **Separate state files**: No shared state between instances
|
|
463
|
+
- **Separate tokens**: Each instance has its own admin token
|
|
464
|
+
- **Separate metrics**: Metrics are per-instance
|
|
465
|
+
- **Separate rollback monitors**: Each instance tracks its own rollback window
|
|
466
|
+
- **Separate rate limits**: Rate limiting is per-instance
|
|
467
|
+
|
|
468
|
+
### Multi-Instance Architecture
|
|
469
|
+
|
|
470
|
+
```
|
|
471
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
472
|
+
│ Load Balancer / Nginx │
|
|
473
|
+
└───────────┬─────────────────────────┬───────────────────────┘
|
|
474
|
+
│ │
|
|
475
|
+
▼ ▼
|
|
476
|
+
┌───────────────────────┐ ┌───────────────────────┐
|
|
477
|
+
│ portok@api (:3001) │ │ portok@web (:3002) │
|
|
478
|
+
│ State: api.json │ │ State: web.json │
|
|
479
|
+
└───────────┬───────────┘ └───────────┬───────────┘
|
|
480
|
+
│ │
|
|
481
|
+
▼ ▼
|
|
482
|
+
┌───────────────────────┐ ┌───────────────────────┐
|
|
483
|
+
│ api-v1 (:8001) │ │ web-v1 (:8002) │
|
|
484
|
+
│ api-v2 (:8011) │ │ web-v2 (:8012) │
|
|
485
|
+
└───────────────────────┘ └───────────────────────┘
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
## Architecture
|
|
489
|
+
|
|
490
|
+
```
|
|
491
|
+
┌─────────────────────────────────────────────────────────┐
|
|
492
|
+
│ Client Traffic │
|
|
493
|
+
└─────────────────────────┬───────────────────────────────┘
|
|
494
|
+
│
|
|
495
|
+
▼
|
|
496
|
+
┌─────────────────────────────────────────────────────────┐
|
|
497
|
+
│ portokd (LISTEN_PORT) │
|
|
498
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
|
|
499
|
+
│ │ Proxy │ │ Admin │ │ Health Monitor │ │
|
|
500
|
+
│ │ (http-proxy)│ │ Endpoints │ │ (auto-rollback)│ │
|
|
501
|
+
│ └──────┬──────┘ └─────────────┘ └─────────────────┘ │
|
|
502
|
+
│ │ │
|
|
503
|
+
│ ┌──────┴──────┐ │
|
|
504
|
+
│ │Socket Tracker│ ← Maps connections to ports for drain │
|
|
505
|
+
│ └──────┬──────┘ │
|
|
506
|
+
└─────────┼───────────────────────────────────────────────┘
|
|
507
|
+
│
|
|
508
|
+
▼
|
|
509
|
+
┌─────────────────────────────────────────────────────────┐
|
|
510
|
+
│ 127.0.0.1:ACTIVE_PORT │
|
|
511
|
+
│ (Your App) │
|
|
512
|
+
└─────────────────────────────────────────────────────────┘
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Zero-Downtime Deployment Flow
|
|
516
|
+
|
|
517
|
+
1. Deploy new app version on a random port (e.g., 54321)
|
|
518
|
+
2. New app starts and exposes `/health` endpoint
|
|
519
|
+
3. Call `portok switch 54321` or `POST /__switch?port=54321`
|
|
520
|
+
4. Portok health-checks the new port
|
|
521
|
+
5. If healthy: switch traffic, drain old connections
|
|
522
|
+
6. If new port fails during rollback window: auto-rollback
|
|
523
|
+
7. Old app can be stopped after drain period
|
|
524
|
+
|
|
525
|
+
## Performance Notes
|
|
526
|
+
|
|
527
|
+
Portok is optimized for high throughput and low latency proxy operations.
|
|
528
|
+
|
|
529
|
+
### FAST_PATH Mode (Recommended for High-Traffic)
|
|
530
|
+
|
|
531
|
+
Enable `FAST_PATH=1` for maximum throughput in production or benchmarks:
|
|
532
|
+
|
|
533
|
+
```bash
|
|
534
|
+
export FAST_PATH=1
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
This disables expensive metrics (status counters, rolling RPS) while keeping essential counters (totalRequests, inflight, proxyErrors).
|
|
538
|
+
|
|
539
|
+
### Keep-Alive (Critical)
|
|
540
|
+
|
|
541
|
+
The upstream keep-alive agent is **critical for performance**. Without it, every request opens a new TCP connection which adds ~0.5-2ms latency and significantly limits throughput.
|
|
542
|
+
|
|
543
|
+
Keep-alive is enabled by default (`UPSTREAM_KEEPALIVE=1`). Do not disable it in production.
|
|
544
|
+
|
|
545
|
+
### Performance Validation
|
|
546
|
+
|
|
547
|
+
Run the validation benchmark to verify performance:
|
|
548
|
+
|
|
549
|
+
```bash
|
|
550
|
+
# Quick validation (3s)
|
|
551
|
+
FAST_PATH=1 node bench/validate.mjs --quick
|
|
552
|
+
|
|
553
|
+
# Full validation (10s)
|
|
554
|
+
FAST_PATH=1 node bench/validate.mjs
|
|
555
|
+
|
|
556
|
+
# Manual autocannon test
|
|
557
|
+
# Direct:
|
|
558
|
+
npx autocannon -c 50 -d 10 http://127.0.0.1:<APP_PORT>/
|
|
559
|
+
|
|
560
|
+
# Proxied:
|
|
561
|
+
npx autocannon -c 50 -d 10 http://127.0.0.1:<PROXY_PORT>/
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
**Acceptance Criteria:**
|
|
565
|
+
- RPS >= 30% of direct (http-proxy adds inherent overhead)
|
|
566
|
+
- Added p50 latency <= 10ms
|
|
567
|
+
- p99 latency <= 50ms
|
|
568
|
+
|
|
569
|
+
**Typical Results (localhost, FAST_PATH=1):**
|
|
570
|
+
- Direct: ~28,000 RPS, p50=1ms
|
|
571
|
+
- Proxied: ~13,000 RPS, p50=3ms
|
|
572
|
+
- Socket reuse: 800-2000x (confirms keep-alive working)
|
|
573
|
+
|
|
574
|
+
### Debug Upstream Connections
|
|
575
|
+
|
|
576
|
+
Enable `DEBUG_UPSTREAM=1` to track upstream socket creation in `/__metrics`:
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
export DEBUG_UPSTREAM=1
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
This exposes `upstreamSocketsCreated` in metrics to verify keep-alive is working.
|
|
583
|
+
|
|
584
|
+
### Optimization Summary
|
|
585
|
+
|
|
586
|
+
| Optimization | Impact |
|
|
587
|
+
|--------------|--------|
|
|
588
|
+
| FAST_PATH mode | Minimal per-request overhead |
|
|
589
|
+
| Keep-alive agent | 10-20x throughput vs no keep-alive |
|
|
590
|
+
| Connection header stripping | Ensures upstream keep-alive works |
|
|
591
|
+
| Minimal URL parsing | No allocations in hot path |
|
|
592
|
+
| res.once() listeners | Auto-cleanup, no memory leaks |
|
|
593
|
+
| Socket reuse tracking | DEBUG_UPSTREAM confirms keep-alive |
|
|
594
|
+
|
|
595
|
+
## Security
|
|
596
|
+
|
|
597
|
+
- **Token authentication**: All admin endpoints require `x-admin-token` header
|
|
598
|
+
- **Timing-safe comparison**: Token validation uses `crypto.timingSafeEqual`
|
|
599
|
+
- **IP allowlist**: Admin endpoints restricted by IP (default: localhost only)
|
|
600
|
+
- **Rate limiting**: 10 requests/minute per IP for admin endpoints
|
|
601
|
+
- **SSRF protection**: Target host fixed to `127.0.0.1`
|
|
602
|
+
|
|
603
|
+
## License
|
|
604
|
+
|
|
605
|
+
MIT
|
|
606
|
+
|