Skip to content

Commit 47fcf93

Browse files
fix(setup): don't start managed Postgres with a password the volume will ignore
POSTGRES_PASSWORD only applies when initdb runs on an empty data directory. The sim-postgres-data volume outlives its container (sim down keeps it, docker rm keeps it, and the wizard's own recreate path keeps it), and inspectManagedContainer recovers the password from the *container*, not the volume — so once the container is gone the password is unrecoverable. Setup then generated a fresh password and ran against the initialized volume. Postgres kept its original password and rejected every connection with 'password authentication failed for user postgres', which surfaced as a misleading 'container did not become healthy'. Detect an already-bootstrapped volume (PG_VERSION present) before choosing a password, and ask: supply the existing password, or delete the volume and start fresh (double-confirmed, since that destroys data). Refusing both fails with the exact docker volume rm command instead of looping.
1 parent dcd5733 commit 47fcf93

1 file changed

Lines changed: 88 additions & 3 deletions

File tree

scripts/setup/db.ts

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,81 @@ async function promptExternalDsn(): Promise<string> {
122122
}
123123
}
124124

125+
const DB_VOLUME = 'sim-postgres-data'
126+
127+
/** True once initdb has run in the volume — PG_VERSION only exists after bootstrap. */
128+
function volumeInitialized(): boolean {
129+
if (spawnSync('docker', ['volume', 'inspect', DB_VOLUME], { stdio: 'ignore' }).status !== 0) {
130+
return false
131+
}
132+
// Read the marker from inside the volume; the image is already local, so this
133+
// costs nothing extra and beats assuming "volume exists" means "bootstrapped"
134+
// (a failed first run leaves an empty volume behind).
135+
return (
136+
spawnSync(
137+
'docker',
138+
[
139+
'run',
140+
'--rm',
141+
'-v',
142+
`${DB_VOLUME}:/pgdata`,
143+
'--entrypoint',
144+
'test',
145+
'pgvector/pgvector:pg17',
146+
'-f',
147+
'/pgdata/PG_VERSION',
148+
],
149+
{ stdio: 'ignore' }
150+
).status === 0
151+
)
152+
}
153+
154+
/**
155+
* The volume already holds a cluster whose password we cannot read back. Either
156+
* the user supplies it, or the data goes — silently generating a new password
157+
* would produce a container that never authenticates.
158+
*/
159+
async function resolveExistingVolume(): Promise<string> {
160+
p.log.warn(
161+
`The ${DB_VOLUME} volume already contains a database, but its password is not recoverable — Postgres ignores POSTGRES_PASSWORD on an existing data directory.`
162+
)
163+
const choice = await p.select({
164+
message: 'How should the wizard proceed?',
165+
options: [
166+
{
167+
value: 'password',
168+
label: 'Keep the data — I have its password',
169+
hint: 'from a previous .env, or your notes',
170+
},
171+
{
172+
value: 'wipe',
173+
label: 'Delete the old data and start fresh',
174+
hint: `removes the ${DB_VOLUME} volume — this cannot be undone`,
175+
},
176+
],
177+
initialValue: 'password',
178+
})
179+
if (choice === 'password') {
180+
return p.password({
181+
message: `Password for the existing ${DB_VOLUME} database`,
182+
validate: (value) => (value ? undefined : 'required'),
183+
})
184+
}
185+
const sure = await p.confirm({
186+
message: theme.error(`Permanently delete the ${DB_VOLUME} volume and all its data?`),
187+
initialValue: false,
188+
})
189+
if (!sure) {
190+
throw new SetupError('kept the existing database volume, so setup cannot continue.', [
191+
're-run and supply the password, or remove it yourself:',
192+
theme.command(`docker volume rm ${DB_VOLUME}`),
193+
])
194+
}
195+
docker(['volume', 'rm', DB_VOLUME])
196+
p.log.step(`Removed ${DB_VOLUME}`)
197+
return generateSecret().slice(0, 24)
198+
}
199+
125200
/**
126201
* Provisions the managed container, reconciling with one that already exists
127202
* rather than colliding on the name. Recreating is always an explicit choice —
@@ -142,14 +217,24 @@ async function startManagedContainer(detection: Detection): Promise<string> {
142217
throw new SetupError(`the existing ${DB_CONTAINER} container is not usable.`, [
143218
`inspect: ${theme.command(`docker logs ${DB_CONTAINER}`)}`,
144219
`remove it: ${theme.command(`docker rm -f ${DB_CONTAINER}`)}`,
145-
`start clean: ${theme.command('docker volume rm sim-postgres-data')} drops its data too`,
220+
`start clean: ${theme.command(`docker volume rm ${DB_VOLUME}`)} drops its data too`,
146221
])
147222
}
148223
docker(['rm', '-f', DB_CONTAINER])
149224
}
150225

151-
const password = generateSecret().slice(0, 24)
152226
const hostPort = detection.postgresPortOpen ? 5433 : 5432
227+
// POSTGRES_PASSWORD only applies when initdb runs on an empty data directory.
228+
// The volume outlives the container (sim down keeps it, so does `docker rm`),
229+
// so once the container is gone the password it was created with is
230+
// unrecoverable — inspectManagedContainer reads it from the container, not the
231+
// volume. Running with a freshly generated password against an initialized
232+
// volume starts a healthy Postgres that rejects every connection with
233+
// "password authentication failed", which surfaces as a bogus "container did
234+
// not become healthy". Ask instead of guessing.
235+
const password = volumeInitialized()
236+
? await resolveExistingVolume()
237+
: generateSecret().slice(0, 24)
153238
const dsn = `postgresql://postgres:${password}@localhost:${hostPort}/simstudio`
154239
docker([
155240
'run',
@@ -159,7 +244,7 @@ async function startManagedContainer(detection: Detection): Promise<string> {
159244
'--label',
160245
'managed-by=sim-setup',
161246
'-v',
162-
'sim-postgres-data:/var/lib/postgresql/data',
247+
`${DB_VOLUME}:/var/lib/postgresql/data`,
163248
'-e',
164249
`POSTGRES_PASSWORD=${password}`,
165250
'-e',

0 commit comments

Comments
 (0)