# Docker Run 9Router in a container. Published image: [`decolua/9router`](https://hub.docker.com/r/decolua/9router) — multi-platform `linux/amd64` + `linux/arm64`. --- # 👤 For Users ## Quick start ```bash docker run -d \ -p 20128:20128 \ -v "$HOME/.9router:/app/data" \ -e DATA_DIR=/app/data \ --name 9router \ decolua/9router:latest ``` App listens on port `20128`. Open: http://localhost:20128 ## Manage container ```bash docker logs -f 9router # view logs docker stop 9router # stop docker start 9router # start again docker rm -f 9router # remove ``` ## Data persistence ```bash -v "$HOME/.9router:/app/data" \ -e DATA_DIR=/app/data ``` Without `DATA_DIR`, the app falls back to `~/.9router/` (macOS/Linux) or `%APPDATA%\9router\` (Windows). In the container, `DATA_DIR=/app/data` makes the bind mount work. Data layout under `$DATA_DIR/`: ```text $DATA_DIR/ ├── db/ │ ├── data.sqlite # main SQLite database │ └── backups/ # auto backups └── ... # certs, logs, runtime configs ``` Host path: `$HOME/.9router/db/data.sqlite` Container path: `/app/data/db/data.sqlite` ## Optional env vars ```bash docker run -d \ -p 20128:20128 \ -v "$HOME/.9router:/app/data" \ -e DATA_DIR=/app/data \ -e PORT=20128 \ -e HOSTNAME=0.0.0.0 \ -e DEBUG=true \ --name 9router \ decolua/9router:latest ``` ## Optional Headroom sidecar The 9Router image does not bundle Python or Headroom. To use Headroom in Docker, run it as a separate service and point 9Router at that proxy: ```yaml services: 9router: image: decolua/9router:latest ports: - "20128:20128" volumes: - "$HOME/.9router:/app/data" environment: DATA_DIR: /app/data HEADROOM_URL: http://headroom:8787 depends_on: - headroom headroom: image: ghcr.io/chopratejas/headroom:latest ports: - "8787:8787" ``` In the dashboard, open `Endpoint` → `Token Saver` → `Headroom`, confirm the URL is `http://headroom:8787`, recheck status, then enable Headroom. If Headroom runs on the Docker host instead of as a sidecar, use `http://host.docker.internal:8787` on macOS/Windows. On Linux, add `--add-host=host.docker.internal:host-gateway` or the equivalent compose `extra_hosts` entry. ## Update to latest ```bash docker pull decolua/9router:latest docker rm -f 9router # re-run the quick start command ``` To pin a specific version instead of following `latest`, use a numbered image tag: ```bash docker pull decolua/9router:0.5.81 ``` --- # 🛠 For Developers ## Build image locally (test) ```bash docker build -t 9router . docker run --rm -p 20128:20128 \ -v "$HOME/.9router:/app/data" \ -e DATA_DIR=/app/data \ 9router ``` The Dockerfile uses the official Alpine and npm registries by default. Regional mirrors can be supplied when needed: ```bash docker build \ --build-arg ALPINE_MIRROR=mirrors.aliyun.com \ --build-arg NPM_REGISTRY=https://registry.npmmirror.com/ \ -t 9router . ``` ## Publish (automatic via CI) Push a Docker-safe semver git tag `vX.Y.Z` (or a prerelease such as `vX.Y.Z-rc.1`) → GitHub Actions builds `linux/amd64` and `linux/arm64` on native runners, health-checks each platform image, verifies the resulting manifest and `/api/health`, then publishes: - `ghcr.io/decolua/9router:X.Y.Z` + `:latest` - `decolua/9router:X.Y.Z` + `:latest` The `v` prefix is used only for the git tag; image tags omit it. A stable tag push promotes `latest`, but a prerelease tag such as `vX.Y.Z-rc.1` publishes only its numbered image by default. Prereleases require an explicit manual `promote_latest` opt-in. Promotion happens only after both native platform builds, both platform health checks, manifest inspection, and the resolved-manifest smoke test succeed. A failed or timed-out platform build therefore cannot move `latest`. The workflow rejects SemVer build metadata such as `v1.2.3+build.7` because the `+` form is not a valid Docker image tag. The git tag and both `package.json` versions must match exactly. ```bash # Use scripts/release.js (recommended) node scripts/release.js "Release title" "Notes" # Or manually git tag v0.5.81 && git push origin v0.5.81 ``` To republish an existing tag, run the `Build and Push Docker Image` workflow manually and provide the exact tag, for example `v0.5.81`, in the `release_tag` input. Manual runs publish the numbered tag but leave `latest` unchanged by default: ```text release_tag: v0.5.81 promote_latest: false ``` The `promote_latest` checkbox is an explicit opt-in for changing `latest`. Use it when a deliberate rollback or recovery should make that version the current default: ```text release_tag: v0.5.75 promote_latest: true ``` Numbered image tags are mutable because a republish can replace their manifest. For a deployment that must be immutable, pin the image digest instead: ```bash docker pull decolua/9router@sha256: ``` The release workflow runs `/api/health` on each native `amd64` and `arm64` platform image before it uploads the digest artifact or assembles the multi-platform manifest. It then runs a second health check against the resolved version manifest before any requested `latest` promotion. During recovery, the selected tag remains the application source while the Dockerfile from the workflow revision is used, so an older tag can be rebuilt with the current publishing fixes. The workflow is tag-driven. Creating a git tag does not automatically create a GitHub Release, so the Releases page and the published package/image tags can be at different versions unless a maintainer creates a release separately. The upstream repository needs these repository secrets for Docker Hub publishing: - `DOCKERHUB_USERNAME` - `DOCKERHUB_TOKEN` GHCR publishing uses the workflow's `GITHUB_TOKEN` with package write permission. Forks can publish to their own GHCR namespace, but Docker Hub publication is restricted to the upstream `decolua/9router` repository. The optional repository variables `ALPINE_MIRROR` and `NPM_REGISTRY` can override the default package mirrors used by the CI Docker build. Workflow: `.github/workflows/docker-publish.yml`