Summary
The documented Docker quick-start crashes on first start. docker/docker-compose.yml mounts a named volume at /data, but the image runs as the non-root user uid=1654 (app) while Docker initializes the named volume mount point as root:root (0755). The app can't create the SQLite database file, so the container exits during schema migration and crash-loops.
This makes the README's primary "Quick Start → Docker" path fail out of the box.
Repro
Against ghcr.io/netclaw-dev/skillserver:0.4.0 and the shipped compose file:
docker compose -f docker/docker-compose.yml up -d
docker compose -f docker/docker-compose.yml logs
Observed
info: SkillServer.Data.DatabaseInitializer[0]
Starting database schema migration...
Unhandled exception. Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 14: 'unable to open database file'.
at SkillServer.Data.SchemaMigrator.MigrateAsync(...)
at SkillServer.Data.DatabaseInitializer.InitializeAsync(...)
at Program.<Main>$(String[] args) in .../src/SkillServer/Program.cs:line 41
When SKILLSERVER__DATAPATH is left at its default, the same failure surfaces as UnauthorizedAccessException: Access to the path '/app/data' is denied — /app is also root-owned and not writable by uid 1654.
Root cause
- Image runs as
uid=1654 (app) (APP_UID=1654, .NET SDK container defaults to non-root).
- The named Docker volume's mount point is created
root:root 0755 because /data does not exist in the image, so Docker has no owned directory to inherit from.
DatabaseInitializer → Directory.CreateDirectory / SQLite open fails: the app user can't write inside a root-owned directory.
Confirmed directly:
$ docker run --rm --entrypoint sh -v <vol>:/data ghcr.io/netclaw-dev/skillserver:0.4.0 -c 'id; ls -ld /data'
uid=1654(app) gid=1654(app) groups=1654(app)
drwxr-xr-x 2 root root 4096 /data
Verified workaround (for users, until this is fixed)
Run as the host user against a bind mount the host owns:
docker run -d -p 8080:8080 \
--user "$(id -u):$(id -g)" \
-v ./data:/data \
-e SKILLSERVER__DATAPATH=/data \
-e SKILLSERVER__APIKEY=<key> \
ghcr.io/netclaw-dev/skillserver:0.4.0
Reaches healthy in ~2s and persists the SQLite DB in ./data.
Suggested fix
Make the shipped docker compose ... up work out of the box while keeping the app non-root. Candidate approaches:
- Ensure the image ships a writable
/data owned by 1654:1654 so the named volume inherits that ownership on first init (needs a Dockerfile step or an SDK-container mechanism to create + chown the dir).
- Or fix
docker/docker-compose.yml: a small init step that chowns the volume to 1654:1654 before the server starts (the image has sh + chown), or switch to a bind mount plus a documented --user.
Whichever approach lands, acceptance is: from a clean state (docker compose ... down -v), docker compose -f docker/docker-compose.yml up -d reaches healthy, runs as non-root, and persists data across restarts.
Environment
- Image:
ghcr.io/netclaw-dev/skillserver:0.4.0 (sha256:049c3f19d10deac5e74f4d7d4c3321b7f783b266f09f55df66c22b18c1769445)
- Docker 29.2.1, Linux
Summary
The documented Docker quick-start crashes on first start.
docker/docker-compose.ymlmounts a named volume at/data, but the image runs as the non-root useruid=1654 (app)while Docker initializes the named volume mount point asroot:root(0755). The app can't create the SQLite database file, so the container exits during schema migration and crash-loops.This makes the README's primary "Quick Start → Docker" path fail out of the box.
Repro
Against
ghcr.io/netclaw-dev/skillserver:0.4.0and the shipped compose file:Observed
When
SKILLSERVER__DATAPATHis left at its default, the same failure surfaces asUnauthorizedAccessException: Access to the path '/app/data' is denied—/appis also root-owned and not writable byuid 1654.Root cause
uid=1654 (app)(APP_UID=1654, .NET SDK container defaults to non-root).root:root 0755because/datadoes not exist in the image, so Docker has no owned directory to inherit from.DatabaseInitializer→Directory.CreateDirectory/ SQLite open fails: the app user can't write inside a root-owned directory.Confirmed directly:
Verified workaround (for users, until this is fixed)
Run as the host user against a bind mount the host owns:
Reaches
healthyin ~2s and persists the SQLite DB in./data.Suggested fix
Make the shipped
docker compose ... upwork out of the box while keeping the app non-root. Candidate approaches:/dataowned by1654:1654so the named volume inherits that ownership on first init (needs a Dockerfile step or an SDK-container mechanism to create + chown the dir).docker/docker-compose.yml: a small init step thatchowns the volume to1654:1654before the server starts (the image hassh+chown), or switch to a bind mount plus a documented--user.Whichever approach lands, acceptance is: from a clean state (
docker compose ... down -v),docker compose -f docker/docker-compose.yml up -dreacheshealthy, runs as non-root, and persists data across restarts.Environment
ghcr.io/netclaw-dev/skillserver:0.4.0(sha256:049c3f19d10deac5e74f4d7d4c3321b7f783b266f09f55df66c22b18c1769445)