Docker Deployment
The maintained Compose file is docker/docker-compose.yml. It runs N.E.K.O. behind Nginx and publishes HTTP on host port 48911 and HTTPS on 48912.
Start a published image
git clone https://github.com/Project-N-E-K-O/N.E.K.O.git
cd N.E.K.O/docker
cp env.template .env
# Review .env and keep only values supported by current code.
docker compose up -dOpen http://127.0.0.1:48911. The checked-out Compose file defines the registry/proxy default. Pin NEKO_IMAGE or NEKO_IMAGE_VERSION for reproducibility. latest is the standard-image alias; latest-full is the full-image alias.
Initial configuration
The entrypoint generates /home/neko/.local/share/N.E.K.O/config/core_config.json only when absent. API environment variables are initialization inputs, not a live universal override. Setting NEKO_FORCE_ENV_UPDATE explicitly regenerates and replaces that persisted bootstrap configuration; back it up first. Confirm effective values in the Web UI.
Persistent mounts
| Host path | Container path | Purpose |
|---|---|---|
./neko-home | /home/neko | User configuration, characters, memories, user plugins and their state, feature data, TLS certificate and private key, OpenFang runtime state |
./logs | /app/logs | Logs |
TZ defaults to Asia/Shanghai; override it in .env with any IANA timezone such as Etc/UTC. Back up neko-home and logs before upgrades. Never expose data or private-key directories through a web server. Do not point PLUGIN_CONFIG_ROOT, PLUGIN_PACKAGES_ROOT, or PACKAGE_PROFILES_ROOT outside neko-home, or the corresponding user-plugin data will not survive container recreation.
Upgrading from the two-mount layout
Earlier versions mounted ./N.E.K.O and ./ssl separately. Pulling a new image without migrating leaves the container with an empty data directory: it starts normally and API keys are regenerated from the environment, so nothing looks wrong, but characters, memories and plugins are all missing. The old data is not deleted — it is simply no longer mounted.
Order matters: docker compose down removes the container, and some state exists nowhere else.
# 1. Export what lives only inside the container — before it is removed.
# OpenFang's workspace was never mounted under the old layout. And if the host
# N.E.K.O/ directory is empty, you followed the old README quickstart, whose
# mount target (/root/Documents/N.E.K.O) never matched where the services
# actually write — the application data is in there too.
# The trailing /. copies directory *contents*, avoiding a nested N.E.K.O/N.E.K.O.
mkdir -p neko-home/.local/share/N.E.K.O neko-home/ssl neko-home/.openfang
# Decide from what the container actually mounts, not from what the host directory
# contains: the old README mounted ./N.E.K.O at /root/Documents/N.E.K.O, a path the
# services never write to, so that host directory can hold files you put there while
# the real data still lives only in the container's writable layer.
if ! MOUNTS=$(docker inspect neko --format '{{range .Mounts}}{{println .Destination}}{{end}}' 2>/dev/null); then
echo "Cannot inspect container neko; stop the migration before removing it." >&2
exit 1
fi
# Needed in step 2: data recovered from the container must not be overwritten by the host copy.
EXPORTED_APP_DATA=""
if printf '%s\n' "$MOUNTS" | grep -qx /home/neko; then
echo "Container already uses the new layout — nothing to export."
else
# The old entrypoint generated its bootstrap config in /app/config. It may be
# the only copy of API settings even when the application data itself was
# mounted. Copy it over the newly generated defaults before removing neko.
if ! LEGACY_CORE_CONFIG_STATE=$(docker exec neko sh -c 'if [ -f /app/config/core_config.json ]; then printf present; elif [ -e /app/config/core_config.json ]; then printf invalid; else printf missing; fi'); then
echo "Cannot inspect legacy bootstrap configuration; stop the migration." >&2
exit 1
fi
case "$LEGACY_CORE_CONFIG_STATE" in
present)
if ! docker cp neko:/app/config/core_config.json ./neko-home/.local/share/N.E.K.O/config/core_config.json; then
echo "Legacy bootstrap-config export failed; stop the migration before removing the container." >&2
exit 1
fi
;;
missing) echo "(no legacy /app/config/core_config.json to export)" ;;
*) echo "Legacy bootstrap configuration is not a regular file; stop the migration." >&2; exit 1 ;;
esac
# Application data first; this is the part that cannot be recovered later. If the
# container does not mount the data directory, that data exists nowhere else.
if ! printf '%s\n' "$MOUNTS" | grep -qx /home/neko/.local/share/N.E.K.O; then
if ! docker cp neko:/home/neko/.local/share/N.E.K.O/. ./neko-home/.local/share/N.E.K.O/; then
echo "Application-data export failed; stop the migration before removing the container." >&2
exit 1
fi
EXPORTED_APP_DATA=1
fi
# OpenFang is optional only when its source directory is genuinely absent.
# Other inspection or copy failures can lose state and must stop the migration.
if ! OPENFANG_STATE=$(docker exec neko sh -c 'if [ -d /home/neko/.openfang ]; then printf present; elif [ -e /home/neko/.openfang ]; then printf invalid; else printf missing; fi'); then
echo "Cannot inspect OpenFang state; stop the migration." >&2
exit 1
fi
case "$OPENFANG_STATE" in
present)
if ! docker cp neko:/home/neko/.openfang/. ./neko-home/.openfang/; then
echo "OpenFang-state export failed; stop the migration before removing the container." >&2
exit 1
fi
;;
missing) echo "(no .openfang in the container)" ;;
*) echo "OpenFang state is not a directory; stop the migration." >&2; exit 1 ;;
esac
fiOnly continue once step 1 succeeded. docker compose down removes the container, which for an empty host N.E.K.O/ is the only copy of that data — if the export failed on permissions, a full disk or an unreachable daemon, stop here and fix that first.
# 2. Stop the container, then merge the host-side directories by content. If the
# new layout has been started once, the destinations already exist (plus a
# freshly generated self-signed certificate) and `mv` would nest them one level
# deeper. Same-named files resolve in favour of the old data.
docker compose down
# The migration merge itself runs as your host user. The new container later
# restores its runtime state to uid/gid 1000, so hosts whose user is not 1000
# may need sudo (or matching group permissions) for later backups and edits.
[ "$(id -u)" = 1000 ] || sudo chown -R "$(id -u):$(id -g)" neko-home
# Only merge the host copy when step 1 did NOT recover the data from the container:
# under the old README layout that directory was mounted at a path the services never
# wrote to, so anything in it would overwrite the only correct copy.
if [ -n "$EXPORTED_APP_DATA" ]; then
echo "Application data came from the container in step 1 - not merging the host N.E.K.O/"
elif [ -d N.E.K.O ]; then
cp -a N.E.K.O/. neko-home/.local/share/N.E.K.O/ && rm -rf N.E.K.O
fi
[ -d ssl ] && cp -a ssl/. neko-home/ssl/ && rm -rf ssl
# 3. Start again
docker compose up -d./logs is unaffected. The application user inside the container is pinned to uid/gid 1000. Users whose host uid is 1000 can normally manage neko-home/ directly; other hosts may need sudo or an appropriate group/ACL after the container starts.
Build locally
The Compose service declares image:, not build:. Build from the repository root explicitly:
uv run python scripts/prepare_speaker_model.py
docker build -f docker/Dockerfile -t neko-local:standard .
docker build -f docker/Dockerfile.full -t neko-local:full .The preparation step downloads and verifies the pinned CAM++ weight on the native host. Both Dockerfiles intentionally re-verify it with --offline; this keeps emulated multi-architecture builds and image layers free of model network fallbacks. Re-run the preparation step after the speaker-model manifest changes.
Set NEKO_IMAGE=neko-local:standard or neko-local:full before docker compose up. docker compose build does nothing useful here unless a reviewed build: definition is added.
Proxy and diagnostics
The entrypoint starts the Python services and container-local OpenFang, then configures Nginx and WebSocket routes. Its generated certificate is self-signed, not public-trust TLS. Supply a managed certificate or terminate TLS at a trusted proxy for real remote deployment.
docker compose ps
docker logs neko
docker exec -it neko bash
curl -f http://127.0.0.1:48911/healthSee Environment Variables for variables verified in current code.
