nixoku

Deploy a NixOS machine from a nix flake.

Pick a flake and a size. nixoku provisions the VM, boots NixOS, applies your flake's module and serves the machine over HTTPS at a name of its own.

The deploy command: your flake and a size. The deployment ends ready at https://quiet-otter.nixoku.com.
nixoku deploy ./my-flake your flake: a folder or github:you/repo --size small how much machine you need

You decide two things

nixoku names the machine for you, with a hyphenated name like quiet-otter. Everything after that is nixoku's job.

A flake
Any flake reference nix understands, such as github:you/repo, or a local folder the CLI uploads for you. It exports nixosModules.default, and that is the whole contract.
A size
How much machine you need. Your flake is built on the machine itself, so even the smallest size has room for nix to work.

Where it runs

One flake, any cloud. Google Cloud is live now; the others are on the way.

  • Google Cloud

    Available now

  • AWS

    Coming soon

  • DigitalOcean

    Coming soon

  • Hetzner

    Coming soon

What you get

NixOS, ready for flakes
A NixOS base image with flakes on.
A name on the internet
Every machine gets a generated name such as quiet-otter.nixoku.com, pointing at its address.
HTTPS by default
Every machine is served over HTTPS at its name, with a Let's Encrypt certificate. Set nixoku.web.port to the port your app listens on and nixoku proxies to it.
SSH as nixoku
nixoku ssh quiet-otter logs you in with your own key as the nixoku user, who has sudo.
Logs as they happen
Provisioning, boot and the rebuild stream live to your terminal and to the web app, line by line.
Redeploy and destroy
nixoku redeploy applies a new revision to the same machine. nixoku destroy deletes the VM and its DNS record.
Checkpoints built in
Every deploy and redeploy is a new NixOS generation, a complete checkpoint of the system. If a change goes wrong, nixoku rollback quiet-otter switches back to the previous one. Generations are kept for two weeks.

Quick start

This is all you write

Your flake exports one module. It runs your app on a local port and tells nixoku which one to serve over HTTPS.

# flake.nix
outputs = { self, nixpkgs }: {
  nixosModules.default = ./module.nix;
};

Python. The module from examples/hello-app: Python serves a page on port 8000.

# module.nix
{ pkgs, config, ... }:
{
  nixoku.web.port = 8000;

  systemd.services.hello-app = {
    wantedBy = [ "multi-user.target" ];
    serviceConfig = {
      ExecStart = "${pkgs.python3}/bin/python3 -m http.server 8000 --bind 127.0.0.1 --directory ${
        pkgs.writeTextDir "index.html" "hello app via nixoku (${config.nixoku.fqdn})\n"
      }";
      DynamicUser = true;
      Restart = "on-failure";
    };
  };
}

TypeScript. server.ts sits next to the module. Node 24 runs TypeScript as is, so there is no build step.

# module.nix
{ pkgs, ... }:
{
  nixoku.web.port = 3000;

  systemd.services.app = {
    wantedBy = [ "multi-user.target" ];
    serviceConfig = {
      # Node 24 runs TypeScript directly (type stripping), no build step.
      ExecStart = "${pkgs.nodejs_24}/bin/node ${./server.ts}";
      DynamicUser = true;
      Restart = "on-failure";
    };
  };
}

Go. go.mod and main.go sit next to the module; nix builds the binary.

# module.nix
{ pkgs, ... }:
let
  app = pkgs.buildGoModule {
    pname = "app";
    version = "0.1.0";
    src = ./.; # go.mod (module app) and main.go
    vendorHash = null; # no dependencies; set the hash once you add some
  };
in
{
  nixoku.web.port = 8080;

  systemd.services.app = {
    wantedBy = [ "multi-user.target" ];
    serviceConfig = {
      ExecStart = "${app}/bin/app";
      DynamicUser = true;
      Restart = "on-failure";
    };
  };
}

Rust. Cargo.toml, Cargo.lock and src/ sit next to the module; nix builds the binary.

# module.nix
{ pkgs, ... }:
let
  app = pkgs.rustPlatform.buildRustPackage {
    pname = "app";
    version = "0.1.0";
    src = ./.; # Cargo.toml (package "app"), Cargo.lock, src/
    cargoLock.lockFile = ./Cargo.lock;
  };
in
{
  nixoku.web.port = 8080;

  systemd.services.app = {
    wantedBy = [ "multi-user.target" ];
    serviceConfig = {
      ExecStart = "${app}/bin/app";
      DynamicUser = true;
      Restart = "on-failure";
    };
  };
}

Then, from your terminal

nixoku login opens the browser so you can approve the CLI once. No keys to paste. nixoku deploy prints the machine's generated name; use it for the rest.

nixoku login                 # approve in the browser
nixoku deploy ./my-flake --size small
nixoku logs quiet-otter -f   # follow the build
nixoku ssh quiet-otter
nixoku rollback quiet-otter  # previous generation
nixoku destroy quiet-otter

When the deploy finishes, the CLI prints your machine's HTTPS address. Prefer clicking? The same steps work in the web app.

Join the waiting list

nixoku is opening up in small batches. Leave your email and we'll write when there is room for you. Nothing else goes to that address.

By joining, you ask us to email you about nixoku access and launch updates. Read our Privacy Policy and Terms of Use.