dokku-compose 0.7.0 → 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.
Files changed (3) hide show
  1. package/README.md +67 -408
  2. package/dist/index.js +164 -91
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -16,396 +16,82 @@
16
16
 
17
17
  ## Why
18
18
 
19
- Configuring a Dokku server 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
- `dokku-compose` replaces that with a single YAML file. Like Docker Compose, but for Dokku.
22
-
23
- ## A Complete Example
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
- Dokku must be pre-installed on the target server.
31
+ Requires Node.js >= 20. See the [Installation Reference](docs/reference/install.md) for details.
133
32
 
134
- ### Application Management
33
+ ### 1. Export your existing server
135
34
 
136
- Create and destroy Dokku apps idempotently. If the app already exists, it's skipped.
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
- [Environment Variables Reference →](docs/reference/config.md)
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
- Comparison is order-insensitive. `down --force` clears port mappings before destroying the app.
41
+ This produces a complete `dokku-compose.yml` reflecting everything on the server apps, services, domains, env vars, and more.
246
42
 
247
- [Port Mappings Reference →](docs/reference/ports.md)
43
+ ### 2. See what's in sync
248
44
 
249
- ### SSL Certificates
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
- [Proxy Reference →](docs/reference/proxy.md)
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
- Host directories must exist before mounting. On `down`, declared mounts are unmounted.
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
- ```yaml
305
- nginx: # global defaults
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
- [Zero-Downtime Checks Reference →](docs/reference/checks.md)
336
-
337
- ### Log Management
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
- [Log Management Reference →](docs/reference/logs.md)
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
- services:
365
- api-postgres:
366
- plugin: postgres
367
-
368
- apps:
369
- api:
370
- links:
371
- - api-postgres
76
+ ```bash
77
+ dokku-compose up
372
78
  ```
373
79
 
374
- [Plugins and Services Reference →](docs/reference/plugins.md)
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 init [app...]` | Create a starter `dokku-compose.yml` |
381
- | `dokku-compose up` | Create/update apps and services to match config |
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
- ### `ps` — Show Status
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
- ## Examples
105
+ ## Features
422
106
 
