Skip to content

fix(docker): make quick-start compose run as non-root against a writable /data#141

Merged
Aaronontheweb merged 2 commits into
devfrom
fix/docker-quickstart-data-perms
Jul 22, 2026
Merged

fix(docker): make quick-start compose run as non-root against a writable /data#141
Aaronontheweb merged 2 commits into
devfrom
fix/docker-quickstart-data-perms

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Contributor

Summary

Fixes the Docker quick-start, which crash-looped on first start (issue #140).

docker/docker-compose.yml mounts a named volume at /data, but the published image runs as non-root uid=1654 (app) (.NET SDK container publish; there is no Dockerfile). Docker creates the named volume's /data mount point as root:root 0755 — there is no /data in the image for it to inherit ownership from — so the app cannot create the SQLite database. DatabaseInitializer / SchemaMigrator fails with SQLite Error 14: 'unable to open database file' and the container exits.

Root cause

  • Image runs as uid=1654 (app) (Entrypoint=["dotnet","/app/SkillServer.dll"], User=1654).
  • The named volume's mount point is created root:root because /data does not exist in the image.
  • Directory.CreateDirectory / SQLite open in DatabaseInitializer can't write inside a root-owned directory → the documented docker compose ... up path fails out of the box.

Fix (compose-only — no image rebuild required)

  • init-data one-shot service: runs as root (user: "0:0"), chown -R 1654:1654 /data, then exits. It uses the same image (Ubuntu 24.04 base, ships sh + chown).
  • skill-server gates on it via depends_on: { init-data: { condition: service_completed_successfully } }. The server itself still runs as non-root uid 1654, and data persists in the named volume.
  • Image reference changed from the local-only tag skillserver:latest to ghcr.io/netclaw-dev/skillserver:0.4.0, so a fresh clone can actually docker compose up and pull the image. (Pinned for a reproducible quick-start; happy to float latest if preferred.)
  • Explicit project name: skillserver so the stack no longer inherits the ambiguous docker project name from its parent directory.

The README quick-start command is unchanged: docker compose -f docker/docker-compose.yml up -d.

Verification

From a clean state (docker compose -f docker/docker-compose.yml down -v), against ghcr.io/netclaw-dev/skillserver:0.4.0:

up -d → init runs and exits 0, server starts:

$ docker compose -f docker/docker-compose.yml ps -a
NAME                         IMAGE                                   COMMAND                  SERVICE        STATUS
skillserver-init-data-1      ghcr.io/netclaw-dev/skillserver:0.4.0   "sh -c 'chown -R 165…"   init-data      Exited (0)
skillserver-skill-server-1   ghcr.io/netclaw-dev/skillserver:0.4.0   "dotnet /app/SkillSe…"   skill-server   Up   0.0.0.0:8080->8080/tcp
$ docker inspect skillserver-init-data-1 --format '{{.State.ExitCode}}'
0

Health check:

$ curl -fsS http://localhost:8080/health
{"status":"healthy","timestamp":"2026-07-22T01:12:44.8782715+00:00"}

Still non-root (uid 1654), /data now writable, DB created:

$ docker compose -f docker/docker-compose.yml exec -T skill-server id
uid=1654(app) gid=1654(app) groups=1654(app)

$ docker compose -f docker/docker-compose.yml exec -T skill-server sh -c 'ls -ld /data; ls -la /data'
drwxr-xr-x 3 app app 4096 /data
drwxr-xr-x 3 app  app    4096 blobs
-rw-r--r-- 1 app  app  126976 skills.db

Logs — migrations apply cleanly, no SQLite/permission exception:

Applying migration: 006_skill_file_unix_mode.sql
Migration 006_skill_file_unix_mode.sql applied successfully
Database schema migration completed successfully
Now listening on: http://[::]:8080
Application started. Press Ctrl+C to shut down.

Persistence across restartdown (WITHOUT -v) then up -d; the SQLite DB is byte-identical (same inode + sha256) and migrations report "already applied":

# before restart
inode=11861755 size=126976
fb7588e6e2fe05acc70ccd371e8194be7a7bf8d8b6d9f8cec041d304c032d522  /data/skills.db

# after `down` (no -v) + `up -d`
$ curl -fsS http://localhost:8080/health
{"status":"healthy","timestamp":"2026-07-22T01:13:20.9299928+00:00"}
inode=11861755 size=126976
fb7588e6e2fe05acc70ccd371e8194be7a7bf8d8b6d9f8cec041d304c032d522  /data/skills.db

Migration 001_initial_schema.sql already applied
...
Migration 006_skill_file_unix_mode.sql already applied
Database schema migration completed successfully

Closes #140

…ble /data

The documented `docker compose -f docker/docker-compose.yml up -d` quick-start
crash-looped on first start. The image runs as non-root uid 1654 (app), but
Docker creates the named volume's `/data` mount point as root:root 0755
(there is no `/data` in the image to inherit ownership from). The app cannot
create the SQLite database there, so DatabaseInitializer/SchemaMigrator fails
with `SQLite Error 14: 'unable to open database file'` and the container exits.

Fix, compose-only (no image rebuild required):

- Add a one-shot `init-data` service that runs as root (`user: "0:0"`) and
  chowns `/data` to 1654:1654 before the server starts. The server now gates
  on it via `depends_on: condition: service_completed_successfully`. The app
  itself still runs as non-root uid 1654, and data persists in the named volume.
- Point the image at `ghcr.io/netclaw-dev/skillserver:0.4.0` instead of the
  local-only `skillserver:latest` tag, so a fresh clone can actually pull and
  `docker compose up` out of the box.
- Set an explicit project `name: skillserver` so the stack no longer inherits
  the ambiguous `docker` project name from its parent directory.

Verified from a clean state: reaches healthy, runs as uid 1654 (non-root),
and the SQLite DB survives a `down` (without -v) + `up` restart.
@Aaronontheweb
Aaronontheweb enabled auto-merge (squash) July 22, 2026 02:13
@Aaronontheweb
Aaronontheweb merged commit d5eef8f into dev Jul 22, 2026
7 checks passed
@Aaronontheweb
Aaronontheweb deleted the fix/docker-quickstart-data-perms branch July 22, 2026 02:17
@Aaronontheweb Aaronontheweb mentioned this pull request Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Docker quick-start crash-loops: named volume /data is root-owned but container runs as uid 1654

1 participant