Skip to content

fix(security): run Docker container as non-root user#14602

Open
JasonOA888 wants to merge 2 commits intotoeverything:canaryfrom
JasonOA888:fix/issue-14584-docker-non-root
Open

fix(security): run Docker container as non-root user#14602
JasonOA888 wants to merge 2 commits intotoeverything:canaryfrom
JasonOA888:fix/issue-14584-docker-non-root

Conversation

@JasonOA888
Copy link

@JasonOA888 JasonOA888 commented Mar 8, 2026

Fixes #14584

Add non-root user to Docker container for security.

  • Create affine user with configurable UID/GID
  • Set proper ownership
  • Run as non-root user

Summary by CodeRabbit

  • Chores
    • Application process in the container now runs as a non-root user.
  • Configuration
    • Default host mount paths for storage and configuration updated from /root/.affine to /home/affine/.affine.

- Create affine user with configurable UID/GID (default 1000:1000)
- Set proper ownership of /app directory
- Switch to non-root user before CMD
- Prevents privilege escalation attacks from container breach

Security: Fixes potential privilege escalation vulnerability where
container running as root could allow attacker to breach host.

Fixes toeverything#14584
@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c55948c1-f055-46a0-83cc-128445433106

📥 Commits

Reviewing files that changed from the base of the PR and between 5ddd435 and b499de6.

📒 Files selected for processing (1)
  • .docker/selfhost/compose.yml

📝 Walkthrough

Walkthrough

Adds non-root execution to the Node Docker image by introducing APP_UID/APP_GID, creating an affine user, chown'ing app data, and switching to that user. Also updates docker-compose volume mounts to use /home/affine/.affine instead of /root/.affine for two services.

Changes

