Skip to content

Commit f8e0427

Browse files
fix(docs): use UID-scoped PostgreSQL directories to fix non-root permission errors (#6205)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent bfa6f57 commit f8e0427

File tree

5 files changed

+109
-43
lines changed

5 files changed

+109
-43
lines changed

servers/self-hosted/scripts/generate.sh

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -173,52 +173,55 @@ log "Base path: ${NEXT_PUBLIC_BASE_PATH:-'(none)'}"
173173
# ----------- Start PostgreSQL -----------
174174
log "Starting PostgreSQL for seeding..."
175175

176-
export PGDATA=/tmp/postgresql/data
177-
export PGHOST=/tmp
178-
179-
rm -rf "$PGDATA" 2>/dev/null || true
176+
# Use UID-scoped directories to avoid permission conflicts
177+
# This prevents issues when build runs with different UIDs
178+
CURRENT_UID=$(id -u)
179+
export PGBASE="/tmp/postgresql-${CURRENT_UID}"
180+
export PGDATA="${PGBASE}/data"
181+
export PGHOST="${PGBASE}"
182+
183+
rm -rf "$PGBASE" 2>/dev/null || true
180184
mkdir -p "$PGDATA"
181185

182186
# Ensure postgres user owns the data directory (needed when running as root)
183-
if [ "$(id -u)" = "0" ]; then
184-
chown -R postgres:postgres "$PGDATA"
185-
chown postgres:postgres /tmp
187+
if [ "$CURRENT_UID" = "0" ]; then
188+
chown -R postgres:postgres "$PGBASE"
186189
fi
187190

188-
log "Initializing PostgreSQL cluster..."
191+
log "Initializing PostgreSQL cluster in $PGDATA (UID: $CURRENT_UID)..."
189192
run_as_postgres "PGDATA=$PGDATA initdb -D $PGDATA --auth-local=trust --auth-host=trust --username=postgres" 2>&1 || {
190193
log "ERROR: Failed to initialize PostgreSQL"
191194
exit 1
192195
}
193196

194-
run_as_postgres "PGDATA=$PGDATA pg_ctl -D $PGDATA -o \"-c listen_addresses='localhost' -c unix_socket_directories='/tmp' -c shared_buffers=128MB -c max_connections=200\" -l $PGDATA/logfile start" 2>&1 || {
197+
run_as_postgres "PGDATA=$PGDATA pg_ctl -D $PGDATA -o \"-c listen_addresses='localhost' -c unix_socket_directories='$PGBASE' -c shared_buffers=128MB -c max_connections=200\" -l $PGDATA/logfile start" 2>&1 || {
195198
log "ERROR: Failed to start PostgreSQL"
196199
exit 1
197200
}
198201

199-
# Wait for PostgreSQL to be ready
202+
# Wait for PostgreSQL to be ready (use UID-scoped socket directory)
200203
for i in {1..30}; do
201-
if pg_isready -h /tmp -p 5432 2>/dev/null; then
204+
if pg_isready -h "$PGBASE" -p 5432 2>/dev/null; then
202205
log "PostgreSQL is ready"
203206
break
204207
fi
205208
log "Waiting for PostgreSQL... ($i/30)"
206209
sleep 1
207210
done
208211

209-
# Create database
210-
run_as_postgres "createdb -h /tmp -p 5432 -U postgres fdr" 2>&1 | add_timestamps || log "Database 'fdr' may already exist"
212+
# Create database (use UID-scoped socket directory)
213+
run_as_postgres "createdb -h $PGBASE -p 5432 -U postgres fdr" 2>&1 | add_timestamps || log "Database 'fdr' may already exist"
211214

212215
# Restore schema from base image dump or run migrations
213216
if [ "$USE_BASE_SCHEMA" = "true" ]; then
214217
log "Restoring database schema from base image dump..."
215-
run_as_postgres "pg_restore -h /tmp -p 5432 -U postgres -d fdr --clean --if-exists $BASE_SCHEMA_DUMP" 2>&1 | add_timestamps || {
218+
run_as_postgres "pg_restore -h $PGBASE -p 5432 -U postgres -d fdr --clean --if-exists $BASE_SCHEMA_DUMP" 2>&1 | add_timestamps || {
216219
log "Warning: pg_restore had some errors (this may be normal for clean restore)"
217220
}
218221
log "Schema restored from base image dump"
219222
else
220223
log "Running Prisma migrations..."
221-
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/fdr \
224+
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/fdr?host=${PGBASE}" \
222225
prisma migrate deploy --schema /prisma/schema.prisma 2>&1 | add_timestamps || {
223226
log "ERROR: Prisma migrations failed"
224227
exit 1
@@ -247,6 +250,12 @@ done
247250
# ----------- Start MinIO -----------
248251
log "Starting MinIO for seeding..."
249252

253+
# Configure MinIO client (mc) to use a writable config directory
254+
# When running as an arbitrary UID that doesn't exist in /etc/passwd,
255+
# $HOME may default to "/" which isn't writable, causing "mc alias set" to fail.
256+
export MC_CONFIG_DIR="/tmp/mc-config"
257+
mkdir -p "$MC_CONFIG_DIR" 2>/dev/null || true
258+
250259
# Use /data for MinIO (will be copied to seed dir later)
251260
minio server /data --console-address ":9001" 2>&1 &
252261
minio_pid=$!
@@ -357,7 +366,7 @@ log "=========================================="
357366

358367
# Dump PostgreSQL database (full data, not just schema)
359368
log "Dumping PostgreSQL database..."
360-
run_as_postgres "pg_dump -h /tmp -p 5432 -U postgres -Fc fdr" > "$SEED_POSTGRES_DUMP" || {
369+
run_as_postgres "pg_dump -h $PGBASE -p 5432 -U postgres -Fc fdr" > "$SEED_POSTGRES_DUMP" || {
361370
log "ERROR: Failed to dump PostgreSQL database"
362371
exit 1
363372
}

servers/self-hosted/scripts/readiness.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@ check_http_endpoint() {
2323
}
2424

2525
check_postgres() {
26-
if pg_isready -h /tmp -p 5432 > /dev/null 2>&1; then
26+
# Read the PostgreSQL socket directory from the file written by run.sh
27+
# This supports UID-scoped directories used for non-root compatibility
28+
local pg_socket_dir="/tmp"
29+
if [ -f /tmp/postgres-socket-dir ]; then
30+
pg_socket_dir=$(cat /tmp/postgres-socket-dir)
31+
fi
32+
33+
if pg_isready -h "$pg_socket_dir" -p 5432 > /dev/null 2>&1; then
2734
echo "✓ PostgreSQL is ready"
2835
return 0
2936
else

servers/self-hosted/scripts/run.sh

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -68,31 +68,42 @@ log "Starting PostgreSQL service..."
6868
start_postgresql() {
6969
local CURRENT_UID=$(id -u)
7070

71-
# Always use /tmp for PostgreSQL to work with any UID
72-
export PGDATA=/tmp/postgresql/data
73-
export PGHOST=/tmp
74-
75-
# Clean up any previous attempts
71+
# Use UID-scoped directories to avoid permission conflicts when container
72+
# is restarted with a different UID (e.g., root -> non-root or vice versa)
73+
# This prevents "Permission denied" errors from stale directories owned by other UIDs
74+
export PGBASE="/tmp/postgresql-${CURRENT_UID}"
75+
export PGDATA="${PGBASE}/data"
76+
export PGHOST="${PGBASE}"
77+
78+
# Clean up any previous attempts for THIS UID
7679
rm -rf "$PGDATA" 2>/dev/null || true
80+
rm -rf "$PGBASE" 2>/dev/null || true
7781
mkdir -p "$PGDATA"
7882

7983
log "Initializing PostgreSQL cluster in $PGDATA (UID: $CURRENT_UID)..."
8084

85+
# Verify we have proper access to the directory
86+
if [ ! -w "$PGDATA" ] || [ ! -x "$PGDATA" ]; then
87+
log "ERROR: Cannot write to or execute in $PGDATA"
88+
ls -ld "$PGBASE" "$PGDATA" 2>&1 | add_timestamps || true
89+
return 1
90+
fi
91+
8192
# Check if running as root - root cannot run initdb directly
8293
if [ "$CURRENT_UID" -eq 0 ]; then
8394
log "Running as root, using 'su - postgres' to initialize and run PostgreSQL..."
8495

8596
# Change ownership of the data directory to postgres user
86-
chown -R postgres:postgres "$PGDATA"
97+
chown -R postgres:postgres "$PGBASE"
8798

8899
# Initialize as postgres user
89100
if ! su - postgres -c "initdb -D $PGDATA --auth-local=trust --auth-host=trust --username=postgres" 2>&1 | add_timestamps; then
90101
log "ERROR: Failed to initialize PostgreSQL as postgres user"
91102
return 1
92103
fi
93104

94-
# Start PostgreSQL as postgres user
95-
if ! su - postgres -c "pg_ctl -D $PGDATA -o \"-c listen_addresses='localhost' -c unix_socket_directories='/tmp' -c shared_buffers=128MB -c max_connections=200 -c logging_collector=on -c log_line_prefix='%t '\" -l $PGDATA/logfile start" 2>&1 | add_timestamps; then
105+
# Start PostgreSQL as postgres user with UID-scoped socket directory
106+
if ! su - postgres -c "pg_ctl -D $PGDATA -o \"-c listen_addresses='localhost' -c unix_socket_directories='$PGBASE' -c shared_buffers=128MB -c max_connections=200 -c logging_collector=on -c log_line_prefix='%t '\" -l $PGDATA/logfile start" 2>&1 | add_timestamps; then
96107
log "ERROR: Failed to start PostgreSQL"
97108
cat "$PGDATA/logfile" 2>/dev/null | add_timestamps
98109
return 1
@@ -106,9 +117,9 @@ start_postgresql() {
106117
return 1
107118
fi
108119

109-
# Start PostgreSQL
120+
# Start PostgreSQL with UID-scoped socket directory
110121
if ! pg_ctl -D "$PGDATA" \
111-
-o "-c listen_addresses='localhost' -c unix_socket_directories='/tmp' -c shared_buffers=128MB -c max_connections=200 -c logging_collector=on -c log_line_prefix='%t '" \
122+
-o "-c listen_addresses='localhost' -c unix_socket_directories='$PGBASE' -c shared_buffers=128MB -c max_connections=200 -c logging_collector=on -c log_line_prefix='%t '" \
112123
-l "$PGDATA/logfile" \
113124
start 2>&1 | add_timestamps; then
114125
log "ERROR: Failed to start PostgreSQL"
@@ -119,27 +130,30 @@ start_postgresql() {
119130

120131
log "PostgreSQL started successfully"
121132

122-
# Wait for PostgreSQL to be ready
133+
# Wait for PostgreSQL to be ready (use UID-scoped socket directory)
123134
for i in {1..30}; do
124-
if pg_isready -h /tmp -p 5432 2>/dev/null; then
135+
if pg_isready -h "$PGBASE" -p 5432 2>/dev/null; then
125136
log "PostgreSQL is ready"
126137
break
127138
fi
128139
log "Waiting for PostgreSQL to start... ($i/30)"
129140
sleep 1
130141
done
131142

132-
# Create the database
143+
# Create the database (use UID-scoped socket directory)
133144
log "Creating database 'fdr'..."
134145
if [ "$CURRENT_UID" -eq 0 ]; then
135-
su - postgres -c "createdb -h /tmp -U postgres fdr" 2>&1 | add_timestamps || true
146+
su - postgres -c "createdb -h $PGBASE -U postgres fdr" 2>&1 | add_timestamps || true
136147
else
137-
createdb -h /tmp -U postgres fdr 2>&1 | add_timestamps || true
148+
createdb -h "$PGBASE" -U postgres fdr 2>&1 | add_timestamps || true
138149
fi
139150

140-
# Update DATABASE_URL for Prisma to use the Unix socket
141-
export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/fdr?host=/tmp"
142-
log "DATABASE_URL configured for Unix socket in /tmp"
151+
# Update DATABASE_URL for Prisma to use the UID-scoped Unix socket
152+
export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/fdr?host=${PGBASE}"
153+
log "DATABASE_URL configured for Unix socket in $PGBASE"
154+
155+
# Write PGBASE to a known file so health check scripts can find the socket directory
156+
echo "$PGBASE" > /tmp/postgres-socket-dir
143157

144158
return 0
145159
}
@@ -187,11 +201,11 @@ if [ "$USE_SEEDED_DATA" = "true" ] && [ -f "$SEED_POSTGRES_DUMP" ]; then
187201
log "Restoring PostgreSQL from seeded dump..."
188202
CURRENT_UID=$(id -u)
189203
if [ "$CURRENT_UID" -eq 0 ]; then
190-
su - postgres -c "pg_restore -h /tmp -p 5432 -U postgres -d fdr --clean --if-exists '$SEED_POSTGRES_DUMP'" 2>&1 | add_timestamps || {
204+
su - postgres -c "pg_restore -h $PGBASE -p 5432 -U postgres -d fdr --clean --if-exists '$SEED_POSTGRES_DUMP'" 2>&1 | add_timestamps || {
191205
log "Warning: pg_restore had some errors (this may be normal for clean restore)"
192206
}
193207
else
194-
pg_restore -h /tmp -p 5432 -U postgres -d fdr --clean --if-exists "$SEED_POSTGRES_DUMP" 2>&1 | add_timestamps || {
208+
pg_restore -h "$PGBASE" -p 5432 -U postgres -d fdr --clean --if-exists "$SEED_POSTGRES_DUMP" 2>&1 | add_timestamps || {
195209
log "Warning: pg_restore had some errors (this may be normal for clean restore)"
196210
}
197211
fi
@@ -281,6 +295,13 @@ fi
281295

282296
# ----------- Start MINIO setup -----------
283297

298+
# Configure MinIO client (mc) to use a writable config directory
299+
# When running as an arbitrary UID (e.g., 65532) that doesn't exist in /etc/passwd,
300+
# $HOME may default to "/" which isn't writable, causing "mc alias set" to fail.
301+
# Setting MC_CONFIG_DIR ensures mc can write its config regardless of the UID.
302+
export MC_CONFIG_DIR="/tmp/mc-config"
303+
mkdir -p "$MC_CONFIG_DIR" 2>/dev/null || true
304+
284305
# Clean up any existing MinIO system files to avoid overlay filesystem issues
285306
# MinIO's .minio.sys can cause "rename across devices" errors if it exists from a previous layer
286307
log "Cleaning up MinIO system files..."

servers/self-hosted/src/__test__/nonRootUser.test.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,18 @@ describe("Self-hosted container with security restrictions", () => {
8686
);
8787
console.log(relevantLogs.join("\n"));
8888

89-
// Check for successful PostgreSQL initialization in /tmp
90-
expect(logs).toContain("Initializing PostgreSQL cluster in /tmp/postgresql/data");
89+
// Check for successful PostgreSQL initialization in UID-scoped /tmp directory
90+
// The path is now /tmp/postgresql-{UID}/data to avoid permission conflicts
91+
expect(logs).toMatch(/Initializing PostgreSQL cluster in \/tmp\/postgresql-\d+\/data/);
9192
expect(logs).toContain("PostgreSQL started successfully");
9293

9394
// Check if PostgreSQL is actually running
95+
// Extract the PGBASE path from logs to use for pg_isready
96+
const pgbaseMatch = logs.match(/DATABASE_URL configured for Unix socket in (\/tmp\/postgresql-\d+)/);
97+
const pgbase = pgbaseMatch ? pgbaseMatch[1] : "/tmp/postgresql-65532";
9498
const { stdout: psOutput } = await execa(
9599
"docker",
96-
["exec", containerId, "pg_isready", "-h", "/tmp", "-p", "5432"],
100+
["exec", containerId, "pg_isready", "-h", pgbase, "-p", "5432"],
97101
{
98102
reject: false
99103
}
@@ -191,8 +195,9 @@ describe("Self-hosted container with security restrictions", () => {
191195
// Check for successful startup indicators
192196
console.log("Checking for successful startup indicators...");
193197

194-
// PostgreSQL should initialize in /tmp
195-
expect(logs).toContain("Initializing PostgreSQL cluster in /tmp/postgresql/data");
198+
// PostgreSQL should initialize in UID-scoped /tmp directory
199+
// The path is now /tmp/postgresql-{UID}/data to avoid permission conflicts
200+
expect(logs).toMatch(/Initializing PostgreSQL cluster in \/tmp\/postgresql-\d+\/data/);
196201
expect(logs).toContain("PostgreSQL started successfully");
197202

198203
// Check if key services started

servers/self-hosted/src/__test__/singleNode.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,16 @@ describe("Self-hosted docs has a running Postgres instance", () => {
3333
const containerId = await getSingleNodeContainerId();
3434
expect(containerId).toBeTruthy();
3535

36+
// Use TCP connection (-h localhost) instead of Unix socket for robustness
37+
// The socket directory is UID-scoped and may vary between runs
3638
const { stdout: postgresStatus } = await execa("docker", [
3739
"exec",
3840
containerId,
3941
"pg_isready",
42+
"-h",
43+
"localhost",
44+
"-p",
45+
"5432",
4046
"-U",
4147
"postgres",
4248
"-d",
@@ -49,12 +55,17 @@ describe("Self-hosted docs has a running Postgres instance", () => {
4955
const containerId = await getSingleNodeContainerId();
5056
expect(containerId).toBeTruthy();
5157

58+
// Use TCP connection (-h localhost) instead of Unix socket for robustness
5259
const { stdout: dbList } = await execa("docker", [
5360
"exec",
5461
"-e",
5562
"PGPASSWORD=postgres",
5663
containerId,
5764
"psql",
65+
"-h",
66+
"localhost",
67+
"-p",
68+
"5432",
5869
"-U",
5970
"postgres",
6071
"-d",
@@ -71,6 +82,10 @@ describe("Self-hosted docs has a running Postgres instance", () => {
7182
"PGPASSWORD=postgres",
7283
containerId,
7384
"psql",
85+
"-h",
86+
"localhost",
87+
"-p",
88+
"5432",
7489
"-U",
7590
"postgres",
7691
"-d",
@@ -86,7 +101,16 @@ describe("Self-hosted docs has a running Postgres instance", () => {
86101
it("Minio Bucket has docs", async () => {
87102
const containerId = await getSingleNodeContainerId();
88103
expect(containerId).toBeTruthy();
89-
const { stdout: minioStatus } = await execa("docker", ["exec", containerId, "mc", "ls", "minio"]);
104+
// Pass MC_CONFIG_DIR to docker exec since run.sh configures mc with this custom config directory
105+
const { stdout: minioStatus } = await execa("docker", [
106+
"exec",
107+
"-e",
108+
"MC_CONFIG_DIR=/tmp/mc-config",
109+
containerId,
110+
"mc",
111+
"ls",
112+
"minio"
113+
]);
90114
const orgName = "example-org"; // this comes from the fern folder we mount
91115
expect(minioStatus).toContain(`${orgName}.docs.buildwithfern.com`);
92116
});

0 commit comments

Comments
 (0)