Docker Deployment
Remote instances use built-in connection authorization: enter the instance key once, then use community login normally. External nginx/NAS auth can be layered on top. Local desktop and loopback debugging proxies remain compatible. Remote replies omit Linux paths/cloud tokens. The default platform relay needs no per-Docker-domain registration. Auth relay and Electron companion releases must ship before #3289 is ready; real Linux + Windows acceptance needs community testing. See contract and tests. Get the key: docker compose exec --user neko -w /app neko-main uv run python -m utils.instance_access When NEKO_INSTANCE_PUBLIC_ORIGIN uses HTTPS, the external gateway must close port 80 or redirect it to HTTPS; never forward plaintext requests with that Host. Keep the private HTTP upstream isolated. Host matching itself does not prove TLS. Plain HTTP (for example http://<NAS-IP>:48911, DISABLE_SSL=1, or an SSH tunnel) is allowed by default because many self-hosted setups cannot obtain a certificate. The pairing page then warns that the key and session travel unencrypted, and the session cookie is issued without the Secure flag under a separate name. Prefer HTTPS/WSS when you can; external TLS gateways may set NEKO_INSTANCE_PUBLIC_ORIGIN. Set NEKO_REQUIRE_HTTPS=1 to refuse plaintext remote pairing and credentials. Browsers only allow microphone access on HTTPS or localhost, so voice input is unavailable on http://<IP>; text chat still works. Leave NEKO_COMMUNITY_WEB_REDIRECT_URI empty for the default relay.
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 https://127.0.0.1:48912 (recommended) or http://<host>: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 |
./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.
Optionally run sudo sh preflight.sh in docker/ before the first start and after moving data in. It runs on the host without pulling any image: it refuses a symlinked neko-home or logs (Docker would mount the link target and the container would take ownership of it), creates missing directories, and sets the owner of each directory itself, not its contents, to uid/gid 1000. When an override file mounts other directories, pass those paths exactly as written there (relative ones resolve against docker/, as in Compose); any symlink along them is rejected.
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.
# 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
# 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
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, 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.
Running on a 2-vCPU / 2 GB cloud server? See Low-Spec Cloud Server for host memory, disk, security, and the optional self-healing watchdog.
