From 5fe7d1a65ade6ef2dd8e71a9e664fc7db74515c0 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 18:12:00 +0100 Subject: [PATCH] fix: keep existing compose volumes and add the two that were missing --- docker-compose.yml | 49 +++++++++++++------------- lib/__tests__/docker-data-dirs.test.ts | 10 ++++-- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index cf46e05b4..50b0085d8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,31 +9,25 @@ services: env_file: - .env.local volumes: - # Everything Bulwark keeps, in one volume: - # settings/ encrypted user settings (SETTINGS_DATA_DIR) - # admin/ config.json, policy.json, admin.json, plugins, themes, - # branding uploads (ADMIN_CONFIG_DIR) - # admin-state/ login timestamps, audit.log, setup token (ADMIN_STATE_DIR) - # plugin-secrets/ users' plugin credentials (PLUGIN_SECRETS_DIR) - # telemetry/ instance id and usage stats consent (TELEMETRY_DATA_DIR) - # version-check/ result of the last update check + # These six volumes together are everything Bulwark keeps under /app/data. + # A fresh install can use one `bulwark-data:/app/data` volume instead. # - # To lock the admin config after the setup wizard, give admin/ its own - # mount with `:ro` and set ADMIN_CONFIG_READONLY=true. - # - # Upgrading from the four volumes this file used to have - # (bulwark-settings, bulwark-admin, bulwark-admin-state, - # bulwark-telemetry)? Copy them into the new one before the first start, - # or the wizard runs again. With the project name from `docker volume ls`: - # docker compose down && docker compose create - # docker run --rm -v _bulwark-settings:/old/settings \ - # -v _bulwark-admin:/old/admin \ - # -v _bulwark-admin-state:/old/admin-state \ - # -v _bulwark-telemetry:/old/telemetry \ - # -v _bulwark-data:/new alpine \ - # sh -c 'cp -a /old/. /new/ && chown -R 1001:1001 /new' - # docker compose up -d - - bulwark-data:/app/data + # Encrypted user settings (SETTINGS_DATA_DIR). + - bulwark-settings:/app/data/settings + # Admin configuration: config.json, policy.json, admin.json (passwordHash), + # plugins, themes, branding uploads (ADMIN_CONFIG_DIR). Can be mounted + # read-only after running the setup wizard - append `:ro` to lock it. + - bulwark-admin:/app/data/admin + # Admin runtime state: admin-state.json (login timestamps), audit.log, + # setup token (ADMIN_STATE_DIR). Always read-write. + - bulwark-admin-state:/app/data/admin-state + # Anonymous telemetry: instance id, consent state, login HMACs (TELEMETRY_DATA_DIR). + # Persisting this preserves the admin's consent choice and stable instance id across upgrades. + - bulwark-telemetry:/app/data/telemetry + # Users' plugin credentials, encrypted (PLUGIN_SECRETS_DIR). + - bulwark-plugin-secrets:/app/data/plugin-secrets + # Result of the last update check (VERSION_CHECK_DATA_DIR). + - bulwark-version-check:/app/data/version-check healthcheck: test: [ @@ -51,4 +45,9 @@ services: restart: unless-stopped volumes: - bulwark-data: + bulwark-settings: + bulwark-admin: + bulwark-admin-state: + bulwark-telemetry: + bulwark-plugin-secrets: + bulwark-version-check: diff --git a/lib/__tests__/docker-data-dirs.test.ts b/lib/__tests__/docker-data-dirs.test.ts index 577f29b3d..26827f754 100644 --- a/lib/__tests__/docker-data-dirs.test.ts +++ b/lib/__tests__/docker-data-dirs.test.ts @@ -45,10 +45,14 @@ describe('docker data dirs', () => { for (const dir of defaultDataDirs()) expect(created).toContain(`/app/data/${dir}`); }); - it('keeps all of /app/data in one volume in docker-compose.yml', () => { + it('persists every data dir in docker-compose.yml, by its own volume or one on /app/data', () => { const compose = readFileSync(path.join(ROOT, 'docker-compose.yml'), 'utf8'); - const mounts = [...compose.matchAll(/^\s*-\s*([\w.-]+):(\/app\/data[^\s:]*)/gm)].map((m) => m[2]); - expect(mounts).toEqual(['/app/data']); + const mounts = [...compose.matchAll(/^\s*-\s*[\w.-]+:(\/app\/data[^\s:]*)/gm)].map((m) => m[1].replace(/\/+$/, '')); + expect(mounts.length).toBeGreaterThan(0); + const unpersisted = defaultDataDirs().filter( + (dir) => !mounts.includes('/app/data') && !mounts.includes(`/app/data/${dir}`), + ); + expect(unpersisted).toEqual([]); }); });