@fabioplunser/epd 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +458 -0
- package/bin/epd.js +13 -0
- package/dist/cli.js +3855 -0
- package/docs/guide.md +576 -0
- package/package.json +56 -0
package/docs/guide.md
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
# Complete Guide to epd (Easy Project Deployer)
|
|
2
|
+
|
|
3
|
+
`epd` is a zero-downtime deployment tool designed to deploy any application to one server or a fleet of servers over standard SSH. It combines zero-downtime blue/green deployments, automatic Let's Encrypt SSL, a shared reverse proxy (Traefik), and multi-host load balancing without requiring Kubernetes or complex container orchestration.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Key Features & Architecture](#1-key-features--architecture)
|
|
10
|
+
2. [Prerequisites & Installation](#2-prerequisites--installation)
|
|
11
|
+
3. [Quick Start (5 Minutes)](#3-quick-start-5-minutes)
|
|
12
|
+
4. [Deployment Modes: Docker vs. Process](#4-deployment-modes-docker-vs-process)
|
|
13
|
+
5. [The Blue/Green Zero-Downtime Lifecycle](#5-the-bluegreen-zero-downtime-lifecycle)
|
|
14
|
+
6. [Multi-Site Hosting on a Single VPS](#6-multi-site-hosting-on-a-single-vps)
|
|
15
|
+
7. [High Availability & Multi-Server Clustering](#7-high-availability--multi-server-clustering)
|
|
16
|
+
8. [Advanced Routing & Traefik Features](#8-advanced-routing--traefik-features)
|
|
17
|
+
9. [Secrets & Environment Management](#9-secrets--environment-management)
|
|
18
|
+
10. [Accessories (Databases, Caches & Dashboards)](#10-accessories-databases-caches--dashboards)
|
|
19
|
+
11. [CLI Commands Reference](#11-cli-commands-reference)
|
|
20
|
+
12. [Configuration Reference (`epd.yml`)](#12-configuration-reference-epdyml)
|
|
21
|
+
13. [Operations, Troubleshooting & Disaster Recovery](#13-operations-troubleshooting--disaster-recovery)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 1. Key Features & Architecture
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Internet (HTTP:80 / HTTPS:443)
|
|
29
|
+
│
|
|
30
|
+
▼
|
|
31
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
32
|
+
│ SERVER (Host) │
|
|
33
|
+
│ │
|
|
34
|
+
│ ┌───────────────────────────────────────────────────────┐ │
|
|
35
|
+
│ │ Shared Traefik Proxy (:80 / :443) │ │
|
|
36
|
+
│ │ Automatically routes traffic based on dynamic YAML │ │
|
|
37
|
+
│ └────────┬─────────────────────────────┬────────────────┘ │
|
|
38
|
+
│ │ │ │
|
|
39
|
+
│ ▼ ▼ │
|
|
40
|
+
│ ┌─────────────────────┐ ┌────────────────────────┐ │
|
|
41
|
+
│ │ App 1 (Blog) │ │ App 2 (Store) │ │
|
|
42
|
+
│ │ Blue/Green replicas │ │ Blue/Green replicas │ │
|
|
43
|
+
│ │ blog.example.com │ │ store.example.com │ │
|
|
44
|
+
│ └─────────────────────┘ └────────────────────────┘ │
|
|
45
|
+
│ │ │
|
|
46
|
+
│ ▼ │
|
|
47
|
+
│ ┌─────────────────────┐ │
|
|
48
|
+
│ │ Accessory: Postgres │ │
|
|
49
|
+
│ │ /var/lib/epd/... │ │
|
|
50
|
+
│ └─────────────────────┘ │
|
|
51
|
+
└─────────────────────────────────────────────────────────────┘
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Why epd?
|
|
55
|
+
- **Many Sites on One Server**: Every app maintains its own isolated dynamic routing file under `/var/lib/epd/proxy/dynamic/<app>.yml`. They share one global Traefik instance. Adding or removing an application leaves other apps on the same machine completely untouched.
|
|
56
|
+
- **Docker Optional (`mode: process`)**: If you don't want containers, `epd` uploads your code via `rsync`, installs dependencies, and manages processes using `pm2` with the same blue/green slot switching, health checks, and rollbacks.
|
|
57
|
+
- **True Zero Downtime**: The existing version continues serving live traffic until the new version passes all health checks and Traefik switches traffic. Old replicas are drained gracefully before being stopped.
|
|
58
|
+
- **Cross-Host Failover (`proxy.cross_host: true`)**: With multi-server setups, each server's proxy can route requests across the entire fleet. If a server goes offline, the other nodes immediately absorb the traffic.
|
|
59
|
+
- **SSH Only**: No agent daemons, no control planes, no vendor lock-in.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 2. Prerequisites & Installation
|
|
64
|
+
|
|
65
|
+
### Requirements
|
|
66
|
+
- **Local Machine**:
|
|
67
|
+
- [Bun](https://bun.sh) (v1.1+)
|
|
68
|
+
- `ssh` client
|
|
69
|
+
- `docker` (if using `mode: docker`)
|
|
70
|
+
- `rsync` (if using `mode: process`)
|
|
71
|
+
- **Remote Server(s)**:
|
|
72
|
+
- Any modern Linux machine (Ubuntu, Debian, Fedora, Arch, Alpine, etc.)
|
|
73
|
+
- Open SSH port with root or passwordless `sudo` access
|
|
74
|
+
|
|
75
|
+
### Installation
|
|
76
|
+
|
|
77
|
+
Install globally using Bun:
|
|
78
|
+
```bash
|
|
79
|
+
bun install -g epd
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Or run directly from source or git:
|
|
83
|
+
```bash
|
|
84
|
+
git clone https://github.com/FabioPlunser/EPD.git
|
|
85
|
+
cd EPD && bun install
|
|
86
|
+
bun run src/cli.ts --help
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
To compile a standalone, self-contained binary (no Bun runtime required on your local machine):
|
|
90
|
+
```bash
|
|
91
|
+
bun run build # Produces dist/epd
|
|
92
|
+
sudo cp dist/epd /usr/local/bin/epd
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 3. Quick Start (5 Minutes)
|
|
98
|
+
|
|
99
|
+
### Step 1: Initialize Your Project
|
|
100
|
+
From inside your application directory:
|
|
101
|
+
```bash
|
|
102
|
+
epd init
|
|
103
|
+
```
|
|
104
|
+
This interactive command detects your runtime (Node, Bun, Python, Go, Ruby, etc.), suggests sensible defaults, creates `epd.yml`, and optionally generates a starter `Dockerfile`.
|
|
105
|
+
|
|
106
|
+
### Step 2: Configure Your Servers
|
|
107
|
+
Edit `epd.yml`:
|
|
108
|
+
```yaml
|
|
109
|
+
name: my-app
|
|
110
|
+
mode: docker
|
|
111
|
+
image: my-app
|
|
112
|
+
|
|
113
|
+
ssh:
|
|
114
|
+
user: root
|
|
115
|
+
|
|
116
|
+
servers:
|
|
117
|
+
web:
|
|
118
|
+
hosts: [203.0.113.10]
|
|
119
|
+
port: 3000
|
|
120
|
+
domains: [myapp.example.com]
|
|
121
|
+
|
|
122
|
+
proxy:
|
|
123
|
+
email: admin@example.com
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Step 3: Run Server Setup
|
|
127
|
+
```bash
|
|
128
|
+
epd setup
|
|
129
|
+
```
|
|
130
|
+
`epd setup`:
|
|
131
|
+
1. Verifies SSH connectivity.
|
|
132
|
+
2. Installs Docker (or PM2 and Node/Bun for process mode) if not already installed.
|
|
133
|
+
3. Sets up the directory structure and reserved port blocks.
|
|
134
|
+
4. Starts the shared Traefik proxy.
|
|
135
|
+
5. Deploys your project.
|
|
136
|
+
|
|
137
|
+
### Step 4: Subsequent Deployments
|
|
138
|
+
For every change thereafter:
|
|
139
|
+
```bash
|
|
140
|
+
epd deploy
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 4. Deployment Modes: Docker vs. Process
|
|
146
|
+
|
|
147
|
+
`epd` supports two distinct runtimes configured via the top-level `mode` field.
|
|
148
|
+
|
|
149
|
+
### Mode: `docker` (Default)
|
|
150
|
+
In Docker mode:
|
|
151
|
+
1. `epd` builds an image locally (using BuildKit / buildx if available) tagged with the commit SHA.
|
|
152
|
+
2. If a `registry:` is specified, the image is pushed and pulled by servers. If omitted, `epd` streams the image directly over SSH (`docker save | gzip | ssh ... docker load`).
|
|
153
|
+
3. Replicas run in containers on a shared Docker bridge network (`epd`).
|
|
154
|
+
4. Replicas communicate with Traefik using internal container DNS names (`http://epd-<app>-<service>-<slot>-<replica>:<port>`).
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
mode: docker
|
|
158
|
+
image: ghcr.io/my-org/my-app
|
|
159
|
+
registry:
|
|
160
|
+
server: ghcr.io
|
|
161
|
+
username: my-user
|
|
162
|
+
password: ${GITHUB_TOKEN}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Mode: `process` (No Docker on Server)
|
|
166
|
+
In process mode:
|
|
167
|
+
1. No Docker is needed on the remote host.
|
|
168
|
+
2. `epd` uploads your code via `rsync` (or clean `git archive`) into `/var/lib/epd/apps/<app>/releases/<version>`.
|
|
169
|
+
3. Runs your `install` and `build` commands on the host.
|
|
170
|
+
4. Starts each replica under `pm2` with unique assigned host ports and environment variables.
|
|
171
|
+
5. Traefik runs as a native binary managed by PM2, routing traffic to localhost ports.
|
|
172
|
+
|
|
173
|
+
```yaml
|
|
174
|
+
mode: process
|
|
175
|
+
|
|
176
|
+
process:
|
|
177
|
+
install: bun install --frozen-lockfile
|
|
178
|
+
build: bun run build
|
|
179
|
+
start: bun run start
|
|
180
|
+
exclude: [".git", "node_modules", ".env", "dist"]
|
|
181
|
+
|
|
182
|
+
servers:
|
|
183
|
+
web:
|
|
184
|
+
hosts: [203.0.113.10]
|
|
185
|
+
replicas: 2
|
|
186
|
+
port: 3000
|
|
187
|
+
domains: [myapp.example.com]
|
|
188
|
+
|
|
189
|
+
proxy:
|
|
190
|
+
ssl: false
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 5. The Blue/Green Zero-Downtime Lifecycle
|
|
196
|
+
|
|
197
|
+
Every deploy follows an automated zero-downtime sequence:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
[Build & Push/Ship]
|
|
201
|
+
│
|
|
202
|
+
▼
|
|
203
|
+
[Start New Slot (e.g. Green)] ───► New replicas start on alternative slot
|
|
204
|
+
│
|
|
205
|
+
▼
|
|
206
|
+
[Health Probe Verification] ───► If probes fail: Green is removed, Blue stays live!
|
|
207
|
+
│
|
|
208
|
+
▼
|
|
209
|
+
[Atomic Route Switch] ───► Traefik dynamic config updated to point to Green
|
|
210
|
+
│
|
|
211
|
+
▼
|
|
212
|
+
[Drain & Retire Old Slot] ───► In-flight requests complete; Blue slot retired
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
1. **Slots**: Each service alternates between `blue` and `green`.
|
|
216
|
+
2. **Health Check**: Before any routing changes, `epd` executes an in-server probe against every new replica.
|
|
217
|
+
3. **Safety Fallback**: If health checks fail or timeout, the new slot is killed and the current slot remains in service. Zero traffic is dropped.
|
|
218
|
+
4. **Instant Rollback**: Because previous releases and images remain on the server, `epd rollback` can reactivate the previous version within seconds without rebuilding:
|
|
219
|
+
```bash
|
|
220
|
+
epd rollback
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 6. Multi-Site Hosting on a Single VPS
|
|
226
|
+
|
|
227
|
+
Kamal and other deploy tools often assume ownership of ports 80/443, making it painful to run multiple distinct web apps on a single machine.
|
|
228
|
+
|
|
229
|
+
With `epd`, every application writes its own isolated configuration file into Traefik's dynamic directory:
|
|
230
|
+
- Site A (`epd.yml` with `name: blog`): writes `/var/lib/epd/proxy/dynamic/blog.yml`
|
|
231
|
+
- Site B (`epd.yml` with `name: shop`): writes `/var/lib/epd/proxy/dynamic/shop.yml`
|
|
232
|
+
|
|
233
|
+
Both applications share the same server IP, the same Traefik instance, and automatic Let's Encrypt certificates without conflict. Removing Site B (`epd remove`) removes only `shop.yml`, leaving Site A operating seamlessly.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 7. High Availability & Multi-Server Clustering
|
|
238
|
+
|
|
239
|
+
### Across-Host Load Balancing (`proxy.cross_host: true`)
|
|
240
|
+
|
|
241
|
+
By default, Traefik routes traffic to replicas located on its own server. When you have multiple servers behind round-robin DNS or a cloud load balancer, set `proxy.cross_host: true`:
|
|
242
|
+
|
|
243
|
+
```yaml
|
|
244
|
+
proxy:
|
|
245
|
+
cross_host: true
|
|
246
|
+
private_ips:
|
|
247
|
+
203.0.113.10: 10.0.0.10
|
|
248
|
+
203.0.113.11: 10.0.0.11
|
|
249
|
+
|
|
250
|
+
servers:
|
|
251
|
+
web:
|
|
252
|
+
hosts: [203.0.113.10, 203.0.113.11]
|
|
253
|
+
replicas: 2
|
|
254
|
+
port: 3000
|
|
255
|
+
domains: [example.com]
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
With `cross_host: true`:
|
|
259
|
+
- The application port is published on each host (bound to the private IP when specified).
|
|
260
|
+
- Traefik on Server A balances across all replicas on Server A **and** Server B.
|
|
261
|
+
- If Server B goes down, visitors hitting Server B's DNS address will still be routed to Server A by Server A's proxy, guaranteeing high availability.
|
|
262
|
+
|
|
263
|
+
### Deployment Strategy
|
|
264
|
+
Control rolling vs. parallel execution across multiple hosts:
|
|
265
|
+
```yaml
|
|
266
|
+
strategy: rolling # Default: rolls one host at a time, keeping the cluster serving
|
|
267
|
+
# strategy: parallel # Deploys to all hosts simultaneously
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 8. Advanced Routing & Traefik Features
|
|
273
|
+
|
|
274
|
+
Use the `routes:` block under any server group for fine-grained routing control:
|
|
275
|
+
|
|
276
|
+
```yaml
|
|
277
|
+
servers:
|
|
278
|
+
web:
|
|
279
|
+
hosts: [203.0.113.10]
|
|
280
|
+
port: 3000
|
|
281
|
+
routes:
|
|
282
|
+
# 1. Standard Domain
|
|
283
|
+
- host: example.com
|
|
284
|
+
|
|
285
|
+
# 2. Subdomain to different container port
|
|
286
|
+
- host: api.example.com
|
|
287
|
+
port: 8080
|
|
288
|
+
|
|
289
|
+
# 3. Path-based routing with prefix stripping and Basic Auth
|
|
290
|
+
- host: example.com
|
|
291
|
+
path: /admin
|
|
292
|
+
port: 4000
|
|
293
|
+
strip_path: true
|
|
294
|
+
basic_auth:
|
|
295
|
+
- "admin:$apr1$xyz$0123456789abcdefghijk."
|
|
296
|
+
|
|
297
|
+
# 4. Domain Redirect (HTTP 301)
|
|
298
|
+
- host: www.example.com
|
|
299
|
+
redirect: https://example.com
|
|
300
|
+
|
|
301
|
+
# 5. Temporary Redirect (HTTP 302)
|
|
302
|
+
- host: promo.example.com
|
|
303
|
+
redirect: https://example.com/special
|
|
304
|
+
permanent: false
|
|
305
|
+
|
|
306
|
+
# 6. Sticky Session Load Balancing (Cookie pinned)
|
|
307
|
+
- host: app.example.com
|
|
308
|
+
path: /socket
|
|
309
|
+
sticky: true
|
|
310
|
+
|
|
311
|
+
# 7. Custom Headers
|
|
312
|
+
- host: secure.example.com
|
|
313
|
+
headers:
|
|
314
|
+
X-Frame-Options: "DENY"
|
|
315
|
+
X-Content-Type-Options: "nosniff"
|
|
316
|
+
|
|
317
|
+
# 8. Wildcard Subdomains (requires DNS challenge)
|
|
318
|
+
- host: "*.example.com"
|
|
319
|
+
priority: 1
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Let's Encrypt DNS Challenge
|
|
323
|
+
For wildcard certificates (`*.example.com`) or multi-server setups:
|
|
324
|
+
```yaml
|
|
325
|
+
proxy:
|
|
326
|
+
challenge: dns
|
|
327
|
+
dns_provider: cloudflare
|
|
328
|
+
dns_env: [CF_DNS_API_TOKEN]
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## 9. Secrets & Environment Management
|
|
334
|
+
|
|
335
|
+
`epd` ensures secrets are never checked into git, never printed in `docker inspect`, and never written to shell histories.
|
|
336
|
+
|
|
337
|
+
### Environment Declarations
|
|
338
|
+
```yaml
|
|
339
|
+
env:
|
|
340
|
+
clear:
|
|
341
|
+
NODE_ENV: production
|
|
342
|
+
LOG_LEVEL: info
|
|
343
|
+
secret:
|
|
344
|
+
- DATABASE_URL
|
|
345
|
+
- STRIPE_SECRET_KEY
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### How Secret Resolution Works
|
|
349
|
+
1. When you run `epd deploy`, variables in `env.secret` are read from:
|
|
350
|
+
- Your local environment (`export DATABASE_URL=...`)
|
|
351
|
+
- `.env` / `.env.local` in your project root
|
|
352
|
+
- `.env.<destination>` (when using `--destination`)
|
|
353
|
+
2. On the server, `epd` writes a `0600` root-owned environment file.
|
|
354
|
+
3. Containers read secrets via `--env-file`; process mode loads them into PM2 environments.
|
|
355
|
+
|
|
356
|
+
### Destination Overlays
|
|
357
|
+
Manage staging vs. production effortlessly:
|
|
358
|
+
```bash
|
|
359
|
+
epd deploy -d staging
|
|
360
|
+
```
|
|
361
|
+
Merges `epd.staging.yml` over `epd.yml` and loads `.env.staging`.
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 10. Accessories (Databases, Caches & Dashboards)
|
|
366
|
+
|
|
367
|
+
Accessories are persistent companion containers (like PostgreSQL, Redis, MinIO, or Adminer) that `epd` provisions and manages alongside your app. Unlike application containers, accessories are **not** replaced on every deploy.
|
|
368
|
+
|
|
369
|
+
```yaml
|
|
370
|
+
accessories:
|
|
371
|
+
db:
|
|
372
|
+
image: postgres:17
|
|
373
|
+
host: 203.0.113.10
|
|
374
|
+
env:
|
|
375
|
+
clear:
|
|
376
|
+
POSTGRES_DB: myapp
|
|
377
|
+
secret:
|
|
378
|
+
- POSTGRES_PASSWORD
|
|
379
|
+
volumes:
|
|
380
|
+
- /var/lib/epd/volumes/myapp-db:/var/lib/postgresql/data
|
|
381
|
+
ports:
|
|
382
|
+
- "127.0.0.1:5432:5432"
|
|
383
|
+
|
|
384
|
+
redis:
|
|
385
|
+
image: redis:7-alpine
|
|
386
|
+
host: 203.0.113.10
|
|
387
|
+
volumes:
|
|
388
|
+
- /var/lib/epd/volumes/myapp-redis:/data
|
|
389
|
+
|
|
390
|
+
# Accessory with public HTTP routing through Traefik
|
|
391
|
+
db_ui:
|
|
392
|
+
image: adminer:latest
|
|
393
|
+
host: 203.0.113.10
|
|
394
|
+
port: 8080
|
|
395
|
+
domains: [db.myapp.example.com]
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Managing Accessories
|
|
399
|
+
- **View accessory status**: `epd status` shows accessory state and ports.
|
|
400
|
+
- **View accessory logs**: `epd logs --accessory db -f`
|
|
401
|
+
- **Execute commands in an accessory**: `epd exec --accessory db -i -- psql -U myapp`
|
|
402
|
+
- **Recreate accessory container**: `epd deploy --recreate-accessories`
|
|
403
|
+
- **Inspect Docker run command**: `epd docker-command` prints the exact `docker run` command for each accessory.
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## 11. CLI Commands Reference
|
|
408
|
+
|
|
409
|
+
| Command | Flags | Description |
|
|
410
|
+
|---|---|---|
|
|
411
|
+
| `epd init` | `--name`, `--mode`, `--host`, `--domain`, `--port`, `--dockerfile` | Initialize a new `epd.yml` for the current project. |
|
|
412
|
+
| `epd setup` | `--skip-deploy`, `--host` | Install prerequisites on servers, start the proxy, and deploy. |
|
|
413
|
+
| `epd deploy` | `--version`, `--no-build`, `--service`, `--host`, `--skip-health`, `--skip-hooks`, `--skip-lock`, `--recreate-accessories`, `-d` | Build and deploy with zero downtime. |
|
|
414
|
+
| `epd redeploy` | `--service`, `--host`, `--skip-health`, `--skip-lock` | Restart the current version without rebuilding or uploading. |
|
|
415
|
+
| `epd rollback [v]`| `--list`, `--service`, `--host` | Roll back to the previous version or a specified version. |
|
|
416
|
+
| `epd status` | `--json` | Show running replicas, slots, health status, and live routes. |
|
|
417
|
+
| `epd logs` | `-f/--follow`, `-n/--lines`, `--service`, `--accessory`, `--host`, `--grep`, `--since` | View or tail logs from servers, services, or accessories. |
|
|
418
|
+
| `epd exec -- <cmd>`| `--host`, `--service`, `--accessory`, `--reuse`, `-i` | Run a one-off command with the app's image and environment. |
|
|
419
|
+
| `epd shell` | `--host`, `--service`, `--accessory` | Open an interactive shell inside a running replica or accessory. |
|
|
420
|
+
| `epd proxy <cmd>` | `status`, `reboot`, `logs`, `routes`, `remove` (`--host`, `-f`, `-n`) | Inspect, follow logs, or restart the shared Traefik proxy. |
|
|
421
|
+
| `epd config` | `--json`, `--raw`, `--traefik` | Validate and print the resolved configuration or Traefik specs. |
|
|
422
|
+
| `epd docker-command`| `--host`, `--service`, `--slot`, `--version`, `--replica` | Output copy-pasteable `docker run` / `pm2` commands. |
|
|
423
|
+
| `epd lock <cmd>` | `status`, `release` | Inspect or clear the atomic deploy lock. |
|
|
424
|
+
| `epd remove` | `-y/--yes`, `--keep-data`, `--host` | Stop this app, remove its routes, and delete files from servers. |
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## 12. Configuration Reference (`epd.yml`)
|
|
429
|
+
|
|
430
|
+
Full annotated schema of `epd.yml`:
|
|
431
|
+
|
|
432
|
+
```yaml
|
|
433
|
+
name: myapp # App name (alphanumeric, dashes, underscores)
|
|
434
|
+
mode: docker # docker | process
|
|
435
|
+
|
|
436
|
+
# Docker Build Settings
|
|
437
|
+
image: ghcr.io/org/myapp # Docker image repository (docker mode)
|
|
438
|
+
registry: # Optional: registry credentials
|
|
439
|
+
server: ghcr.io
|
|
440
|
+
username: deployer
|
|
441
|
+
password: ${GITHUB_TOKEN} # Interpolated from environment
|
|
442
|
+
build:
|
|
443
|
+
dockerfile: Dockerfile # Relative path to Dockerfile
|
|
444
|
+
context: . # Build context
|
|
445
|
+
platform: linux/amd64 # Target architecture
|
|
446
|
+
args: # Build arguments
|
|
447
|
+
COMMIT_SHA: ${GIT_SHA:-dev}
|
|
448
|
+
secrets: # BuildKit build secrets
|
|
449
|
+
npm_token: NPM_TOKEN
|
|
450
|
+
|
|
451
|
+
# Process Mode Settings
|
|
452
|
+
process:
|
|
453
|
+
install: bun install # Dependency install command
|
|
454
|
+
build: bun run build # Build command
|
|
455
|
+
start: bun run start # Start command (receives $PORT)
|
|
456
|
+
exclude: [".git", "node_modules"]
|
|
457
|
+
source: rsync # rsync | git
|
|
458
|
+
|
|
459
|
+
# SSH Configuration
|
|
460
|
+
ssh:
|
|
461
|
+
user: root # Remote SSH user
|
|
462
|
+
port: 22 # Remote SSH port
|
|
463
|
+
key: ~/.ssh/id_ed25519 # Optional explicit private key
|
|
464
|
+
proxy_jump: bastion.net # Optional Jump host / Bastion
|
|
465
|
+
options: # Extra SSH options
|
|
466
|
+
- ServerAliveInterval=30
|
|
467
|
+
|
|
468
|
+
# Server Definitions
|
|
469
|
+
servers:
|
|
470
|
+
web:
|
|
471
|
+
hosts: [203.0.113.10] # Server IPs or hostnames
|
|
472
|
+
replicas: 2 # Replicas per host
|
|
473
|
+
port: 3000 # Internal application port
|
|
474
|
+
domains: [myapp.com] # Domains to serve
|
|
475
|
+
cpus: "2.0" # CPU limit
|
|
476
|
+
memory: "1g" # Memory limit
|
|
477
|
+
drain: 10 # Seconds to drain old slot
|
|
478
|
+
stop_timeout: 15 # SIGTERM grace period in seconds
|
|
479
|
+
volumes:
|
|
480
|
+
- /var/lib/epd/volumes/uploads:/app/uploads
|
|
481
|
+
env:
|
|
482
|
+
clear:
|
|
483
|
+
APP_ENV: production
|
|
484
|
+
secret:
|
|
485
|
+
- APP_SECRET
|
|
486
|
+
|
|
487
|
+
worker: # Background worker (no exposed routes)
|
|
488
|
+
hosts: [203.0.113.10]
|
|
489
|
+
replicas: 1
|
|
490
|
+
command: bun run worker.ts
|
|
491
|
+
|
|
492
|
+
# Shared Reverse Proxy
|
|
493
|
+
proxy:
|
|
494
|
+
enabled: true
|
|
495
|
+
ssl: true # Automatically request Let's Encrypt certificates
|
|
496
|
+
email: admin@myapp.com # Registration email for ACME
|
|
497
|
+
challenge: http # http | dns | tlsalpn
|
|
498
|
+
entrypoints:
|
|
499
|
+
web: 80
|
|
500
|
+
websecure: 443
|
|
501
|
+
cross_host: false # Enable cross-server load balancing
|
|
502
|
+
reload_wait: 3 # Throttle duration for route changes
|
|
503
|
+
|
|
504
|
+
# Health Check Settings
|
|
505
|
+
healthcheck:
|
|
506
|
+
path: /health # HTTP path to probe (null for TCP-only check)
|
|
507
|
+
status: 200-399 # Acceptable status codes
|
|
508
|
+
timeout: 60 # Timeout in seconds
|
|
509
|
+
interval: 2 # Seconds between probe attempts
|
|
510
|
+
delay: 1 # Initial delay before probing
|
|
511
|
+
|
|
512
|
+
# Global Environment
|
|
513
|
+
env:
|
|
514
|
+
clear:
|
|
515
|
+
NODE_ENV: production
|
|
516
|
+
secret:
|
|
517
|
+
- DATABASE_URL
|
|
518
|
+
|
|
519
|
+
# Lifecycle Hooks
|
|
520
|
+
hooks:
|
|
521
|
+
pre_build: ./scripts/lint.sh
|
|
522
|
+
pre_deploy: ./scripts/db-migrate.sh
|
|
523
|
+
post_deploy: ./scripts/notify-slack.sh
|
|
524
|
+
on_failure: ./scripts/alert-ops.sh
|
|
525
|
+
|
|
526
|
+
# Deployment Strategy
|
|
527
|
+
strategy: rolling # rolling | parallel
|
|
528
|
+
keep_releases: 5 # Retain last N images / release directories
|
|
529
|
+
remote_root: /var/lib/epd # Root directory on remote hosts
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## 13. Operations, Troubleshooting & Disaster Recovery
|
|
535
|
+
|
|
536
|
+
### Common Scenarios
|
|
537
|
+
|
|
538
|
+
#### 1. Deployment Fails on Health Check
|
|
539
|
+
- **What Happened**: New replicas started, but `/health` (or `/`) did not return `200-399` within the timeout.
|
|
540
|
+
- **Result**: The new replicas were automatically purged. The previous slot is still live. Zero downtime occurred.
|
|
541
|
+
- **Investigation**:
|
|
542
|
+
```bash
|
|
543
|
+
epd logs --service web --lines 50
|
|
544
|
+
```
|
|
545
|
+
- **Remedy**: Fix the issue, or adjust `healthcheck.timeout` / `healthcheck.path` in `epd.yml`.
|
|
546
|
+
|
|
547
|
+
#### 2. Deploy Lock Held
|
|
548
|
+
- **What Happened**: A previous deploy process was terminated abruptly (e.g. laptop closed or killed with `kill -9`).
|
|
549
|
+
- **Investigation**:
|
|
550
|
+
```bash
|
|
551
|
+
epd lock status
|
|
552
|
+
```
|
|
553
|
+
- **Remedy**:
|
|
554
|
+
```bash
|
|
555
|
+
epd lock release
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
#### 3. Inspecting Server Routes
|
|
559
|
+
To see the exact Traefik route file epd generated on the server:
|
|
560
|
+
```bash
|
|
561
|
+
epd proxy routes
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
#### 4. Checking Proxy Health & Restarting
|
|
565
|
+
If you updated global proxy options (such as adding an entrypoint or enabling the dashboard):
|
|
566
|
+
```bash
|
|
567
|
+
epd proxy status
|
|
568
|
+
epd proxy reboot
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
#### 5. Completely Removing an Application
|
|
572
|
+
When decommissioning an app from your servers:
|
|
573
|
+
```bash
|
|
574
|
+
epd remove
|
|
575
|
+
```
|
|
576
|
+
This cleans up all replicas, releases, and Traefik dynamic routes without touching any other epd applications sharing the machine.
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fabioplunser/epd",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Easy Project Deployer — deploy any project to one or many servers with Docker or PM2, fronted by Traefik.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "Fabio Plünser",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/FabioPlunser/epd#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/FabioPlunser/epd.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/FabioPlunser/epd/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"deployment",
|
|
18
|
+
"docker",
|
|
19
|
+
"traefik",
|
|
20
|
+
"zero-downtime",
|
|
21
|
+
"devops",
|
|
22
|
+
"pm2",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"bin": {
|
|
26
|
+
"epd": "bin/epd.js",
|
|
27
|
+
"edp": "bin/epd.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"bin",
|
|
31
|
+
"dist/cli.js",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"docs"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"start": "bun run src/cli.ts",
|
|
41
|
+
"build": "bun run build:bundle && bun run build:binary",
|
|
42
|
+
"build:bundle": "bun build src/cli.ts --target=bun --outfile dist/cli.js",
|
|
43
|
+
"build:binary": "bun build --compile --minify --sourcemap src/cli.ts --outfile dist/epd",
|
|
44
|
+
"build:binaries": "bun run scripts/build-all.ts",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"test": "bun test",
|
|
47
|
+
"prepublishOnly": "bun run typecheck && bun test && bun run build:bundle"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"bun": ">=1.1.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/bun": "latest",
|
|
54
|
+
"typescript": "^5.6.0"
|
|
55
|
+
}
|
|
56
|
+
}
|