@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fabio Plünser
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# epd — Easy Project Deployer
|
|
2
|
+
|
|
3
|
+
Deploy any project to one server or ten, with zero downtime, from a single
|
|
4
|
+
command:
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
epd deploy
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
epd takes the good ideas from [Kamal](https://kamal-deploy.org) — a config file
|
|
11
|
+
in your repo, SSH as the only requirement, blue/green containers, rollbacks —
|
|
12
|
+
and fixes the two things that get in the way in practice:
|
|
13
|
+
|
|
14
|
+
- **Many sites on one server.** Every app writes its own routing file for a
|
|
15
|
+
Traefik instance shared by the whole machine. A second (or tenth) site on the
|
|
16
|
+
same box is just another `epd.yml`, with its own domains, paths and ports. No
|
|
17
|
+
fighting over port 80, no proxy that only knows about one app.
|
|
18
|
+
- **You do not have to use Docker.** `mode: process` uploads your project,
|
|
19
|
+
installs dependencies on the server and runs your own start command
|
|
20
|
+
(`bun run start`, `node server.js`, anything) under pm2 — with the same
|
|
21
|
+
blue/green switch, health checks and rollbacks. Traefik runs as a plain
|
|
22
|
+
binary, so the server needs nothing but SSH.
|
|
23
|
+
|
|
24
|
+
Multiple machines, load balancing and failover are first-class: replicas are
|
|
25
|
+
balanced by Traefik, and with `cross_host: true` every server's proxy balances
|
|
26
|
+
across every server's replicas, so one machine can serve while another is down.
|
|
27
|
+
|
|
28
|
+
📖 **For the in-depth user manual and complete configuration reference, see the [Complete Guide](docs/guide.md).**
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
### Global CLI via NPM or Bun
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install -g @fabioplunser/epd
|
|
38
|
+
# or
|
|
39
|
+
bun install -g @fabioplunser/epd
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Once installed, use `epd` anywhere:
|
|
43
|
+
```bash
|
|
44
|
+
epd --help
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Run directly without installing (npx / bunx)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx @fabioplunser/epd deploy
|
|
51
|
+
# or
|
|
52
|
+
bunx @fabioplunser/epd deploy
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Standalone Precompiled Executables (Zero Runtime Dependencies)
|
|
56
|
+
|
|
57
|
+
Download single-file standalone binaries directly from [GitHub Releases](https://github.com/FabioPlunser/epd/releases):
|
|
58
|
+
- `epd-linux-x64` / `epd-linux-arm64`
|
|
59
|
+
- `epd-darwin-x64` / `epd-darwin-arm64` (macOS Intel & Apple Silicon)
|
|
60
|
+
- `epd-windows-x64.exe`
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
curl -fsSL https://github.com/FabioPlunser/epd/releases/latest/download/epd-linux-x64 -o /usr/local/bin/epd
|
|
64
|
+
chmod +x /usr/local/bin/epd
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### From source
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git clone https://github.com/FabioPlunser/epd.git && cd epd && bun install
|
|
71
|
+
bun run build # compiles dist/cli.js and dist/epd
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
You need on **your machine**: `bun`, `ssh`, and `docker` (only for `mode: docker`)
|
|
75
|
+
or `rsync` (only for `mode: process`). You need on **the server**: SSH access.
|
|
76
|
+
`epd setup` installs everything else.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Quick start
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
cd my-project
|
|
84
|
+
epd init # writes epd.yml (and a Dockerfile if you want one)
|
|
85
|
+
epd setup # prepares the servers and deploys
|
|
86
|
+
epd deploy # every time after that
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Point your domain's A record at the server(s) first — Traefik requests a
|
|
90
|
+
Let's Encrypt certificate the first time a request arrives for the domain.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## What a deploy does
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
build → ship → start new replicas → health check → switch routes → retire old
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
1. Builds and tags the image with your git sha (or uploads the project, in
|
|
101
|
+
process mode).
|
|
102
|
+
2. Starts the new version on the *other* slot — `blue` and `green` alternate —
|
|
103
|
+
next to the version that is currently serving.
|
|
104
|
+
3. Waits for every new replica to pass its health check. If any fails, the new
|
|
105
|
+
replicas are removed and the old version keeps serving. Nothing is switched.
|
|
106
|
+
4. Writes the routing file, waits for the proxy to pick it up, then retires the
|
|
107
|
+
old replicas after a drain period.
|
|
108
|
+
|
|
109
|
+
The old version is never stopped before the new one is proven and routed, which
|
|
110
|
+
is what makes the deploy zero-downtime. There is an end-to-end test that hammers
|
|
111
|
+
a site while redeploying it and fails if a single request is not a 200.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Configuration
|
|
116
|
+
|
|
117
|
+
`epd.yml` lives in your project root. Everything below is optional except
|
|
118
|
+
`name`, one server entry, and — in process mode — a start command.
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
name: blog
|
|
122
|
+
mode: docker # docker (build an image) | process (upload + pm2)
|
|
123
|
+
|
|
124
|
+
image: ghcr.io/me/blog # docker mode only
|
|
125
|
+
|
|
126
|
+
registry: # omit entirely to ship images over SSH instead
|
|
127
|
+
server: ghcr.io
|
|
128
|
+
username: me
|
|
129
|
+
password: ${GITHUB_TOKEN}
|
|
130
|
+
|
|
131
|
+
build:
|
|
132
|
+
dockerfile: Dockerfile
|
|
133
|
+
context: .
|
|
134
|
+
platform: linux/amd64
|
|
135
|
+
args:
|
|
136
|
+
COMMIT: ${GIT_SHA:-dev}
|
|
137
|
+
secrets: # id -> env var, exposed to the build via BuildKit
|
|
138
|
+
npm_token: NPM_TOKEN
|
|
139
|
+
|
|
140
|
+
ssh:
|
|
141
|
+
user: root
|
|
142
|
+
port: 22
|
|
143
|
+
key: ~/.ssh/id_ed25519
|
|
144
|
+
proxy_jump: bastion.example.com
|
|
145
|
+
|
|
146
|
+
servers:
|
|
147
|
+
web:
|
|
148
|
+
hosts: [203.0.113.10, 203.0.113.11]
|
|
149
|
+
replicas: 2 # per host, load balanced by the proxy
|
|
150
|
+
port: 3000 # the port your app listens on
|
|
151
|
+
domains: [example.com, www.example.com]
|
|
152
|
+
env:
|
|
153
|
+
clear:
|
|
154
|
+
LOG_LEVEL: info
|
|
155
|
+
secret:
|
|
156
|
+
- DATABASE_URL
|
|
157
|
+
volumes:
|
|
158
|
+
- /var/lib/epd/volumes/blog-uploads:/app/uploads
|
|
159
|
+
cpus: "2"
|
|
160
|
+
memory: 1g
|
|
161
|
+
|
|
162
|
+
worker: # no routes → not exposed, no HTTP health check
|
|
163
|
+
hosts: [203.0.113.10]
|
|
164
|
+
command: bun run worker.ts
|
|
165
|
+
|
|
166
|
+
proxy:
|
|
167
|
+
ssl: true
|
|
168
|
+
email: me@example.com
|
|
169
|
+
challenge: http # http | dns | tlsalpn
|
|
170
|
+
entrypoints:
|
|
171
|
+
web: 80
|
|
172
|
+
websecure: 443
|
|
173
|
+
cross_host: false
|
|
174
|
+
reload_wait: 3
|
|
175
|
+
|
|
176
|
+
healthcheck:
|
|
177
|
+
path: /up
|
|
178
|
+
status: 200-399
|
|
179
|
+
timeout: 60
|
|
180
|
+
interval: 2
|
|
181
|
+
|
|
182
|
+
env:
|
|
183
|
+
clear:
|
|
184
|
+
NODE_ENV: production
|
|
185
|
+
secret:
|
|
186
|
+
- SECRET_KEY_BASE # read from your shell or .env at deploy time
|
|
187
|
+
|
|
188
|
+
accessories:
|
|
189
|
+
db:
|
|
190
|
+
image: postgres:17
|
|
191
|
+
host: 203.0.113.10
|
|
192
|
+
env:
|
|
193
|
+
clear: { POSTGRES_DB: blog }
|
|
194
|
+
secret: [POSTGRES_PASSWORD]
|
|
195
|
+
volumes: [/var/lib/epd/volumes/blog-db:/var/lib/postgresql/data]
|
|
196
|
+
ports: ["127.0.0.1:5432:5432"]
|
|
197
|
+
|
|
198
|
+
hooks:
|
|
199
|
+
pre_deploy: ./bin/test
|
|
200
|
+
post_deploy: ./bin/notify
|
|
201
|
+
|
|
202
|
+
strategy: rolling # rolling (one host at a time) | parallel
|
|
203
|
+
keep_releases: 5
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Run `epd config` to see every default filled in, and `epd config --traefik` to
|
|
207
|
+
see the exact proxy configuration epd will write.
|
|
208
|
+
|
|
209
|
+
### The short form
|
|
210
|
+
|
|
211
|
+
For a single service you can skip the `servers:` block entirely:
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
name: site
|
|
215
|
+
hosts: [203.0.113.10]
|
|
216
|
+
port: 3000
|
|
217
|
+
domains: [example.com]
|
|
218
|
+
proxy:
|
|
219
|
+
email: me@example.com
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Routes: several sites, paths and ports
|
|
223
|
+
|
|
224
|
+
`domains:` is shorthand. `routes:` gives you the rest — this is the part Kamal
|
|
225
|
+
cannot express:
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
servers:
|
|
229
|
+
web:
|
|
230
|
+
hosts: [203.0.113.10]
|
|
231
|
+
port: 3000
|
|
232
|
+
routes:
|
|
233
|
+
- host: example.com # → :3000
|
|
234
|
+
- host: api.example.com
|
|
235
|
+
port: 8080 # a different port, same container
|
|
236
|
+
- host: example.com
|
|
237
|
+
path: /admin
|
|
238
|
+
port: 4000
|
|
239
|
+
strip_path: true # /admin/x reaches the app as /x
|
|
240
|
+
basic_auth: ["admin:$apr1$…"]
|
|
241
|
+
- host: www.example.com
|
|
242
|
+
redirect: https://example.com
|
|
243
|
+
- host: "*.example.com" # wildcard (needs a DNS challenge)
|
|
244
|
+
priority: 1
|
|
245
|
+
- host: internal.example.com
|
|
246
|
+
entrypoint: private # a non-standard port, see below
|
|
247
|
+
ssl: false
|
|
248
|
+
- host: example.com
|
|
249
|
+
path: /ws
|
|
250
|
+
sticky: true # cookie-pinned load balancing
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Extra entrypoints are ports Traefik listens on, declared once and shared by
|
|
254
|
+
every app on the server:
|
|
255
|
+
|
|
256
|
+
```yaml
|
|
257
|
+
proxy:
|
|
258
|
+
entrypoints:
|
|
259
|
+
web: 80
|
|
260
|
+
websecure: 443
|
|
261
|
+
private: 8443
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Environment and secrets
|
|
265
|
+
|
|
266
|
+
`env.clear` is written into the config; `env.secret` lists variable *names*
|
|
267
|
+
whose values are read from your shell or a `.env` file at deploy time and
|
|
268
|
+
written to the server as a `0600` file. Secrets never end up in the repo, in a
|
|
269
|
+
`docker inspect`, or in your shell history.
|
|
270
|
+
|
|
271
|
+
`.env`, `.env.local` and `.env.<destination>` next to `epd.yml` are loaded
|
|
272
|
+
automatically. Real environment variables always win.
|
|
273
|
+
|
|
274
|
+
### Destinations
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
epd deploy -d staging
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
merges `epd.staging.yml` over `epd.yml` and loads `.env.staging`. Useful for a
|
|
281
|
+
staging fleet, a different domain, or fewer replicas.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Multiple servers, load balancing and failover
|
|
286
|
+
|
|
287
|
+
Each server runs a Traefik instance and its own replicas. Two levels of
|
|
288
|
+
balancing are available:
|
|
289
|
+
|
|
290
|
+
**Per host (default).** Traefik balances across the replicas on its own machine
|
|
291
|
+
and drops any replica that fails its health check. Spread traffic across
|
|
292
|
+
machines with round-robin DNS or a load balancer in front.
|
|
293
|
+
|
|
294
|
+
**Across hosts (`proxy.cross_host: true`).** Every server's proxy balances
|
|
295
|
+
across *every* server's replicas. If one machine's app dies — or the whole
|
|
296
|
+
machine does — the others keep serving its share, so DNS round-robin no longer
|
|
297
|
+
sends a fraction of visitors to a dead box.
|
|
298
|
+
|
|
299
|
+
```yaml
|
|
300
|
+
proxy:
|
|
301
|
+
cross_host: true
|
|
302
|
+
private_ips: # optional: keep the traffic on a private network
|
|
303
|
+
203.0.113.10: 10.0.0.10
|
|
304
|
+
203.0.113.11: 10.0.0.11
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
With `cross_host` the app port is published on each host (bound to the private
|
|
308
|
+
address when you set one), so replicas can be reached from the other machines.
|
|
309
|
+
Firewall that port to your servers.
|
|
310
|
+
|
|
311
|
+
Deploys roll one host at a time by default, so the fleet keeps serving
|
|
312
|
+
throughout. `strategy: parallel` deploys everywhere at once when you would
|
|
313
|
+
rather have it over with.
|
|
314
|
+
|
|
315
|
+
### Certificates on several machines
|
|
316
|
+
|
|
317
|
+
Each server gets its own certificate. With the HTTP challenge and round-robin
|
|
318
|
+
DNS, a challenge request can land on a machine that is not the one asking, and
|
|
319
|
+
the first attempt may fail before a retry succeeds. For more than one machine
|
|
320
|
+
use the DNS challenge:
|
|
321
|
+
|
|
322
|
+
```yaml
|
|
323
|
+
proxy:
|
|
324
|
+
challenge: dns
|
|
325
|
+
dns_provider: cloudflare
|
|
326
|
+
dns_env: [CF_DNS_API_TOKEN]
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
The listed variables are read from your environment and passed to Traefik.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Without Docker: process mode
|
|
334
|
+
|
|
335
|
+
```yaml
|
|
336
|
+
name: site
|
|
337
|
+
mode: process
|
|
338
|
+
|
|
339
|
+
process:
|
|
340
|
+
install: bun install --frozen-lockfile
|
|
341
|
+
build: bun run build
|
|
342
|
+
start: bun run start # each replica gets a unique $PORT
|
|
343
|
+
exclude: [".git", "node_modules", ".env"]
|
|
344
|
+
source: rsync # or "git" to upload a clean archive of HEAD
|
|
345
|
+
|
|
346
|
+
servers:
|
|
347
|
+
web:
|
|
348
|
+
hosts: [203.0.113.10]
|
|
349
|
+
replicas: 2
|
|
350
|
+
port: 3000
|
|
351
|
+
domains: [example.com]
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`epd setup` installs rsync, Node (pm2 needs it), Bun and pm2, and downloads the
|
|
355
|
+
Traefik binary — the server never needs Docker. Deploys rsync the project into
|
|
356
|
+
`/var/lib/epd/apps/<app>/releases/<version>`, run your install and build
|
|
357
|
+
commands there, start the new replicas under pm2 on the other slot, health check
|
|
358
|
+
them, switch the routes and then delete the old slot.
|
|
359
|
+
|
|
360
|
+
Your start command is run as-is with `PORT`, `EPD_REPLICA`, `EPD_VERSION` and
|
|
361
|
+
your `env` entries set. Two replicas means two processes on two ports, balanced
|
|
362
|
+
by Traefik.
|
|
363
|
+
|
|
364
|
+
Everything else works the same: `epd status`, `epd logs`, `epd rollback`,
|
|
365
|
+
`epd exec`. Accessories are the one exception — they are always containers, so
|
|
366
|
+
they need Docker on their host.
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Commands
|
|
371
|
+
|
|
372
|
+
| Command | What it does |
|
|
373
|
+
| --- | --- |
|
|
374
|
+
| `epd init` | Create an `epd.yml` (and a starter Dockerfile) |
|
|
375
|
+
| `epd setup` | Install what the servers need, start the proxy, deploy |
|
|
376
|
+
| `epd deploy` | Build and deploy; `--no-build` to reuse a version |
|
|
377
|
+
| `epd redeploy` | Restart the current version everywhere |
|
|
378
|
+
| `epd rollback [version]` | Back to the previous version; `--list` to see them |
|
|
379
|
+
| `epd status` | Every replica, slot, version, accessories and the proxy state |
|
|
380
|
+
| `epd logs -f` | Logs from every replica, prefixed by name (supports `--accessory`) |
|
|
381
|
+
| `epd exec -- <cmd>` | Run a command with the app's image and environment (supports `--accessory`) |
|
|
382
|
+
| `epd shell` | A shell inside a container or accessory that is serving traffic |
|
|
383
|
+
| `epd proxy status\|reboot\|logs\|routes\|remove` | The shared proxy |
|
|
384
|
+
| `epd config` | The resolved configuration; `--traefik` for the proxy files |
|
|
385
|
+
| `epd docker-command` | Print the exact `docker run` (or pm2) commands epd uses (including accessories) |
|
|
386
|
+
| `epd lock status\|release` | The deploy lock |
|
|
387
|
+
| `epd remove` | Stop this app and delete its files from the servers |
|
|
388
|
+
|
|
389
|
+
Global flags: `-c/--config`, `-d/--destination`, `-v/--verbose`.
|
|
390
|
+
|
|
391
|
+
### Generating the docker command
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
epd docker-command --service web --slot blue
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
prints the exact, copy-pasteable `docker run` for each replica plus the proxy
|
|
398
|
+
container — useful to check what epd is doing, to run something by hand, or to
|
|
399
|
+
lift the command into another tool. In process mode it prints the pm2
|
|
400
|
+
equivalent.
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## How it fits together on a server
|
|
405
|
+
|
|
406
|
+
```
|
|
407
|
+
/var/lib/epd/
|
|
408
|
+
apps/<app>/ state.json, env files, releases (process mode)
|
|
409
|
+
proxy/
|
|
410
|
+
traefik.yml static config, merged from every app on the host
|
|
411
|
+
dynamic/<app>.yml one routing file per app — this is what makes
|
|
412
|
+
acme/ multiple sites on one machine painless
|
|
413
|
+
ports/ the port block each app reserved
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Containers are named `epd-<app>-<service>-<slot>-<replica>` and labelled with
|
|
417
|
+
the app, service, slot and version, so everything epd owns is easy to find and
|
|
418
|
+
nothing else on the machine is touched.
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## Troubleshooting
|
|
423
|
+
|
|
424
|
+
**`epd status` says a host is unreachable.** epd uses your `ssh` binary and
|
|
425
|
+
config. Check with `ssh -p <port> <user>@<host>` first; add `ssh.key` if you
|
|
426
|
+
have many keys loaded.
|
|
427
|
+
|
|
428
|
+
**The health check times out.** The new replicas started but did not answer.
|
|
429
|
+
`epd logs --service web` shows why. The old version is still serving — nothing
|
|
430
|
+
was switched. Point `healthcheck.path` at something cheap that returns 2xx, or
|
|
431
|
+
set `healthcheck: false` for a service that is not an HTTP server.
|
|
432
|
+
|
|
433
|
+
**A certificate is not issued.** `epd proxy logs` shows the ACME exchange. The
|
|
434
|
+
domain must resolve to the server and port 80 must be reachable. On multiple
|
|
435
|
+
machines, use the DNS challenge.
|
|
436
|
+
|
|
437
|
+
**`ports NNNNN-NNNNN are already reserved`.** Two apps hashed to the same port
|
|
438
|
+
block. Set `port_base:` in one of them to any free multiple of 256.
|
|
439
|
+
|
|
440
|
+
**A deploy was interrupted and now everything is locked.** `epd lock release`.
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Tests
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
bun test # config, routing, port allocation, health probes
|
|
448
|
+
./test/e2e/run.sh # real deploy over ssh: docker mode, two apps, rollback
|
|
449
|
+
./test/e2e/process.sh # real deploy over ssh: process mode, pm2, no docker
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
The end-to-end tests build a throwaway "server" container running sshd and
|
|
453
|
+
deploy to it for real — including a zero-downtime check that fails if any
|
|
454
|
+
request drops during a deploy.
|
|
455
|
+
|
|
456
|
+
## License
|
|
457
|
+
|
|
458
|
+
MIT
|
package/bin/epd.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const distBundle = join(__dirname, "..", "dist", "cli.js");
|
|
8
|
+
|
|
9
|
+
if (existsSync(distBundle)) {
|
|
10
|
+
await import(distBundle);
|
|
11
|
+
} else {
|
|
12
|
+
await import("../src/cli.ts");
|
|
13
|
+
}
|