The ultra-lightweight self-hosted deployment hub for modern frontends.
339
The ultra-lightweight self-hosted deployment hub for modern frontends.
A single Go binary that ships with an embedded React admin UI, pulls pre-built container images, manages a Caddy reverse proxy for HTTPS, and gives you a Vercel-style "git push → deployed" experience on your own server. Think of it as a nano Coolify / Dokploy.
docker login on demand with logout
after pull, no lingering credentials/healthz for orchestrators and load balancersCoolify and Dokploy are great, but sometimes you just want something truly lightweight. Nanoku is the nano version: fewer features, less overhead, but the same core joy of "push → deployed".
Builds live in your CI where they belong. nanoku only does the parts that have to live on the host: pull the image, swap the container, reload the proxy, and stream the logs back.
The installer pulls the latest image, writes /etc/nanoku/, generates
a fresh NANOKU_SECRET_KEY + admin password, drops a systemd unit,
starts nanoku, and prints the access URL.
curl -fsSL https://raw.githubusercontent.com/isaced/nanoku/main/scripts/install.sh | sudo bash
Useful overrides:
| Variable | Default | Notes |
|---|---|---|
NANOKU_VERSION | latest | Pin a specific tag, e.g. v0.1.0 |
NANOKU_ADMIN_USER | admin | Seed admin username |
NANOKU_ADMIN_PASSWORD | (generated) | Set your own to skip the one-time print at the end |
NANOKU_SECRET_KEY | (generated) | AES-256-GCM passphrase; back up after install |
NANOKU_LISTEN | :8080 | nanoku UI listen address |
NANOKU_IMAGE_REGISTRY | dockerhub | Set to ghcr to use ghcr.io/isaced/nanoku |
After install:
# Upgrade
sudo /etc/nanoku/upgrade.sh
# Tail logs
journalctl -u nanoku -f
# Uninstall (keeps data)
sudo bash scripts/uninstall.sh
# or wipe everything
sudo bash scripts/uninstall.sh --purge
If you'd rather skip Docker for nanoku itself, grab a single binary.
Note: this still requires Docker on the host — nanoku manages
containers, so the docker socket must be reachable via DOCKER_HOST
(defaults to /var/run/docker.sock).
# Grab the latest release
curl -L -o nanoku https://github.com/isaced/nanoku/releases/latest/download/nanoku_$(uname -s)_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz
tar -xzf nanoku*.tar.gz
chmod +x nanoku
# Generate a secret key
export NANOKU_SECRET_KEY=$(openssl rand -base64 32)
# Required on first boot only
export NANOKU_ADMIN_USER=admin
export NANOKU_ADMIN_PASSWORD=changeme
./nanoku
Or grab a specific asset from the Releases page. You'll need to wire up your own systemd unit / launchd plist if you want auto-start on boot.
docker compose from source (alternative)For development or if you prefer to run from a local checkout:
git clone https://github.com/isaced/nanoku.git
cd nanoku
cp .env.example .env
# Edit .env — set NANOKU_ADMIN_PASSWORD and NANOKU_SECRET_KEY at minimum
docker compose up -d
The image is multi-arch (linux/amd64, linux/arm64) and is published
to both Docker Hub and GHCR on every release tag.
http://<host>:8080 and log in with the credentials from your
.env (NANOKU_ADMIN_USER / NANOKU_ADMIN_PASSWORD). The seed
only runs on a fresh DB; change the password from the UI afterwards.nanoku-caddy) on
first start. Sites served through Caddy will get automatic HTTPS
via Let's Encrypt once a domain is pointed at the host. To enable
auto-HTTPS, set NANOKU_CADDY_AUTO_HTTPS=true in /etc/nanoku/.env
and run sudo systemctl restart nanoku.NANOKU_SECRET_KEY is required — it derives the AES-256-GCM
key that encrypts registry passwords, trigger tokens, and env var
values at rest. Lost key = permanently lost secrets.The embedded SPA is built with React 19, Vite, TanStack Router, Ant
Design, and Tailwind 4. All routes are session-authenticated (except
/login and the HTTP trigger endpoint).
| Page | What it does |
|---|---|
/sites | Manage domains: one domain = one upstream, link to an app or free |
/apps | Create / edit apps, configure deploys, rotate trigger tokens |
/dashboard | Aggregate view: sites, running apps, live container stats (CPU/mem) |
/system | Caddy / nanoku logs, orphan container reconcile, version info |
/login | Admin sign-in |
The app editor is tabbed: General · Environment · Volumes · Network
(exposed ports) · Registry · Trigger. Switch an app between Docker
and Docker Compose deploy methods with a single toggle.
Each app runs in one of two modes:
<current_container>:<port>.On deploy, nanoku:
docker compose pull)A single DeployLock enforces one deploy per app at a time — calling
the trigger while a deploy is running returns 409 Conflict.
Nanoku doesn't build your code. It pulls pre-built images. The build runs in your CI; nanoku just exposes a small endpoint that fires off a deploy when called.
POST /api/apps/{name}/trigger
Authorization: Bearer <token>
Content-Type: application/json
{"tag": "v1.2.3", "commit_message": "fix: ..."}
{name} is the app's DNS-1123 name (set on create)<token> is the per-app bearer token, shown once when you
enable the trigger in the editor (or via
POST /api/apps/{id}/rotate-trigger-token)tag becomes the image tag; nanoku pulls
<app.image repo part>:<tag>202 Accepted with the new deploy id, or 409 if a deploy for that
app is already runningcrypto/subtle.ConstantTimeCompare —
exact matches only, no prefix / suffix tricksNANOKU_TRIGGER_URL: https://your-nanoku/api/apps/<name>/triggerNANOKU_TRIGGER_TOKEN: the token from step 1Drop this into your app repo at .github/workflows/deploy.yml:
name: deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ github.sha }}
- name: Notify Nanoku
env:
URL: ${{ vars.NANOKU_TRIGGER_URL }}
TOKEN: ${{ vars.NANOKU_TRIGGER_TOKEN }}
SHA: ${{ github.sha }}
MSG: ${{ github.event.head_commit.message }}
run: |
BODY=$(jq -nc --arg t "$SHA" --arg m "$MSG" '{tag: $t, commit_message: $m}')
curl -fsS -X POST "$URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$BODY"
BODY='{"tag":"v1.2.3","commit_message":"fix: ..."}'
curl -fsS -X POST "$NANOKU_TRIGGER_URL" \
-H "Authorization: Bearer $NANOKU_TRIGGER_TOKEN" \
-H "Content-Type: application/json" \
-d "$BODY"
Add matching registry credentials in the app editor (Registry URL +
username + password). The trigger worker logs into the registry before
pull and logs out after, so credentials don't linger in
~/.docker/config.json.
Everything is configured through NANOKU_* environment variables
(or command-line flags — see nanoku --help).
| Variable | Flag | Default | Description |
|---|---|---|---|
NANOKU_LISTEN | --listen | :8080 | HTTP listen address |
NANOKU_DB | --db | ./nanoku.db | SQLite database file path |
NANOKU_CADDYFILE | --caddyfile | ./Caddyfile | Generated Caddyfile path (host filesystem) |
NANOKU_CADDY_MODE | — | managed | Caddy integration mode (v1 only supports managed) |
NANOKU_CADDY_IMAGE | — | caddy:2 | Caddy image used by the managed container |
NANOKU_CADDY_CONTAINER | — | nanoku-caddy | Managed Caddy container name |
NANOKU_CADDY_VOLUME | — | nanoku-caddy-data | Caddy data volume name |
NANOKU_CADDY_NETWORK | — | nanoku-net | Docker network the managed Caddy and app containers share |
NANOKU_ACME_EMAIL | — | (empty) | Email for Let's Encrypt registration |
NANOKU_COMPOSE_DIR | — | ./composes | Where nanoku stores generated docker-compose.yml files |
NANOKU_DEPLOY_LOG_DIR | — | ./data/deploy-logs | Where per-deploy log files live |
NANOKU_SELF_CONTAINER | — | (empty) | nanoku's own container name (enables the "self log" view in /system) |
NANOKU_ADMIN_USER | — | admin | Seed admin username on a fresh DB; ignored after first boot |
NANOKU_ADMIN_PASSWORD | — | (empty) | Seed admin password; required on first boot |
NANOKU_SECRET_KEY | — | (empty) | Required. Passphrase for the AES-256-GCM key (generate with openssl rand -base64 32) |
NANOKU_CADDY_AUTO_HTTPS | — | (empty) | Set to true for Caddy auto-HTTPS in production |
NANOKU_TRUST_PROXY | — | false | Honor X-Forwarded-For for client IP. Enable only behind a trusted proxy |
DOCKER_HOST | — | unix:///var/run/docker.sock | Docker daemon socket |
| — | --skip-caddy-reload | false | Write Caddyfile but don't manage the Caddy container |
💡 Lost
NANOKU_SECRET_KEY= permanently lost secrets. The key is not stored anywhere except the env you set it in. There is no rotation tool yet — back it up.
# Requirements: Go 1.26+, Node 20+
go mod download
cd ui && npm ci && cd ..
# Run the stack: Go on :8080, UI dev server on :3000 (HMR)
make dev
# Build everything (UI + Go) into bin/nanoku
make build
# Run all tests
go test -race -count=1 ./...
cd ui && npm test
Local
go testwill fail to compile unlessinternal/api/distexists (it'sgo:embed'd). Runcd ui && npm run build(ormake build-ui) first.make devandmake buildboth do this for you.
See AGENTS.md for the full project layout, code style,
and testing conventions.
NANOKU_SECRET_KEY is required — nanoku refuses to boot without
it. It derives the AES-256-GCM key that encrypts
registry_password, trigger_token, and envvar.value at rest.registry_password is passed to docker login --password-stdin
(never via argv). The worker logs out after the pull.5/min). Only honor
X-Forwarded-For when you sit behind a trusted proxy that
sanitizes the header (set NANOKU_TRUST_PROXY=true).HttpOnly, SameSite=Strict,
Secure (when TLS). Sessions are stored SHA-256-hashed; the raw
token only ever lives in the cookie. TTL 7 days, sliding renewal.Origin: * for the trigger endpoint (CI calls
are cross-origin). Session cookies are SameSite=Strict so they
won't ride along cross-site./var/run/docker.sock. Anyone who can call the admin API can
spawn containers on the host.main.go — entry point: config, DB open + migrate, Caddy ensure,
session store, HTTP wiring, graceful shutdowninternal/api/ — HTTP handlers, routing, session auth, deploy lock,
embedded UIinternal/db/ — ent ORM schema + generated codeinternal/secret/ — AES-256-GCM sealerinternal/caddy/ — Caddyfile writer / rendererinternal/config/ — env-based config loadinginternal/docker/ — Docker manager (Caddy lifecycle, image pulls,
container stats, log streaming)ui/ — React 19 + Vite 8 + TanStack Router + Ant Design + Tailwind 4composes/ — example compose apps used by the Docker Compose deploy pathdocs/ — design records (proposals, schema, reviews)PRs welcome. Conventional commits enforced (see AGENTS.md for the
full list and changelog rules). Branch from main; test.yml must
be green before review.
# Before opening a PR
go test -race -count=1 ./...
cd ui && npm test
Released under the MIT License. See LICENSE (add one if you're
forking for distribution — none is currently committed).
Content type
Image
Digest
sha256:e8d4b53f4…
Size
28 MB
Last updated
2 months ago
docker pull isaced/nanoku