Cohort / File(s) Summary
Dockerfile (non-root user)
.github/deployment/node/Dockerfile
Adds APP_UID and APP_GID build args, creates affine group/user, adjusts ownership of /app, and sets USER to run the Node process as the non-root user while preserving existing ENV/CMD.
Compose volume paths
.docker/selfhost/compose.yml
Updates volume mounts for affine and affine_migration services to bind host paths into /home/affine/.affine/... instead of /root/.affine/... (no command/env changes).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 I hopped into builds with cheer,
Swapped root for affine, safe and near,
Volumes moved to a cozy home,
Less privilege where shadows roam,
A rabbit's nod — secure we roam.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: running the Docker container as a non-root user for security purposes.
Linked Issues check ✅ Passed The pull request successfully addresses all coding requirements from issue #14584: creates a non-root affine user with configurable UID/GID, updates volume paths to non-root directories, and ensures proper file ownership.
Out of Scope Changes check ✅ Passed All changes are directly related to the security objective: Dockerfile adds non-root user setup, and compose.yml updates paths to match the non-root user's home directory.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/deployment/node/Dockerfile:
- Around line 30-35: The Dockerfile introduces a non-root user (ARG
APP_UID/APP_GID, useradd affine) but only chowns /app, causing a mismatch with
the app's homedir() (/home/affine) and mounted volumes in compose; update the
deployment by (1) changing mount targets in .docker/selfhost/compose.yml from
/root/.affine/* to /home/affine/.affine/* so volumes match the affine user's
homedir(), (2) modify the Dockerfile to create the expected runtime directories
(e.g., /home/affine/.affine/storage and /home/affine/.affine/config) and chown
them to "${APP_UID}:${APP_GID}" after useradd, and (3) add a short note in
docs/README about the breaking change for existing deployments so operators can
update their mounts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3f76578d-a5d9-446f-9639-1799ca468afd

📥 Commits

Reviewing files that changed from the base of the PR and between 9c55ede and 5ddd435.

📒 Files selected for processing (1)
  • .github/deployment/node/Dockerfile

Comment on lines +30 to +35
# Create non-root user for security
ARG APP_UID=1000
ARG APP_GID=1000
RUN groupadd -g "${APP_GID}" affine && \
useradd -m -u "${APP_UID}" -g "${APP_GID}" -s /usr/sbin/nologin affine && \
chown -R "${APP_UID}:${APP_GID}" /app
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how the application determines its data/config paths
# Check if paths are configurable via environment variables

echo "=== Checking for hardcoded /root paths in application code ==="
rg -n '/root/.affine' --type js --type ts -g '!node_modules' || echo "No hardcoded /root/.affine paths found in JS/TS"

echo ""
echo "=== Checking for AFFINE_DATA_DIR or similar env var usage ==="
rg -n 'AFFINE.*DIR|DATA.*PATH|CONFIG.*PATH|STORAGE.*PATH' --type js --type ts -g '!node_modules' | head -30

echo ""
echo "=== Checking selfhost compose.yml for volume mounts ==="
cat .docker/selfhost/compose.yml 2>/dev/null | grep -A2 'volumes:'

Repository: toeverything/AFFiNE

Length of output: 1107


🏁 Script executed:

#!/bin/bash
# Check how GitHub Actions builds the Docker image

echo "=== Searching for GitHub Actions workflows that build Docker image ==="
find .github/workflows -name '*.yml' -o -name '*.yaml' | xargs grep -l "docker.*build\|Dockerfile" 2>/dev/null | head -5

echo ""
echo "=== Checking if APP_UID/APP_GID are passed as build args in CI ==="
find .github/workflows -name '*.yml' -o -name '*.yaml' | xargs grep -A5 -B5 "APP_UID\|APP_GID" 2>/dev/null

echo ""
echo "=== Searching for all compose files that might mount /root paths ==="
find . -name "compose*.yml" -o -name "docker-compose*.yml" | xargs grep -l "/root" 2>/dev/null

echo ""
echo "=== Full check of selfhost compose.yml ==="
cat .docker/selfhost/compose.yml 2>/dev/null || echo "File not found"

Repository: toeverything/AFFiNE

Length of output: 2748


🏁 Script executed:

#!/bin/bash
# Check the GitHub Actions build configuration

echo "=== Checking build-images.yml ==="
cat .github/workflows/build-images.yml | head -100

echo ""
echo "=== Checking migration script to see if it would fail ==="
cat scripts/self-host-predeploy.js 2>/dev/null | head -50

echo ""
echo "=== Check Dockerfile USER directive around lines 42-43 ==="
sed -n '38,50p' .github/deployment/node/Dockerfile

Repository: toeverything/AFFiNE

Length of output: 3530


🏁 Script executed:

#!/bin/bash
# Verify the migration script
echo "=== Looking for self-host-predeploy.js ==="
find . -name "self-host-predeploy.js" -type f 2>/dev/null

echo ""
echo "=== Check prelude.ts to confirm path handling ==="
sed -n '1,30p' packages/backend/server/src/prelude.ts

echo ""
echo "=== Verify homedir() usage in env.ts ==="
sed -n '55,65p' packages/backend/server/src/env.ts

Repository: toeverything/AFFiNE

Length of output: 1387


Breaking change: Volume mount paths incompatible with non-root user.

The selfhost compose.yml mounts volumes to /root/.affine/storage and /root/.affine/config. The application uses homedir() which resolves to /home/affine when running as the non-root user affine. This creates a mismatch: the app will attempt to read/write from /home/affine/.affine/* but the volumes are mounted to /root/.affine/*, resulting in permission errors.

Additionally, the Dockerfile only chowns /app but doesn't create or chown the data directories that the application needs to write to.

Consider:

  1. Updating .docker/selfhost/compose.yml to mount volumes to /home/affine/.affine/storage and /home/affine/.affine/config
  2. Creating and chowning data directories in the Dockerfile for the non-root user
  3. Documenting this breaking change for existing deployments
🛠️ Suggested fix to create writable data directories
 # Create non-root user for security
 ARG APP_UID=1000
 ARG APP_GID=1000
 RUN groupadd -g "${APP_GID}" affine && \
     useradd -m -u "${APP_UID}" -g "${APP_GID}" -s /usr/sbin/nologin affine && \
-    chown -R "${APP_UID}:${APP_GID}" /app
+    chown -R "${APP_UID}:${APP_GID}" /app && \
+    mkdir -p /home/affine/.affine/storage /home/affine/.affine/config && \
+    chown -R "${APP_UID}:${APP_GID}" /home/affine/.affine
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Create non-root user for security
ARG APP_UID=1000
ARG APP_GID=1000
RUN groupadd -g "${APP_GID}" affine && \
useradd -m -u "${APP_UID}" -g "${APP_GID}" -s /usr/sbin/nologin affine && \
chown -R "${APP_UID}:${APP_GID}" /app
# Create non-root user for security
ARG APP_UID=1000
ARG APP_GID=1000
RUN groupadd -g "${APP_GID}" affine && \
useradd -m -u "${APP_UID}" -g "${APP_GID}" -s /usr/sbin/nologin affine && \
chown -R "${APP_UID}:${APP_GID}" /app && \
mkdir -p /home/affine/.affine/storage /home/affine/.affine/config && \
chown -R "${APP_UID}:${APP_GID}" /home/affine/.affine
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/deployment/node/Dockerfile around lines 30 - 35, The Dockerfile
introduces a non-root user (ARG APP_UID/APP_GID, useradd affine) but only chowns
/app, causing a mismatch with the app's homedir() (/home/affine) and mounted
volumes in compose; update the deployment by (1) changing mount targets in
.docker/selfhost/compose.yml from /root/.affine/* to /home/affine/.affine/* so
volumes match the affine user's homedir(), (2) modify the Dockerfile to create
the expected runtime directories (e.g., /home/affine/.affine/storage and
/home/affine/.affine/config) and chown them to "${APP_UID}:${APP_GID}" after
useradd, and (3) add a short note in docs/README about the breaking change for
existing deployments so operators can update their mounts.

- Changed /root/.affine/* to /home/affine/.affine/* in volume mounts
- Ensures volumes match the affine user's homedir()

Breaking change: Existing deployments need to update volume mounts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Bug]: Docker image runs as root

2 participants