423
- ```bash
424
- dokku-compose up # Configure all apps
425
- dokku-compose up myapp # Configure one app
426
- dokku-compose up --dry-run # Preview changes
427
- dokku-compose down --force myapp # Destroy an app
428
- dokku-compose ps # Show status
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 (use DOKKU_HOST=localhost for best compatibility)
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 Dokku commands are sent over SSH. This is the recommended mode — it works both remotely from your local machine and directly on the server. SSH mode avoids compatibility issues with Dokku's basher environment that can affect some commands when called directly from Node.js. SSH key access to the Dokku server is required.
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
- │ │ ├── services.ts # Service instances, links, plugin scripts
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/services.test.ts
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 ServiceSchema = z.object({
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
- services: z.record(z.string(), ServiceSchema).optional(),
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/services.ts
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 ensureServices(ctx, services) {
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(`${config.plugin}:exists`, name);
823
+ const exists = await ctx.check("postgres:exists", name);
820
824
  if (exists) {
821
825
  logSkip();
822
826
  continue;
823
827
  }
824
- const args = [`${config.plugin}:create`, name];
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 ensureServiceBackups(ctx, services) {
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(`${config.plugin}:backup-deauth`, name);
847
+ await ctx.run("postgres:backup-deauth", name);
844
848
  await ctx.run(
845
- `${config.plugin}:backup-auth`,
849
+ "postgres:backup-auth",
846
850
  name,
847
851
  auth.access_key_id,
848
852
  auth.secret_access_key,
@@ -850,86 +854,145 @@ async function ensureServiceBackups(ctx, services) {
850
854
  auth.signature_version,
851
855
  auth.endpoint
852
856
  );
853
- await ctx.run(`${config.plugin}:backup-schedule`, name, schedule, bucket);
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 ensureAppLinks(ctx, app, desiredLinks, allServices) {
859
- const desiredSet = new Set(desiredLinks);
860
- for (const [serviceName, serviceConfig] of Object.entries(allServices)) {
861
- const isLinked = await ctx.check(`${serviceConfig.plugin}:linked`, serviceName, app);
862
- const isDesired = desiredSet.has(serviceName);
863
- if (isDesired && !isLinked) {
864
- logAction(app, `Linking ${serviceName}`);
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 destroyAppLinks(ctx, app, links, allServices) {
875
- for (const serviceName of links) {
876
- const config = allServices[serviceName];
877
- if (!config) continue;
878
- const isLinked = await ctx.check(`${config.plugin}:linked`, serviceName, app);
879
- if (isLinked) {
880
- await ctx.run(`${config.plugin}:unlink`, serviceName, app, "--no-restart");
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
- async function destroyServices(ctx, services) {
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(`${config.plugin}:exists`, name);
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(`${config.plugin}:destroy`, name, "--force");
924
+ await ctx.run("redis:destroy", name, "--force");
893
925
  logDone();
894
926
  }
895
927
  }
896
- var SERVICE_PLUGINS = ["postgres", "redis"];
897
- async function exportServices(ctx) {
928
+ async function exportRedis(ctx) {
898
929
  const services = {};
899
- const pluginOutput = await ctx.query("plugin:list");
900
- const installedPlugins = new Set(
901
- pluginOutput.split("\n").map((line) => line.trim().split(/\s+/)[0]).filter(Boolean)
902
- );
903
- for (const plugin of SERVICE_PLUGINS) {
904
- if (!installedPlugins.has(plugin)) continue;
905
- const listOutput = await ctx.query(`${plugin}:list`);
906
- const lines = listOutput.split("\n").slice(1);
907
- for (const line of lines) {
908
- const name = line.trim().split(/\s+/)[0];
909
- if (!name) continue;
910
- const infoOutput = await ctx.query(`${plugin}:info`, name);
911
- const versionMatch = infoOutput.match(/Version:\s+(\S+)/);
912
- if (!versionMatch) continue;
913
- const versionField = versionMatch[1];
914
- const colonIdx = versionField.lastIndexOf(":");
915
- const config = { plugin };
916
- if (colonIdx > 0) {
917
- const image = versionField.slice(0, colonIdx);
918
- const version2 = versionField.slice(colonIdx + 1);
919
- if (image !== plugin) config.image = image;
920
- if (version2) config.version = version2;
921
- } else {
922
- config.version = versionField;
923
- }
924
- services[name] = config;
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;
925
948
  }
949
+ services[name] = config;
926
950
  }
927
951
  return services;
928
952
  }
929
- async function exportAppLinks(ctx, app, services) {
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
+ }
991
+ }
992
+ async function exportAppLinks(ctx, app, config) {
930
993
  const linked = [];
931
- for (const [serviceName, config] of Object.entries(services)) {
932
- const isLinked = await ctx.check(`${config.plugin}:linked`, serviceName, app);
994
+ for (const [serviceName, plugin] of allServices(config)) {
995
+ const isLinked = await ctx.check(`${plugin}:linked`, serviceName, app);
933
996
  if (isLinked) linked.push(serviceName);
934
997
  }
935
998
  return linked;
@@ -976,8 +1039,9 @@ async function runUp(ctx, config, appFilter) {
976
1039
  if (config.logs !== void 0) await ensureGlobalLogs(ctx, config.logs);
977
1040
  if (config.nginx !== void 0) await ensureGlobalNginx(ctx, config.nginx);
978
1041
  if (config.networks) await ensureNetworks(ctx, config.networks);
979
- if (config.services) await ensureServices(ctx, config.services);
980
- if (config.services) await ensureServiceBackups(ctx, config.services);
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);
981
1045
  for (const app of apps) {
982
1046
  const appConfig = config.apps[app];
983
1047
  if (!appConfig) continue;
@@ -987,8 +1051,8 @@ async function runUp(ctx, config, appFilter) {
987
1051
  await reconcile(NetworkProps, ctx, app, appConfig.network);
988
1052
  await reconcile(Proxy, ctx, app, appConfig.proxy?.enabled);
989
1053
  await reconcile(Ports, ctx, app, appConfig.ports);
990
- if (config.services) {
991
- await ensureAppLinks(ctx, app, appConfig.links ?? [], config.services);
1054
+ if (config.postgres || config.redis) {
1055
+ await ensureAppLinks(ctx, app, appConfig.links ?? [], config);
992
1056
  }
993
1057
  await reconcile(Certs, ctx, app, appConfig.ssl);
994
1058
  await reconcile(Storage, ctx, app, appConfig.storage);
@@ -1028,14 +1092,13 @@ async function runDown(ctx, config, appFilter, opts) {
1028
1092
  for (const app of apps) {
1029
1093
  const appConfig = config.apps[app];
1030
1094
  if (!appConfig) continue;
1031
- if (config.services && appConfig.links) {
1032
- await destroyAppLinks(ctx, app, appConfig.links, config.services);
1095
+ if (appConfig.links && (config.postgres || config.redis)) {
1096
+ await destroyAppLinks(ctx, app, appConfig.links, config);
1033
1097
  }
1034
1098
  await destroyApp(ctx, app);
1035
1099
  }
1036
- if (config.services) {
1037
- await destroyServices(ctx, config.services);
1038
- }
1100
+ if (config.postgres) await destroyPostgres(ctx, config.postgres);
1101
+ if (config.redis) await destroyRedis(ctx, config.redis);
1039
1102
  if (config.networks) {
1040
1103
  for (const net of config.networks) {
1041
1104
  logAction("network", `Destroying ${net}`);
@@ -1088,8 +1151,10 @@ async function runExport(ctx, opts) {
1088
1151
  const apps = opts.appFilter?.length ? opts.appFilter : await exportApps(ctx);
1089
1152
  const networks = await exportNetworks(ctx);
1090
1153
  if (networks.length > 0) config.networks = networks;
1091
- const services = await exportServices(ctx);
1092
- if (Object.keys(services).length > 0) config.services = services;
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;
1093
1158
  const prefetched = /* @__PURE__ */ new Map();
1094
1159
  await Promise.all(
1095
1160
  ALL_APP_RESOURCES.filter((r) => !r.forceApply && !r.key.startsWith("_") && r.readAll).map(async (r) => {
@@ -1112,8 +1177,8 @@ async function runExport(ctx, opts) {
1112
1177
  appConfig[resource.key] = value;
1113
1178
  }
1114
1179
  }
1115
- if (Object.keys(services).length > 0) {
1116
- const links = await exportAppLinks(ctx, app, services);
1180
+ if (config.postgres || config.redis) {
1181
+ const links = await exportAppLinks(ctx, app, config);
1117
1182
  if (links.length > 0) appConfig.links = links;
1118
1183
  }
1119
1184
  config.apps[app] = appConfig;
@@ -1189,8 +1254,13 @@ async function computeDiff(ctx, config) {
1189
1254
  }
1190
1255
  result.apps[app] = appDiff;
1191
1256
  }
1192
- for (const [svc, svcConfig] of Object.entries(config.services ?? {})) {
1193
- const exists = await ctx.check(`${svcConfig.plugin}:exists`, svc);
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);
1194
1264
  result.services[svc] = { status: exists ? "in-sync" : "missing" };
1195
1265
  if (!exists) result.inSync = false;
1196
1266
  }
@@ -1296,25 +1366,28 @@ function validate(filePath) {
1296
1366
  }
1297
1367
  const data = raw;
1298
1368
  if (data?.apps && typeof data.apps === "object") {
1299
- const serviceNames = new Set(
1300
- data?.services && typeof data.services === "object" ? Object.keys(data.services) : []
1301
- );
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
+ }
1302
1376
  for (const [appName, appCfg] of Object.entries(data.apps)) {
1303
1377
  if (!appCfg?.links) continue;
1304
1378
  for (const link of appCfg.links) {
1305
1379
  if (!serviceNames.has(link)) {
1306
- errors.push(`apps.${appName}.links: service "${link}" not defined in services`);
1380
+ errors.push(`apps.${appName}.links: service "${link}" not defined in postgres or redis`);
1307
1381
  }
1308
1382
  }
1309
1383
  }
1310
1384
  }
1311
- if (data?.services && data?.plugins) {
1312
- const pluginNames = new Set(Object.keys(data.plugins));
1313
- for (const [svcName, svcCfg] of Object.entries(data.services)) {
1314
- if (svcCfg?.plugin && !pluginNames.has(svcCfg.plugin)) {
1315
- warnings.push(`services.${svcName}.plugin: "${svcCfg.plugin}" not declared in plugins (may be pre-installed)`);
1316
- }
1317
- }
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)`);
1318
1391
  }
1319
1392
  return { errors, warnings };
1320
1393
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dokku-compose",
3
- "version": "0.7.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": ">=18"
16
+ "node": ">=20"
17
17
  },
18
18
  "scripts": {
19
19
  "test": "bun test",