Skip to Content
InfrastructureFleet Provisioning

Infrastructure Provisioning

Nanokit turns any server — from a bare DigitalOcean droplet to a managed cloud cluster — into a fully-managed container runtime. The infra and deploy blocks in your nanokit.yml are the single source of truth for how your infrastructure is provisioned, connected, and maintained.


Overview

When you run nkapp deploy or nkapp host setup, Nanokit performs a fully automated provisioning sequence:

  1. Connects to the target host over SSH using your configured key.
  2. Bootstraps the host if necessary — installing Docker, configuring UFW, and creating the Nanokit directory tree.
  3. Opens an SSH tunnel to the remote Docker socket so all subsequent Docker commands are proxied securely through your existing connection.
  4. Synchronises your project code to the remote host using the configured method (rsync or git).
  5. Prunes any stale build artefacts listed in prune.
  6. Runs your prepare command inside an isolated Docker container on the remote host.
  7. Starts or updates your services via the remote Docker daemon.

[!NOTE] Nanokit never requires you to open the Docker port to the internet. All Docker API traffic travels inside the encrypted SSH tunnel, so the daemon socket never leaves localhost on the remote machine.


VPS / Bare-Metal Setup

Follow these steps to go from a brand-new server to a running Nanokit deployment.

Step 1 — Provision a VPS

Create a server with any supported provider (see the Supported Providers table below). The only prerequisites are:

  • A Debian/Ubuntu-based OS (recommended: Ubuntu 22.04 LTS or later)
  • Root or sudo SSH access
  • Ports 22, 80, and 443 reachable from the internet

Step 2 — Add Your SSH Key

Upload your public key to the server so Nanokit can authenticate without a password:

ssh-copy-id -i ~/.ssh/id_rsa.pub root@<YOUR_SERVER_IP>

Verify the connection works before proceeding:

ssh -i ~/.ssh/id_rsa root@<YOUR_SERVER_IP> "echo connected"

Step 3 — Point the Environment at the Host

Set the SSH deploy target for the environment in nanokit.yml:

nkapp config deploy set target=root@<YOUR_SERVER_IP> -e production nkapp config deploy set sshKey=~/.ssh/id_rsa -e production

This writes the deploy.target / deploy.sshKey fields into the environments.production block, which all subsequent remote commands use.

Step 4 — Bootstrap the Host

Run the automated setup wizard against your chosen environment:

nkapp host setup production

Nanokit will SSH into the host and automatically:

  • Install Docker Engine (latest stable)
  • Configure UFW to allow ports 80 and 443 (HTTP/HTTPS) and keep 22 open
  • Create /opt/nanokit/projects/<name>/ and the Caddy gateway directory tree
  • Pull the Nanokit gateway image

[!TIP] You can re-run nkapp host setup <env> at any time to repair or upgrade a host. The command is idempotent — it will skip steps that are already complete.

Step 5 — Configure nanokit.yml

Add the infra and deploy blocks to your project configuration:

infra: provider: docker # use the VPS Docker daemon deploy: target: root@203.0.113.42 # SSH connection string sshKey: ~/.ssh/id_rsa # path to your private key sshPort: 22 # default; omit if unchanged method: rsync # rsync | git runtime: node:22-slim # Docker image for the prepare step prepare: npm ci && npm run build prune: - dist - apps/*/.next

Step 6 — Deploy

nkapp deploy --env production

Nanokit connects, syncs, builds, and starts your services. The first deploy on a fresh host may take a minute or two while Docker images are pulled.

[!IMPORTANT] Ensure the sshKey path resolves correctly on the machine running the CLI. Nanokit supports ~/, relative paths, and vault:// URIs for secret-manager-backed keys.


The SSH Tunnel Architecture

Nanokit never exposes the Docker daemon port (2375/2376) to the public internet. Instead, it creates a local SSH port-forward to the remote Unix socket each time a session begins:

Your Machine Remote VPS ┌──────────────────────┐ ┌──────────────────────────────────┐ │ nkapp CLI │ │ sshd │ │ │ SSH │ │ │ Docker client ─────┼──tunnel───┼──► /var/run/docker.sock │ │ (via tunnel) │ :22 only │ │ └──────────────────────┘ └──────────────────────────────────┘

