dokku-compose 0.6.13 → 0.8.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/README.md +67 -408
- package/dist/index.js +168 -65
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,396 +16,82 @@
|
|
|
16
16
|
|
|
17
17
|
## Why
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```yaml
|
|
26
|
-
dokku:
|
|
27
|
-
version: "0.35.12"
|
|
28
|
-
|
|
29
|
-
plugins:
|
|
30
|
-
postgres:
|
|
31
|
-
url: https://github.com/dokku/dokku-postgres.git
|
|
32
|
-
version: "1.41.0"
|
|
33
|
-
redis:
|
|
34
|
-
url: https://github.com/dokku/dokku-redis.git
|
|
35
|
-
|
|
36
|
-
services:
|
|
37
|
-
api-postgres:
|
|
38
|
-
plugin: postgres
|
|
39
|
-
version: "17-3.5"
|
|
40
|
-
shared-cache:
|
|
41
|
-
plugin: redis
|
|
42
|
-
|
|
43
|
-
networks:
|
|
44
|
-
- backend-net
|
|
45
|
-
|
|
46
|
-
domains:
|
|
47
|
-
- example.com
|
|
48
|
-
|
|
49
|
-
nginx:
|
|
50
|
-
client-max-body-size: "50m"
|
|
51
|
-
|
|
52
|
-
logs:
|
|
53
|
-
max-size: "50m"
|
|
54
|
-
|
|
55
|
-
apps:
|
|
56
|
-
api:
|
|
57
|
-
build:
|
|
58
|
-
context: apps/api
|
|
59
|
-
dockerfile: docker/prod/api/Dockerfile
|
|
60
|
-
env:
|
|
61
|
-
APP_ENV: production
|
|
62
|
-
APP_SECRET: "${SECRET_KEY}"
|
|
63
|
-
domains:
|
|
64
|
-
- api.example.com
|
|
65
|
-
links:
|
|
66
|
-
- api-postgres
|
|
67
|
-
- shared-cache
|
|
68
|
-
networks:
|
|
69
|
-
- backend-net
|
|
70
|
-
ports:
|
|
71
|
-
- "https:4001:4000"
|
|
72
|
-
ssl:
|
|
73
|
-
certfile: certs/example.com/fullchain.pem
|
|
74
|
-
keyfile: certs/example.com/privkey.pem
|
|
75
|
-
storage:
|
|
76
|
-
- "/var/lib/dokku/data/storage/api/uploads:/app/uploads"
|
|
77
|
-
nginx:
|
|
78
|
-
client-max-body-size: "15m"
|
|
79
|
-
proxy-read-timeout: "120s"
|
|
80
|
-
checks:
|
|
81
|
-
wait-to-retire: 60
|
|
82
|
-
disabled:
|
|
83
|
-
- worker
|
|
84
|
-
|
|
85
|
-
worker:
|
|
86
|
-
links:
|
|
87
|
-
- api-postgres
|
|
88
|
-
- shared-cache
|
|
89
|
-
checks: false
|
|
90
|
-
proxy:
|
|
91
|
-
enabled: false
|
|
92
|
-
```
|
|
19
|
+
Dokku is a battle-tested, single-server PaaS — and one of the best platforms for self-hosting. But configuring it means running dozens of imperative commands in the right order. Miss one and your deploy breaks. Change servers and you're starting over.
|
|
20
|
+
|
|
21
|
+
AI agents can generate and deploy code better than ever, but they can't reason about infrastructure that lives as a sequence of shell commands. There's no file to diff, no history to track, no way to review changes in a PR.
|
|
22
|
+
|
|
23
|
+
`dokku-compose` makes Dokku declarative. One YAML file. Git-trackable. AI-friendly. Like Docker Compose, but for Dokku.
|
|
93
24
|
|
|
94
25
|
## Quick Start
|
|
95
26
|
|
|
96
27
|
```bash
|
|
97
|
-
# Install
|
|
98
28
|
npm install -g dokku-compose
|
|
99
|
-
|
|
100
|
-
# Create a starter config
|
|
101
|
-
dokku-compose init myapp
|
|
102
|
-
|
|
103
|
-
# Preview what will happen
|
|
104
|
-
dokku-compose up --dry-run
|
|
105
|
-
|
|
106
|
-
# Apply configuration
|
|
107
|
-
dokku-compose up
|
|
108
|
-
|
|
109
|
-
# Or apply to a remote server over SSH
|
|
110
|
-
DOKKU_HOST=my-server.example.com dokku-compose up
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Requires Node.js >= 18. See the [Installation Reference →](docs/reference/install.md) for requirements and remote execution details.
|
|
114
|
-
|
|
115
|
-
## Features
|
|
116
|
-
|
|
117
|
-
Features are listed roughly in execution order — the sequence `dokku-compose up` follows.
|
|
118
|
-
|
|
119
|
-
### Dokku Version Check
|
|
120
|
-
|
|
121
|
-
Declare the expected Dokku version. A warning is logged if the running version doesn't match.
|
|
122
|
-
|
|
123
|
-
```yaml
|
|
124
|
-
dokku:
|
|
125
|
-
version: "0.35.12"
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
```
|
|
129
|
-
[dokku ] WARN: Version mismatch: running 0.34.0, config expects 0.35.12
|
|
130
29
|
```
|
|
131
30
|
|
|
132
|
-
|
|
31
|
+
Requires Node.js >= 20. See the [Installation Reference](docs/reference/install.md) for details.
|
|
133
32
|
|
|
134
|
-
###
|
|
33
|
+
### 1. Export your existing server
|
|
135
34
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
```yaml
|
|
139
|
-
apps:
|
|
140
|
-
api:
|
|
141
|
-
# per-app configuration goes here
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
[Application Management Reference →](docs/reference/apps.md)
|
|
145
|
-
|
|
146
|
-
### Environment Variables
|
|
147
|
-
|
|
148
|
-
Set config vars per app or globally. All declared vars are converged — orphaned vars are automatically unset.
|
|
149
|
-
|
|
150
|
-
```yaml
|
|
151
|
-
apps:
|
|
152
|
-
api:
|
|
153
|
-
env:
|
|
154
|
-
APP_ENV: production
|
|
155
|
-
APP_SECRET: "${SECRET_KEY}"
|
|
156
|
-
```
|
|
35
|
+
Point at your Dokku server and generate a config file from its current state:
|
|
157
36
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
### Build
|
|
161
|
-
|
|
162
|
-
Configure Dockerfile builds: build context, Dockerfile path, app.json location, and build args. Key names follow docker-compose conventions.
|
|
163
|
-
|
|
164
|
-
```yaml
|
|
165
|
-
apps:
|
|
166
|
-
api:
|
|
167
|
-
build:
|
|
168
|
-
context: apps/api
|
|
169
|
-
dockerfile: docker/prod/api/Dockerfile
|
|
170
|
-
app_json: docker/prod/api/app.json
|
|
171
|
-
args:
|
|
172
|
-
SENTRY_AUTH_TOKEN: "${SENTRY_AUTH_TOKEN}"
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
[Build Reference →](docs/reference/builder.md)
|
|
176
|
-
|
|
177
|
-
### Docker Options
|
|
178
|
-
|
|
179
|
-
Add custom Docker options per build phase (`build`, `deploy`, `run`). Options are converged with targeted add/remove — only changed options are modified, preserving entries managed by other resources (e.g. `--link`, `--build-arg`).
|
|
180
|
-
|
|
181
|
-
```yaml
|
|
182
|
-
apps:
|
|
183
|
-
api:
|
|
184
|
-
docker_options:
|
|
185
|
-
deploy:
|
|
186
|
-
- "--shm-size 256m"
|
|
187
|
-
run:
|
|
188
|
-
- "--ulimit nofile=12"
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
[Docker Options Reference →](docs/reference/docker_options.md)
|
|
192
|
-
|
|
193
|
-
### Networks
|
|
194
|
-
|
|
195
|
-
Create shared Docker networks and configure per-app network properties.
|
|
196
|
-
|
|
197
|
-
```yaml
|
|
198
|
-
networks:
|
|
199
|
-
- backend-net
|
|
200
|
-
|
|
201
|
-
apps:
|
|
202
|
-
api:
|
|
203
|
-
networks: # attach-post-deploy
|
|
204
|
-
- backend-net
|
|
205
|
-
network: # other network:set properties
|
|
206
|
-
attach_post_create:
|
|
207
|
-
- init-net
|
|
208
|
-
initial_network: custom-bridge
|
|
209
|
-
bind_all_interfaces: true
|
|
210
|
-
tld: internal
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
`down --force` clears network settings and destroys declared networks.
|
|
214
|
-
|
|
215
|
-
[Networks Reference →](docs/reference/network.md)
|
|
216
|
-
|
|
217
|
-
### Domains
|
|
218
|
-
|
|
219
|
-
Configure custom domains per app or globally.
|
|
220
|
-
|
|
221
|
-
```yaml
|
|
222
|
-
domains:
|
|
223
|
-
- example.com
|
|
224
|
-
|
|
225
|
-
apps:
|
|
226
|
-
api:
|
|
227
|
-
domains:
|
|
228
|
-
- api.example.com
|
|
229
|
-
- api.example.co
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
[Domains Reference →](docs/reference/domains.md)
|
|
233
|
-
|
|
234
|
-
### Port Mappings
|
|
235
|
-
|
|
236
|
-
Map external ports to container ports using `SCHEME:HOST_PORT:CONTAINER_PORT` format.
|
|
237
|
-
|
|
238
|
-
```yaml
|
|
239
|
-
apps:
|
|
240
|
-
api:
|
|
241
|
-
ports:
|
|
242
|
-
- "https:4001:4000"
|
|
37
|
+
```bash
|
|
38
|
+
DOKKU_HOST=my-server.example.com dokku-compose export -o dokku-compose.yml
|
|
243
39
|
```
|
|
244
40
|
|
|
245
|
-
|
|
41
|
+
This produces a complete `dokku-compose.yml` reflecting everything on the server — apps, services, domains, env vars, and more.
|
|
246
42
|
|
|
247
|
-
|
|
43
|
+
### 2. See what's in sync
|
|
248
44
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
Specify cert and key file paths. Idempotent — skips if SSL is already enabled. Set to `false` to remove an existing certificate.
|
|
252
|
-
|
|
253
|
-
```yaml
|
|
254
|
-
apps:
|
|
255
|
-
api:
|
|
256
|
-
ssl: # add cert (idempotent)
|
|
257
|
-
certfile: certs/example.com/fullchain.pem
|
|
258
|
-
keyfile: certs/example.com/privkey.pem
|
|
259
|
-
worker:
|
|
260
|
-
ssl: false # remove cert
|
|
45
|
+
```bash
|
|
46
|
+
dokku-compose diff
|
|
261
47
|
```
|
|
262
48
|
|
|
263
|
-
[SSL Certificates Reference →](docs/reference/certs.md)
|
|
264
|
-
|
|
265
|
-
### Proxy
|
|
266
|
-
|
|
267
|
-
Enable or disable the proxy for an app, and optionally select the proxy implementation (nginx, caddy, haproxy, traefik). All operations are idempotent.
|
|
268
|
-
|
|
269
|
-
```yaml
|
|
270
|
-
apps:
|
|
271
|
-
api:
|
|
272
|
-
proxy: true # shorthand enable
|
|
273
|
-
|
|
274
|
-
worker:
|
|
275
|
-
proxy: false # shorthand disable
|
|
276
|
-
|
|
277
|
-
caddy-app:
|
|
278
|
-
proxy:
|
|
279
|
-
enabled: true
|
|
280
|
-
type: caddy # proxy:set caddy-app caddy
|
|
281
49
|
```
|
|
50
|
+
app: api
|
|
51
|
+
(in sync)
|
|
52
|
+
app: worker
|
|
53
|
+
~ env: 1 → 2 items
|
|
54
|
+
+ ports: (not set on server)
|
|
282
55
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
### Persistent Storage
|
|
286
|
-
|
|
287
|
-
Declare persistent bind mounts for an app. Mounts are fully converged on each `up` run — new mounts are added, mounts removed from YAML are unmounted, and existing mounts are skipped.
|
|
288
|
-
|
|
289
|
-
```yaml
|
|
290
|
-
apps:
|
|
291
|
-
api:
|
|
292
|
-
storage:
|
|
293
|
-
- "/var/lib/dokku/data/storage/api/uploads:/app/uploads"
|
|
56
|
+
1 resource(s) out of sync.
|
|
294
57
|
```
|
|
295
58
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
[Storage Reference →](docs/reference/storage.md)
|
|
299
|
-
|
|
300
|
-
### Nginx Configuration
|
|
301
|
-
|
|
302
|
-
Set any nginx property supported by Dokku via a key-value map — per-app or globally.
|
|
59
|
+
### 3. Preview changes
|
|
303
60
|
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
client-max-body-size: "50m"
|
|
307
|
-
|
|
308
|
-
apps:
|
|
309
|
-
api:
|
|
310
|
-
nginx: # per-app overrides
|
|
311
|
-
client-max-body-size: "15m"
|
|
312
|
-
proxy-read-timeout: "120s"
|
|
61
|
+
```bash
|
|
62
|
+
dokku-compose up --dry-run
|
|
313
63
|
```
|
|
314
64
|
|
|
315
|
-
[Nginx Reference →](docs/reference/nginx.md)
|
|
316
|
-
|
|
317
|
-
### Zero-Downtime Checks
|
|
318
|
-
|
|
319
|
-
Configure zero-downtime deploy check properties, disable checks entirely, or control per process type. Properties are idempotent — current values are checked before setting.
|
|
320
|
-
|
|
321
|
-
```yaml
|
|
322
|
-
apps:
|
|
323
|
-
api:
|
|
324
|
-
checks:
|
|
325
|
-
wait-to-retire: 60
|
|
326
|
-
attempts: 5
|
|
327
|
-
disabled:
|
|
328
|
-
- worker
|
|
329
|
-
skipped:
|
|
330
|
-
- cron
|
|
331
|
-
worker:
|
|
332
|
-
checks: false # disable all checks (causes downtime)
|
|
333
65
|
```
|
|
66
|
+
[worker ] Setting 2 env var(s)... (dry run)
|
|
67
|
+
[worker ] Setting ports http:5000:5000... (dry run)
|
|
334
68
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
Configure log retention and shipping globally or per-app.
|
|
340
|
-
|
|
341
|
-
```yaml
|
|
342
|
-
logs: # global defaults
|
|
343
|
-
max-size: "50m"
|
|
344
|
-
vector-sink: "console://?encoding[codec]=json"
|
|
345
|
-
|
|
346
|
-
apps:
|
|
347
|
-
api:
|
|
348
|
-
logs: # per-app overrides
|
|
349
|
-
max-size: "10m"
|
|
69
|
+
# Commands that would run:
|
|
70
|
+
dokku config:set --no-restart worker APP_ENV=production WORKER_COUNT=****
|
|
71
|
+
dokku ports:set worker http:5000:5000
|
|
350
72
|
```
|
|
351
73
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
### Plugins and Services
|
|
355
|
-
|
|
356
|
-
Install plugins and declare service instances. Services are created before apps during `up` and linked on demand.
|
|
357
|
-
|
|
358
|
-
```yaml
|
|
359
|
-
plugins:
|
|
360
|
-
postgres:
|
|
361
|
-
url: https://github.com/dokku/dokku-postgres.git
|
|
362
|
-
version: "1.41.0"
|
|
74
|
+
### 4. Apply
|
|
363
75
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
plugin: postgres
|
|
367
|
-
|
|
368
|
-
apps:
|
|
369
|
-
api:
|
|
370
|
-
links:
|
|
371
|
-
- api-postgres
|
|
76
|
+
```bash
|
|
77
|
+
dokku-compose up
|
|
372
78
|
```
|
|
373
79
|
|
|
374
|
-
|
|
80
|
+
Running `up` again produces no changes — every step checks current state before acting.
|
|
375
81
|
|
|
376
82
|
## Commands
|
|
377
83
|
|
|
378
84
|
| Command | Description |
|
|
379
85
|
|---------|-------------|
|
|
380
|
-
| `dokku-compose
|
|
381
|
-
| `dokku-compose
|
|
382
|
-
| `dokku-compose down --force` | Destroy apps and services (requires `--force`) |
|
|
383
|
-
| `dokku-compose ps` | Show status of configured apps |
|
|
384
|
-
| `dokku-compose validate` | Validate config file offline (no server contact) |
|
|
385
|
-
| `dokku-compose export` | Reverse-engineer server state into YAML |
|
|
86
|
+
| `dokku-compose up [apps...]` | Create/update apps and services to match config |
|
|
87
|
+
| `dokku-compose down --force [apps...]` | Destroy apps and services (requires `--force`) |
|
|
386
88
|
| `dokku-compose diff` | Show what's out of sync between config and server |
|
|
89
|
+
| `dokku-compose export` | Export current server state to YAML |
|
|
90
|
+
| `dokku-compose ps [apps...]` | Show status of configured apps |
|
|
91
|
+
| `dokku-compose validate` | Validate config file offline (no server contact) |
|
|
92
|
+
| `dokku-compose init [apps...]` | Create a starter `dokku-compose.yml` |
|
|
387
93
|
|
|
388
|
-
###
|
|
389
|
-
|
|
390
|
-
Queries each configured app and prints its deploy status:
|
|
391
|
-
|
|
392
|
-
```
|
|
393
|
-
$ dokku-compose ps
|
|
394
|
-
api running
|
|
395
|
-
worker running
|
|
396
|
-
web not created
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
### `down` — Tear Down
|
|
400
|
-
|
|
401
|
-
Destroys apps and their linked services. Requires `--force` as a safety measure. For each app, services are unlinked first, then the app is destroyed. Service instances from the top-level `services:` section are destroyed after all apps.
|
|
402
|
-
|
|
403
|
-
```bash
|
|
404
|
-
dokku-compose down --force myapp # Destroy one app and its services
|
|
405
|
-
dokku-compose down --force # Destroy all configured apps
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
## Options
|
|
94
|
+
### Options
|
|
409
95
|
|
|
410
96
|
| Option | Description |
|
|
411
97
|
|--------|-------------|
|
|
@@ -415,18 +101,30 @@ dokku-compose down --force # Destroy all configured apps
|
|
|
415
101
|
| `--fail-fast` | Stop on first error (default: continue to next app) |
|
|
416
102
|
| `--remove-orphans` | Destroy services and networks not in config |
|
|
417
103
|
| `--verbose` | Show git-style +/- diff (diff command only) |
|
|
418
|
-
| `--help` | Show usage |
|
|
419
|
-
| `--version` | Show version |
|
|
420
104
|
|
|
421
|
-
##
|
|
105
|
+
## Features
|
|
422
106
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
107
|
+
All features are idempotent — running `up` twice produces no changes.
|
|
108
|
+
|
|
109
|
+
| Feature | Description | Reference |
|
|
110
|
+
|---------|-------------|-----------|
|
|
111
|
+
| Apps | Create and destroy Dokku apps | [apps](docs/reference/apps.md) |
|
|
112
|
+
| Environment Variables | Set config vars per app or globally, with full convergence | [config](docs/reference/config.md) |
|
|
113
|
+
| Build | Dockerfile path, build context, app.json, build args | [builder](docs/reference/builder.md) |
|
|
114
|
+
| Docker Options | Custom Docker options per phase (build/deploy/run) | [docker_options](docs/reference/docker_options.md) |
|
|
115
|
+
| Networks | Create shared Docker networks, attach to apps | [network](docs/reference/network.md) |
|
|
116
|
+
| Domains | Configure domains per app or globally | [domains](docs/reference/domains.md) |
|
|
117
|
+
| Port Mappings | Map external ports to container ports | [ports](docs/reference/ports.md) |
|
|
118
|
+
| SSL Certificates | Add or remove SSL certs | [certs](docs/reference/certs.md) |
|
|
119
|
+
| Proxy | Enable/disable proxy, select implementation | [proxy](docs/reference/proxy.md) |
|
|
120
|
+
| Storage | Persistent bind mounts with full convergence | [storage](docs/reference/storage.md) |
|
|
121
|
+
| Nginx | Set any nginx property per app or globally | [nginx](docs/reference/nginx.md) |
|
|
122
|
+
| Zero-Downtime Checks | Configure deploy checks, disable per process type | [checks](docs/reference/checks.md) |
|
|
123
|
+
| Log Management | Log retention and vector sink configuration | [logs](docs/reference/logs.md) |
|
|
124
|
+
| Plugins | Install Dokku plugins declaratively | [plugins](docs/reference/plugins.md) |
|
|
125
|
+
| Postgres | Postgres services with optional S3 backups | [postgres](docs/reference/postgres.md) |
|
|
126
|
+
| Redis | Redis service instances | [redis](docs/reference/redis.md) |
|
|
127
|
+
| Service Links | Link postgres/redis services to apps | [plugins](docs/reference/plugins.md#linking-services-to-apps-appsapplinks) |
|
|
430
128
|
|
|
431
129
|
## Execution Modes
|
|
432
130
|
|
|
@@ -434,52 +132,11 @@ dokku-compose ps # Show status
|
|
|
434
132
|
# Run remotely over SSH (recommended)
|
|
435
133
|
DOKKU_HOST=my-server.example.com dokku-compose up
|
|
436
134
|
|
|
437
|
-
# Run on the Dokku server itself
|
|
135
|
+
# Run on the Dokku server itself
|
|
438
136
|
DOKKU_HOST=localhost dokku-compose up
|
|
439
137
|
```
|
|
440
138
|
|
|
441
|
-
When `DOKKU_HOST` is set, all
|
|
442
|
-
|
|
443
|
-
## What `up` Does
|
|
444
|
-
|
|
445
|
-
Idempotently ensures desired state, in order:
|
|
446
|
-
|
|
447
|
-
1. Check Dokku version (warn on mismatch)
|
|
448
|
-
2. Install missing plugins
|
|
449
|
-
3. Set global config (domains, env vars, nginx defaults)
|
|
450
|
-
4. Create shared networks
|
|
451
|
-
5. Create service instances (from top-level `services:`)
|
|
452
|
-
6. For each app:
|
|
453
|
-
- Create app (if not exists)
|
|
454
|
-
- Set domains, link/unlink services, attach networks
|
|
455
|
-
- Enable/disable proxy, set ports, add SSL, mount storage
|
|
456
|
-
- Configure nginx, checks, logs, env vars, build, and docker options
|
|
457
|
-
|
|
458
|
-
Running `up` twice produces no changes — every step checks current state before acting.
|
|
459
|
-
|
|
460
|
-
`up` is mostly additive. Removing a key (e.g. deleting a `ports:` block) won't remove the corresponding setting from Dokku. The exception is `links:`, which is fully declarative — services not in the list are unlinked. Use `down --force` to fully reset an app, or `--remove-orphans` to destroy services and networks no longer in config.
|
|
461
|
-
|
|
462
|
-
## Output
|
|
463
|
-
|
|
464
|
-
```
|
|
465
|
-
[networks ] Creating backend-net... done
|
|
466
|
-
[services ] Creating api-postgres (postgres 17-3.5)... done
|
|
467
|
-
[services ] Creating api-redis (redis)... done
|
|
468
|
-
[services ] Creating shared-cache (redis)... done
|
|
469
|
-
[api ] Creating app... done
|
|
470
|
-
[api ] Setting domains: api.example.com... done
|
|
471
|
-
[api ] Linking api-postgres... done
|
|
472
|
-
[api ] Linking api-redis... done
|
|
473
|
-
[api ] Linking shared-cache... done
|
|
474
|
-
[api ] Setting ports https:4001:4000... done
|
|
475
|
-
[api ] Adding SSL certificate... done
|
|
476
|
-
[api ] Mounting /var/lib/dokku/data/storage/api/uploads:/app/uploads... done
|
|
477
|
-
[api ] Setting nginx client-max-body-size=15m... done
|
|
478
|
-
[api ] Setting checks wait-to-retire=60... done
|
|
479
|
-
[api ] Setting 2 env var(s)... done
|
|
480
|
-
[worker ] Creating app... already configured
|
|
481
|
-
[worker ] Linking shared-cache... already configured
|
|
482
|
-
```
|
|
139
|
+
When `DOKKU_HOST` is set, all commands are sent over SSH. This is the recommended mode — it works both remotely and on the server. SSH key access to the Dokku server is required.
|
|
483
140
|
|
|
484
141
|
## Architecture
|
|
485
142
|
|
|
@@ -513,7 +170,9 @@ dokku-compose/
|
|
|
513
170
|
│ │ ├── proxy.ts # dokku proxy:*
|
|
514
171
|
│ │ ├── registry.ts # dokku registry:*
|
|
515
172
|
│ │ ├── scheduler.ts # dokku scheduler:*
|
|
516
|
-
│ │ ├──
|
|
173
|
+
│ │ ├── postgres.ts # dokku postgres:* (create, backup, export)
|
|
174
|
+
│ │ ├── redis.ts # dokku redis:* (create, export)
|
|
175
|
+
│ │ ├── links.ts # Service link resolution across plugins
|
|
517
176
|
│ │ └── storage.ts # dokku storage:*
|
|
518
177
|
│ ├── commands/
|
|
519
178
|
│ │ ├── up.ts # up command orchestration
|
|
@@ -542,7 +201,7 @@ bun install
|
|
|
542
201
|
bun test
|
|
543
202
|
|
|
544
203
|
# Run a specific module's tests
|
|
545
|
-
bun test src/modules/
|
|
204
|
+
bun test src/modules/postgres.test.ts
|
|
546
205
|
```
|
|
547
206
|
|
|
548
207
|
Tests use [Bun's test runner](https://bun.sh/docs/cli/test) with a mocked `Runner` — no real Dokku server needed.
|
package/dist/index.js
CHANGED
|
@@ -79,12 +79,15 @@ var ServiceBackupSchema = z.object({
|
|
|
79
79
|
bucket: z.string(),
|
|
80
80
|
auth: ServiceBackupAuthSchema
|
|
81
81
|
});
|
|
82
|
-
var
|
|
83
|
-
plugin: z.string(),
|
|
82
|
+
var PostgresSchema = z.object({
|
|
84
83
|
version: z.string().optional(),
|
|
85
84
|
image: z.string().optional(),
|
|
86
85
|
backup: ServiceBackupSchema.optional()
|
|
87
86
|
});
|
|
87
|
+
var RedisSchema = z.object({
|
|
88
|
+
version: z.string().optional(),
|
|
89
|
+
image: z.string().optional()
|
|
90
|
+
});
|
|
88
91
|
var PluginSchema = z.object({
|
|
89
92
|
url: z.string().url(),
|
|
90
93
|
version: z.string().optional()
|
|
@@ -95,7 +98,8 @@ var ConfigSchema = z.object({
|
|
|
95
98
|
}).optional(),
|
|
96
99
|
plugins: z.record(z.string(), PluginSchema).optional(),
|
|
97
100
|
networks: z.array(z.string()).optional(),
|
|
98
|
-
|
|
101
|
+
postgres: z.record(z.string(), PostgresSchema).optional(),
|
|
102
|
+
redis: z.record(z.string(), RedisSchema).optional(),
|
|
99
103
|
apps: z.record(z.string(), AppSchema),
|
|
100
104
|
domains: z.union([z.array(z.string()), z.literal(false)]).optional(),
|
|
101
105
|
env: EnvMapSchema.optional(),
|
|
@@ -805,7 +809,7 @@ async function exportNetworks(ctx) {
|
|
|
805
809
|
return output.split("\n").map((s) => s.trim()).filter((s) => s && !s.startsWith("=====>") && !DOCKER_BUILTIN_NETWORKS.has(s));
|
|
806
810
|
}
|
|
807
811
|
|
|
808
|
-
// src/modules/
|
|
812
|
+
// src/modules/postgres.ts
|
|
809
813
|
import { createHash as createHash2 } from "crypto";
|
|
810
814
|
function backupHashKey(serviceName) {
|
|
811
815
|
return "DOKKU_COMPOSE_BACKUP_HASH_" + serviceName.toUpperCase().replace(/-/g, "_");
|
|
@@ -813,22 +817,22 @@ function backupHashKey(serviceName) {
|
|
|
813
817
|
function computeBackupHash(backup) {
|
|
814
818
|
return createHash2("sha256").update(JSON.stringify(backup)).digest("hex");
|
|
815
819
|
}
|
|
816
|
-
async function
|
|
820
|
+
async function ensurePostgres(ctx, services) {
|
|
817
821
|
for (const [name, config] of Object.entries(services)) {
|
|
818
822
|
logAction("services", `Ensuring ${name}`);
|
|
819
|
-
const exists = await ctx.check(
|
|
823
|
+
const exists = await ctx.check("postgres:exists", name);
|
|
820
824
|
if (exists) {
|
|
821
825
|
logSkip();
|
|
822
826
|
continue;
|
|
823
827
|
}
|
|
824
|
-
const args = [
|
|
828
|
+
const args = ["postgres:create", name];
|
|
825
829
|
if (config.image) args.push("--image", config.image);
|
|
826
830
|
if (config.version) args.push("--image-version", config.version);
|
|
827
831
|
await ctx.run(...args);
|
|
828
832
|
logDone();
|
|
829
833
|
}
|
|
830
834
|
}
|
|
831
|
-
async function
|
|
835
|
+
async function ensurePostgresBackups(ctx, services) {
|
|
832
836
|
for (const [name, config] of Object.entries(services)) {
|
|
833
837
|
if (!config.backup) continue;
|
|
834
838
|
logAction("services", `Configuring backup for ${name}`);
|
|
@@ -840,9 +844,9 @@ async function ensureServiceBackups(ctx, services) {
|
|
|
840
844
|
continue;
|
|
841
845
|
}
|
|
842
846
|
const { schedule, bucket, auth } = config.backup;
|
|
843
|
-
await ctx.run(
|
|
847
|
+
await ctx.run("postgres:backup-deauth", name);
|
|
844
848
|
await ctx.run(
|
|
845
|
-
|
|
849
|
+
"postgres:backup-auth",
|
|
846
850
|
name,
|
|
847
851
|
auth.access_key_id,
|
|
848
852
|
auth.secret_access_key,
|
|
@@ -850,56 +854,145 @@ async function ensureServiceBackups(ctx, services) {
|
|
|
850
854
|
auth.signature_version,
|
|
851
855
|
auth.endpoint
|
|
852
856
|
);
|
|
853
|
-
await ctx.run(
|
|
857
|
+
await ctx.run("postgres:backup-schedule", name, schedule, bucket);
|
|
854
858
|
await ctx.run("config:set", "--global", `${hashKey}=${desiredHash}`);
|
|
855
859
|
logDone();
|
|
856
860
|
}
|
|
857
861
|
}
|
|
858
|
-
async function
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
await ctx.run(`${serviceConfig.plugin}:link`, serviceName, app, "--no-restart");
|
|
866
|
-
logDone();
|
|
867
|
-
} else if (!isDesired && isLinked) {
|
|
868
|
-
logAction(app, `Unlinking ${serviceName}`);
|
|
869
|
-
await ctx.run(`${serviceConfig.plugin}:unlink`, serviceName, app, "--no-restart");
|
|
870
|
-
logDone();
|
|
862
|
+
async function destroyPostgres(ctx, services) {
|
|
863
|
+
for (const [name] of Object.entries(services)) {
|
|
864
|
+
logAction("services", `Destroying ${name}`);
|
|
865
|
+
const exists = await ctx.check("postgres:exists", name);
|
|
866
|
+
if (!exists) {
|
|
867
|
+
logSkip();
|
|
868
|
+
continue;
|
|
871
869
|
}
|
|
870
|
+
await ctx.run("postgres:destroy", name, "--force");
|
|
871
|
+
logDone();
|
|
872
872
|
}
|
|
873
873
|
}
|
|
874
|
-
async function
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
874
|
+
async function exportPostgres(ctx) {
|
|
875
|
+
const services = {};
|
|
876
|
+
const listOutput = await ctx.query("postgres:list");
|
|
877
|
+
const lines = listOutput.split("\n").slice(1);
|
|
878
|
+
for (const line of lines) {
|
|
879
|
+
const name = line.trim().split(/\s+/)[0];
|
|
880
|
+
if (!name) continue;
|
|
881
|
+
const infoOutput = await ctx.query("postgres:info", name);
|
|
882
|
+
const versionMatch = infoOutput.match(/Version:\s+(\S+)/);
|
|
883
|
+
if (!versionMatch) continue;
|
|
884
|
+
const versionField = versionMatch[1];
|
|
885
|
+
const colonIdx = versionField.lastIndexOf(":");
|
|
886
|
+
const config = {};
|
|
887
|
+
if (colonIdx > 0) {
|
|
888
|
+
const image = versionField.slice(0, colonIdx);
|
|
889
|
+
const version2 = versionField.slice(colonIdx + 1);
|
|
890
|
+
if (image !== "postgres") config.image = image;
|
|
891
|
+
if (version2) config.version = version2;
|
|
892
|
+
} else {
|
|
893
|
+
config.version = versionField;
|
|
881
894
|
}
|
|
895
|
+
services[name] = config;
|
|
882
896
|
}
|
|
897
|
+
return services;
|
|
883
898
|
}
|
|
884
|
-
|
|
899
|
+
|
|
900
|
+
// src/modules/redis.ts
|
|
901
|
+
async function ensureRedis(ctx, services) {
|
|
885
902
|
for (const [name, config] of Object.entries(services)) {
|
|
903
|
+
logAction("services", `Ensuring ${name}`);
|
|
904
|
+
const exists = await ctx.check("redis:exists", name);
|
|
905
|
+
if (exists) {
|
|
906
|
+
logSkip();
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
const args = ["redis:create", name];
|
|
910
|
+
if (config.image) args.push("--image", config.image);
|
|
911
|
+
if (config.version) args.push("--image-version", config.version);
|
|
912
|
+
await ctx.run(...args);
|
|
913
|
+
logDone();
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
async function destroyRedis(ctx, services) {
|
|
917
|
+
for (const [name] of Object.entries(services)) {
|
|
886
918
|
logAction("services", `Destroying ${name}`);
|
|
887
|
-
const exists = await ctx.check(
|
|
919
|
+
const exists = await ctx.check("redis:exists", name);
|
|
888
920
|
if (!exists) {
|
|
889
921
|
logSkip();
|
|
890
922
|
continue;
|
|
891
923
|
}
|
|
892
|
-
await ctx.run(
|
|
924
|
+
await ctx.run("redis:destroy", name, "--force");
|
|
893
925
|
logDone();
|
|
894
926
|
}
|
|
895
927
|
}
|
|
896
|
-
async function
|
|
897
|
-
|
|
928
|
+
async function exportRedis(ctx) {
|
|
929
|
+
const services = {};
|
|
930
|
+
const listOutput = await ctx.query("redis:list");
|
|
931
|
+
const lines = listOutput.split("\n").slice(1);
|
|
932
|
+
for (const line of lines) {
|
|
933
|
+
const name = line.trim().split(/\s+/)[0];
|
|
934
|
+
if (!name) continue;
|
|
935
|
+
const infoOutput = await ctx.query("redis:info", name);
|
|
936
|
+
const versionMatch = infoOutput.match(/Version:\s+(\S+)/);
|
|
937
|
+
if (!versionMatch) continue;
|
|
938
|
+
const versionField = versionMatch[1];
|
|
939
|
+
const colonIdx = versionField.lastIndexOf(":");
|
|
940
|
+
const config = {};
|
|
941
|
+
if (colonIdx > 0) {
|
|
942
|
+
const image = versionField.slice(0, colonIdx);
|
|
943
|
+
const version2 = versionField.slice(colonIdx + 1);
|
|
944
|
+
if (image !== "redis") config.image = image;
|
|
945
|
+
if (version2) config.version = version2;
|
|
946
|
+
} else {
|
|
947
|
+
config.version = versionField;
|
|
948
|
+
}
|
|
949
|
+
services[name] = config;
|
|
950
|
+
}
|
|
951
|
+
return services;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/modules/links.ts
|
|
955
|
+
function resolveServicePlugin(name, config) {
|
|
956
|
+
if (config.postgres?.[name]) return { plugin: "postgres", config: config.postgres[name] };
|
|
957
|
+
if (config.redis?.[name]) return { plugin: "redis", config: config.redis[name] };
|
|
958
|
+
return void 0;
|
|
959
|
+
}
|
|
960
|
+
function allServices(config) {
|
|
961
|
+
const entries = [];
|
|
962
|
+
for (const name of Object.keys(config.postgres ?? {})) entries.push([name, "postgres"]);
|
|
963
|
+
for (const name of Object.keys(config.redis ?? {})) entries.push([name, "redis"]);
|
|
964
|
+
return entries;
|
|
965
|
+
}
|
|
966
|
+
async function ensureAppLinks(ctx, app, desiredLinks, config) {
|
|
967
|
+
const desiredSet = new Set(desiredLinks);
|
|
968
|
+
for (const [serviceName, plugin] of allServices(config)) {
|
|
969
|
+
const isLinked = await ctx.check(`${plugin}:linked`, serviceName, app);
|
|
970
|
+
const isDesired = desiredSet.has(serviceName);
|
|
971
|
+
if (isDesired && !isLinked) {
|
|
972
|
+
logAction(app, `Linking ${serviceName}`);
|
|
973
|
+
await ctx.run(`${plugin}:link`, serviceName, app, "--no-restart");
|
|
974
|
+
logDone();
|
|
975
|
+
} else if (!isDesired && isLinked) {
|
|
976
|
+
logAction(app, `Unlinking ${serviceName}`);
|
|
977
|
+
await ctx.run(`${plugin}:unlink`, serviceName, app, "--no-restart");
|
|
978
|
+
logDone();
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
async function destroyAppLinks(ctx, app, links, config) {
|
|
983
|
+
for (const serviceName of links) {
|
|
984
|
+
const resolved = resolveServicePlugin(serviceName, config);
|
|
985
|
+
if (!resolved) continue;
|
|
986
|
+
const isLinked = await ctx.check(`${resolved.plugin}:linked`, serviceName, app);
|
|
987
|
+
if (isLinked) {
|
|
988
|
+
await ctx.run(`${resolved.plugin}:unlink`, serviceName, app, "--no-restart");
|
|
989
|
+
}
|
|
990
|
+
}
|
|
898
991
|
}
|
|
899
|
-
async function exportAppLinks(ctx, app,
|
|
992
|
+
async function exportAppLinks(ctx, app, config) {
|
|
900
993
|
const linked = [];
|
|
901
|
-
for (const [serviceName,
|
|
902
|
-
const isLinked = await ctx.check(`${
|
|
994
|
+
for (const [serviceName, plugin] of allServices(config)) {
|
|
995
|
+
const isLinked = await ctx.check(`${plugin}:linked`, serviceName, app);
|
|
903
996
|
if (isLinked) linked.push(serviceName);
|
|
904
997
|
}
|
|
905
998
|
return linked;
|
|
@@ -946,8 +1039,9 @@ async function runUp(ctx, config, appFilter) {
|
|
|
946
1039
|
if (config.logs !== void 0) await ensureGlobalLogs(ctx, config.logs);
|
|
947
1040
|
if (config.nginx !== void 0) await ensureGlobalNginx(ctx, config.nginx);
|
|
948
1041
|
if (config.networks) await ensureNetworks(ctx, config.networks);
|
|
949
|
-
if (config.
|
|
950
|
-
if (config.
|
|
1042
|
+
if (config.postgres) await ensurePostgres(ctx, config.postgres);
|
|
1043
|
+
if (config.redis) await ensureRedis(ctx, config.redis);
|
|
1044
|
+
if (config.postgres) await ensurePostgresBackups(ctx, config.postgres);
|
|
951
1045
|
for (const app of apps) {
|
|
952
1046
|
const appConfig = config.apps[app];
|
|
953
1047
|
if (!appConfig) continue;
|
|
@@ -957,8 +1051,8 @@ async function runUp(ctx, config, appFilter) {
|
|
|
957
1051
|
await reconcile(NetworkProps, ctx, app, appConfig.network);
|
|
958
1052
|
await reconcile(Proxy, ctx, app, appConfig.proxy?.enabled);
|
|
959
1053
|
await reconcile(Ports, ctx, app, appConfig.ports);
|
|
960
|
-
if (config.
|
|
961
|
-
await ensureAppLinks(ctx, app, appConfig.links ?? [], config
|
|
1054
|
+
if (config.postgres || config.redis) {
|
|
1055
|
+
await ensureAppLinks(ctx, app, appConfig.links ?? [], config);
|
|
962
1056
|
}
|
|
963
1057
|
await reconcile(Certs, ctx, app, appConfig.ssl);
|
|
964
1058
|
await reconcile(Storage, ctx, app, appConfig.storage);
|
|
@@ -998,14 +1092,13 @@ async function runDown(ctx, config, appFilter, opts) {
|
|
|
998
1092
|
for (const app of apps) {
|
|
999
1093
|
const appConfig = config.apps[app];
|
|
1000
1094
|
if (!appConfig) continue;
|
|
1001
|
-
if (
|
|
1002
|
-
await destroyAppLinks(ctx, app, appConfig.links, config
|
|
1095
|
+
if (appConfig.links && (config.postgres || config.redis)) {
|
|
1096
|
+
await destroyAppLinks(ctx, app, appConfig.links, config);
|
|
1003
1097
|
}
|
|
1004
1098
|
await destroyApp(ctx, app);
|
|
1005
1099
|
}
|
|
1006
|
-
if (config.
|
|
1007
|
-
|
|
1008
|
-
}
|
|
1100
|
+
if (config.postgres) await destroyPostgres(ctx, config.postgres);
|
|
1101
|
+
if (config.redis) await destroyRedis(ctx, config.redis);
|
|
1009
1102
|
if (config.networks) {
|
|
1010
1103
|
for (const net of config.networks) {
|
|
1011
1104
|
logAction("network", `Destroying ${net}`);
|
|
@@ -1058,8 +1151,10 @@ async function runExport(ctx, opts) {
|
|
|
1058
1151
|
const apps = opts.appFilter?.length ? opts.appFilter : await exportApps(ctx);
|
|
1059
1152
|
const networks = await exportNetworks(ctx);
|
|
1060
1153
|
if (networks.length > 0) config.networks = networks;
|
|
1061
|
-
const
|
|
1062
|
-
if (Object.keys(
|
|
1154
|
+
const postgres = await exportPostgres(ctx);
|
|
1155
|
+
if (Object.keys(postgres).length > 0) config.postgres = postgres;
|
|
1156
|
+
const redis = await exportRedis(ctx);
|
|
1157
|
+
if (Object.keys(redis).length > 0) config.redis = redis;
|
|
1063
1158
|
const prefetched = /* @__PURE__ */ new Map();
|
|
1064
1159
|
await Promise.all(
|
|
1065
1160
|
ALL_APP_RESOURCES.filter((r) => !r.forceApply && !r.key.startsWith("_") && r.readAll).map(async (r) => {
|
|
@@ -1082,8 +1177,8 @@ async function runExport(ctx, opts) {
|
|
|
1082
1177
|
appConfig[resource.key] = value;
|
|
1083
1178
|
}
|
|
1084
1179
|
}
|
|
1085
|
-
if (
|
|
1086
|
-
const links = await exportAppLinks(ctx, app,
|
|
1180
|
+
if (config.postgres || config.redis) {
|
|
1181
|
+
const links = await exportAppLinks(ctx, app, config);
|
|
1087
1182
|
if (links.length > 0) appConfig.links = links;
|
|
1088
1183
|
}
|
|
1089
1184
|
config.apps[app] = appConfig;
|
|
@@ -1159,8 +1254,13 @@ async function computeDiff(ctx, config) {
|
|
|
1159
1254
|
}
|
|
1160
1255
|
result.apps[app] = appDiff;
|
|
1161
1256
|
}
|
|
1162
|
-
for (const
|
|
1163
|
-
const exists = await ctx.check(
|
|
1257
|
+
for (const svc of Object.keys(config.postgres ?? {})) {
|
|
1258
|
+
const exists = await ctx.check("postgres:exists", svc);
|
|
1259
|
+
result.services[svc] = { status: exists ? "in-sync" : "missing" };
|
|
1260
|
+
if (!exists) result.inSync = false;
|
|
1261
|
+
}
|
|
1262
|
+
for (const svc of Object.keys(config.redis ?? {})) {
|
|
1263
|
+
const exists = await ctx.check("redis:exists", svc);
|
|
1164
1264
|
result.services[svc] = { status: exists ? "in-sync" : "missing" };
|
|
1165
1265
|
if (!exists) result.inSync = false;
|
|
1166
1266
|
}
|
|
@@ -1266,25 +1366,28 @@ function validate(filePath) {
|
|
|
1266
1366
|
}
|
|
1267
1367
|
const data = raw;
|
|
1268
1368
|
if (data?.apps && typeof data.apps === "object") {
|
|
1269
|
-
const serviceNames = new Set(
|
|
1270
|
-
|
|
1271
|
-
|
|
1369
|
+
const serviceNames = /* @__PURE__ */ new Set();
|
|
1370
|
+
if (data?.postgres && typeof data.postgres === "object") {
|
|
1371
|
+
for (const name of Object.keys(data.postgres)) serviceNames.add(name);
|
|
1372
|
+
}
|
|
1373
|
+
if (data?.redis && typeof data.redis === "object") {
|
|
1374
|
+
for (const name of Object.keys(data.redis)) serviceNames.add(name);
|
|
1375
|
+
}
|
|
1272
1376
|
for (const [appName, appCfg] of Object.entries(data.apps)) {
|
|
1273
1377
|
if (!appCfg?.links) continue;
|
|
1274
1378
|
for (const link of appCfg.links) {
|
|
1275
1379
|
if (!serviceNames.has(link)) {
|
|
1276
|
-
errors.push(`apps.${appName}.links: service "${link}" not defined in
|
|
1380
|
+
errors.push(`apps.${appName}.links: service "${link}" not defined in postgres or redis`);
|
|
1277
1381
|
}
|
|
1278
1382
|
}
|
|
1279
1383
|
}
|
|
1280
1384
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
}
|
|
1385
|
+
const pluginNames = data?.plugins ? new Set(Object.keys(data.plugins)) : /* @__PURE__ */ new Set();
|
|
1386
|
+
if (data?.postgres && pluginNames.size > 0 && !pluginNames.has("postgres")) {
|
|
1387
|
+
warnings.push(`postgres: "postgres" plugin not declared in plugins (may be pre-installed)`);
|
|
1388
|
+
}
|
|
1389
|
+
if (data?.redis && pluginNames.size > 0 && !pluginNames.has("redis")) {
|
|
1390
|
+
warnings.push(`redis: "redis" plugin not declared in plugins (may be pre-installed)`);
|
|
1288
1391
|
}
|
|
1289
1392
|
return { errors, warnings };
|
|
1290
1393
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dokku-compose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Docker Compose for Dokku — declare your entire server in a single YAML file.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": "./dist/index.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"type": "module",
|
|
15
15
|
"engines": {
|
|
16
|
-
"node": ">=
|
|
16
|
+
"node": ">=20"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"test": "bun test",
|