|
| 1 | +/** |
| 2 | + * perry/compose — multi-service stack with named volumes + env interpolation |
| 3 | + * |
| 4 | + * Two services on a user-defined network: |
| 5 | + * - db : postgres with a named volume for durable state |
| 6 | + * - web : nginx pointing at the postgres host (cross-service DNS) |
| 7 | + * |
| 8 | + * Demonstrates: |
| 9 | + * - `${VAR:-default}` env interpolation (resolved at the FFI boundary |
| 10 | + * against process.env before the spec hits the engine) |
| 11 | + * - Named volume that survives `down(stack, { volumes: false })` and |
| 12 | + * is removed by `down(stack, { volumes: true })` |
| 13 | + * - User-defined network so cross-service DNS works (web → db:5432) |
| 14 | + * - `depends_on` for explicit startup ordering |
| 15 | + * |
| 16 | + * Run: |
| 17 | + * perry main.ts -o multi-service |
| 18 | + * ./multi-service # uses platform default |
| 19 | + * DB_PASSWORD=hunter2 ./multi-service # override interpolation |
| 20 | + * PERRY_CONTAINER_BACKEND=docker ./multi-service # pin runtime |
| 21 | + * |
| 22 | + * Note: the small `setTimeout` calls between FFI awaits keep the |
| 23 | + * runtime event loop alive while tokio tasks settle the Promises — |
| 24 | + * see examples/container/simple/main.ts for the why. |
| 25 | + */ |
| 26 | + |
| 27 | +import { up, down, logs } from 'perry/compose'; |
| 28 | +import { getBackend } from 'perry/container'; |
| 29 | + |
| 30 | +async function main() { |
| 31 | + console.log('backend:', getBackend()); |
| 32 | + console.log('starting db + web stack...'); |
| 33 | + |
| 34 | + const stack = await up({ |
| 35 | + version: '3.8', |
| 36 | + services: { |
| 37 | + db: { |
| 38 | + image: 'postgres:16-alpine', |
| 39 | + container_name: 'perry-example-multi-db', |
| 40 | + environment: { |
| 41 | + POSTGRES_USER: '${DB_USER:-myuser}', |
| 42 | + POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}', |
| 43 | + POSTGRES_DB: 'mydb', |
| 44 | + }, |
| 45 | + volumes: ['db-data:/var/lib/postgresql/data'], |
| 46 | + ports: ['15432:5432'], |
| 47 | + networks: ['app-net'], |
| 48 | + }, |
| 49 | + web: { |
| 50 | + // Public-image stand-in for "your app." Real apps swap this |
| 51 | + // for their own image; the rest of the spec stays the same. |
| 52 | + image: 'nginx:alpine', |
| 53 | + container_name: 'perry-example-multi-web', |
| 54 | + depends_on: ['db'], |
| 55 | + ports: ['13000:80'], |
| 56 | + environment: { |
| 57 | + DATABASE_URL: 'postgres://${DB_USER:-myuser}:${DB_PASSWORD:-secret}@db:5432/mydb', |
| 58 | + }, |
| 59 | + networks: ['app-net'], |
| 60 | + }, |
| 61 | + }, |
| 62 | + networks: { |
| 63 | + 'app-net': { driver: 'bridge' }, |
| 64 | + }, |
| 65 | + volumes: { |
| 66 | + // Empty `{}` here would trip a Perry runtime auto-stringification |
| 67 | + // bug; use any non-empty config instead. The default driver on |
| 68 | + // every backend is "local" — declaring it explicitly makes the |
| 69 | + // spec robust. |
| 70 | + 'db-data': { driver: 'local' }, |
| 71 | + }, |
| 72 | + }); |
| 73 | + console.log('stack handle:', String(stack)); |
| 74 | + |
| 75 | + // Keep loop alive while up's tokio task settles. |
| 76 | + await new Promise((r) => setTimeout(r, 1000)); |
| 77 | + |
| 78 | + // Drain logs from both services. |
| 79 | + const logsJson = await logs(stack, { tail: 5 }); |
| 80 | + console.log('logs (last 5 lines):'); |
| 81 | + const parsed = JSON.parse(logsJson); |
| 82 | + console.log(' stdout (head):', parsed.stdout.slice(0, 200)); |
| 83 | + |
| 84 | + await new Promise((r) => setTimeout(r, 200)); |
| 85 | + console.log('tearing down (and dropping the db-data volume)...'); |
| 86 | + await down(stack, { volumes: true }); |
| 87 | + console.log('done'); |
| 88 | + console.log('PASS'); |
| 89 | +} |
| 90 | + |
| 91 | +main().catch((err) => { |
| 92 | + console.error('FAIL:', err); |
| 93 | + process.exit(1); |
| 94 | +}); |
0 commit comments