The tunnel is established immediately after authentication and torn down when the CLI session ends. This means:

  • No extra ports need to be opened in your firewall.
  • mTLS is not required — SSH handles transport security.
  • All docker API calls — image pulls, container starts, volume management — travel through this single authenticated channel.

[!WARNING] If your SSH key is passphrase-protected and no SSH agent is running, the CLI will prompt for the passphrase. In CI/CD pipelines, use a dedicated key stored in your secrets manager (e.g., sshKey: vault://secrets/deploy-key) to avoid interactive prompts.


Synchronisation Methods

The deploy.method key controls how your source code is copied to the remote host before the build step runs.

rsync (default)

Nanokit runs rsync over the existing SSH connection, transferring only changed files:

deploy: method: rsync

Characteristics:

  • Fastest incremental transfers — only deltas are sent
  • Respects .gitignore and a built-in exclusion list (node_modules, .git, etc.)
  • Works without Git being installed on the remote
  • Ideal for large projects where minimal transfer time matters

git

Nanokit pushes a Git bundle or runs a remote git pull on the host:

deploy: method: git

Characteristics:

  • Full Git history is available on the remote (useful if prepare runs git log, changelogs, etc.)
  • Requires Git to be installed on the remote host (Nanokit installs it during host setup)
  • Slightly slower than rsync for large repositories with many files

[!TIP] Use rsync for frontend/Node.js monorepos with large node_modules (even though they are excluded, the directory tree walk is faster). Use git when your prepare step relies on Git metadata.


Isolated Build (The prepare Step)

After code is synced, Nanokit runs your prepare command inside an ephemeral Docker container on the remote host. This container:

  • Uses the image specified by deploy.runtime (e.g., node:22-slim)
  • Mounts the synced project directory as its working directory
  • Has no access to the host network by default
  • Is destroyed immediately after the command exits
deploy: runtime: node:22-slim prepare: npm ci && npm run build

The isolation guarantees that build tooling versions are pinned to the image tag, and that no build artefact can accidentally write outside the project directory.

[!NOTE] The prepare step runs after pruning and before services are restarted. If prepare exits with a non-zero code, the deploy is aborted and the previous running version is left untouched.

Custom Runtimes

You can use any Docker image as the build runtime:

deploy: runtime: python:3.12-slim # Python project prepare: pip install -r requirements.txt && python manage.py collectstatic --noinput
deploy: runtime: golang:1.22 # Go binary build prepare: go build -o ./bin/server ./cmd/server

Cache Pruning

The deploy.prune list specifies directories that should be deleted on the remote before the prepare step runs. This prevents stale build output from interfering with a fresh build:

deploy: prune: - dist - apps/*/.next - .turbo

Glob patterns are supported. Paths are relative to the project root on the remote host (/opt/nanokit/projects/<name>/).

[!CAUTION] Be conservative with prune paths. Pruning directories that contain user-uploaded files or persistent runtime data will delete them permanently. Keep build artefacts strictly separated from runtime state.


Supported Providers

The infra.provider key selects where Nanokit manages infrastructure. For VPS and bare-metal deployments, docker is almost always the correct choice.

ProviderValueNotes
Local / VPS DockerdockerDefault. Works on any Docker host, local or remote.
Amazon Web ServicesawsEC2 + ECS/Fargate, ECR image build/push, NLB ingress, VPC/subnets, EBS volumes, Route 53 DNS, ECS exec, log retrieval, telemetry. Authenticate via nkapp provider auth aws or standard AWS env vars.
DigitalOceandigitaloceanAt parity with AWS: Droplets and App Platform (managed ingress), DOCR image build/push, VPC networking, block volumes, DNS, discover/clean/exec, App Platform log retrieval, and Droplet telemetry via the DO Monitoring API. Authenticate via nkapp provider auth do or DIGITALOCEAN_TOKEN.
Google Cloud PlatformgcpCloud Run and Compute Engine, Artifact Registry, Cloud DNS — ~25 provider methods. Authenticate via nkapp provider auth gcp or GOOGLE_APPLICATION_CREDENTIALS.
Microsoft AzureazureVirtual Machines, container services, registry, networking and DNS. Authenticate via nkapp provider auth azure or AZURE_* env vars.

These are the only supported compute providers, and they are all implemented to roadmap.v1 parity. Cloudflare is additionally supported as a DNS-only provider (infra.dns: cloudflare); it is not a compute target.

[!NOTE] For managed-ingress providers (DigitalOcean App Platform, AWS NLB), Nanokit provisions the platform’s own load balancer rather than a self-managed gateway — so the Caddy ensureGateway step is intentionally a no-op on that path.

[!NOTE] The DigitalOcean integration covers the full flow end-to-end; the few remaining gaps (and optional cron/bridge parity) need a live DO account to validate.


Scaling Configuration

Nanokit supports both automatic and manual scaling policies, configured under the infra block.

Manual Scaling

Pin a fixed number of instances with specific roles:

infra: provider: aws scaling: manual instances: - type: t3.medium count: 2 role: web - type: t3.large count: 1 role: worker

Auto-scaling

Let Nanokit (or the underlying cloud provider) adjust capacity within bounds you define:

infra: provider: digitalocean scaling: auto min: 1 max: 10

Shared / Adopted Clusters

To deploy into an existing cluster that Nanokit should not modify (e.g., a shared staging cluster managed by another team), set infra.shared: true:

infra: provider: aws shared: true # adopt the cluster; do not provision or deprovision nodes

VPC and Networking

infra: provider: aws vpc: vpc-0abc12345def67890 cidr: 10.0.0.0/16

[!NOTE] vpc and cidr are only relevant for cloud providers that manage VPC networking (AWS, GCP, Azure, DigitalOcean). They are ignored for the docker provider.


Remote Directory Structure

When Nanokit provisions a VPS, it creates the following directory tree under /opt/nanokit/:

/opt/nanokit/ └── projects/ └── <project-name>/ ← synced project files live here ├── nanokit.yml ├── apps/ ├── packages/ └── .nanokit/ └── gateway/ └── data/ └── caddy/ ← Caddy reverse-proxy configuration & certificates
PathPurpose
/opt/nanokit/projects/<name>/Project root; destination for rsync/git sync
.nanokit/gateway/Internal Nanokit gateway service files
.nanokit/gateway/data/caddy/Caddy configuration, TLS certificate storage

[!NOTE] Do not manually edit files under .nanokit/gateway/. These are managed by Nanokit and will be overwritten on the next deploy.


Troubleshooting

Stale Infrastructure Locks

If a deploy is interrupted (e.g., by a network drop), Nanokit may leave a lock in place to prevent concurrent modifications. Unlock manually with:

nkapp infra unlock --env production

[!CAUTION] Only run infra unlock if you are certain no deploy is actively in progress. Unlocking during an active deploy can leave infrastructure in an inconsistent state.

SSH Connection Refused

  • Confirm port 22 is open: nc -zv <IP> 22
  • Check that your public key is in /root/.ssh/authorized_keys on the remote
  • Verify deploy.sshKey points to the private key that corresponds to the uploaded public key
  • If using a non-standard port, set deploy.sshPort explicitly

prepare Step Fails on Remote

  • SSH into the host and check container logs: docker logs nanokit-prepare-<id>
  • Verify the deploy.runtime image is pullable from the remote host (check internet access and Docker Hub rate limits)
  • Test your prepare command locally first using the same Docker image:
    docker run --rm -v $(pwd):/app -w /app node:22-slim sh -c "npm ci && npm run build"

Docker Not Found After host setup

Occasionally, Docker installation may fail silently if the host’s package index is stale. SSH into the host and run:

apt-get update && apt-get install -y docker.io systemctl enable --now docker

Then re-run nkapp host setup <env> to let Nanokit verify and complete the setup.

UFW Blocking Traffic

If your application is unreachable on port 80/443, check UFW status on the remote:

ssh root@<IP> "ufw status verbose"

Expected output should include 80/tcp ALLOW IN and 443/tcp ALLOW IN. If not, re-run nkapp host setup <env> or add rules manually:

ufw allow 80/tcp ufw allow 443/tcp

Rsync Fails with Permission Denied

Ensure the remote /opt/nanokit/projects/<name>/ directory is owned by the user specified in deploy.target. By default this is root:

ssh root@<IP> "ls -la /opt/nanokit/projects/"

If a previous setup created the directory with the wrong owner, fix it with:

ssh root@<IP> "chown -R root:root /opt/nanokit/projects/<name>"