Integrations » Deploying a project

Deploying a project


What is deploy?

nuzur-cli deploy turns an approved model into a running server with a single command. Either point it at a Linux box you already own or let it create the server for you on DigitalOcean or Hetzner. From there it generates the application code from your project version, builds it on the box, provisions a database, applies your schema, and wires the whole thing back to nuzur — so you can manage the deployed data from the Data Manager straight away.

The generated code lands in a directory you own and can edit — commit it to git, add custom endpoints, and re-deploy to ship them. See Your app source.

You keep the server. nuzur never receives your SSH key, your database password, or your cloud provider credentials — everything runs from your machine over your own SSH connection, and the database password is generated on the box so the plaintext never crosses the wire.


Requirements

  • nuzur CLI installed on your machine (download)
  • A nuzur account with access to the project you want to deploy
  • A server, which is either:
    • one you already have — running Ubuntu or Debian (the bootstrap uses apt), reachable over SSH as root or a user with passwordless sudo; or
    • one nuzur creates for you on DigitalOcean or Hetzner, which needs that provider's CLI installed and logged in (see Where to deploy)
  • For a full app deploy: a Go Code Gen configuration for the project. Run nuzur-cli go-code-gen once (or pass --config-file) so the CLI knows which API surface, auth, and options to generate.

Quick start

Deploy to a server you already have:

nuzur-cli deploy --host 203.0.113.10 --project my-project

Or have nuzur create the server too — no --host, just a region:

nuzur-cli deploy --provider digitalocean --region nyc3 --project my-project

Either way that's a full deploy: the latest version of my-project, a self-hosted MySQL database, and the generated API served over HTTP.

For a real HTTPS certificate, point a domain at the server first and pass it:

nuzur-cli deploy --host 203.0.113.10 --project my-project --domain api.example.com

When it finishes, the CLI prints the deployment id, the agent uuid, the public URL, and a Data Manager link that opens the deployed database directly.


What deploy actually does

Each step is idempotent — re-running the same command updates the deployment in place rather than creating a second one.

  1. Resolves your project and version (defaults to the latest) and logs you in if needed.
  2. Generates the application code with the Go Code Gen extension (including a Dockerfile) into a persistent workspace you own — ./nuzur-<identifier> by default.
  3. Mints a single-use provisioning token (valid 15 minutes) so the server can pair itself without an interactive login.
  4. Creates the server and waits for SSH — managed providers only; with --provider ssh it uses the host you gave it.
  5. Copies the generated source to the server over SSH.
  6. Bootstraps the box: installs Docker, the database engine, and Caddy; creates the database and a least-privilege application user; builds the image; writes systemd units.
  7. Pairs the nuzur agent on the box and registers the database as a named connection.
  8. Applies your schema to the empty database through the SQL Push (local) extension.
  9. Records the deployment locally and reports it to nuzur, where it appears on the Deployments screen with live health.

What ends up on the server

Thing Where
App service {identifier}-api.service (systemd, Restart=always)
Container / image {identifier}-api / nuzur/{identifier}:latest
App config /etc/nuzur/{identifier}/config/prod.yaml (mode 0600)
DB password /etc/nuzur/{identifier}/db_password — generated on the box, never sent to nuzur
Front door Caddy, one site snippet per project in /etc/caddy/conf.d/{identifier}.caddy
Agent nuzur-agent.serviceone shared agent per box, serving every project on it
Backups Nightly dump to /var/backups/nuzur, 14-day retention (self-hosted databases only)
Firewall ufw allows SSH (22), plus 80/443 and the project's public port for a full deploy

The database binds to 127.0.0.1 and the agent connects outbound only, so neither is exposed to the internet.


Where to deploy

--provider picks how the server is obtained. Everything after that — the bootstrap, the database, the agent, the schema — is identical across providers.

Provider What it does Needs
ssh (default) Uses a Linux box you already have. Also the universal fallback for any provider without an adapter. --host
digitalocean Creates a droplet for you, adds a cloud firewall, and deletes it on destroy. doctl, --region
hetzner Creates a Hetzner Cloud server for you, adds a firewall, and deletes it on destroy. hcloud, --region

AWS, GCP, Azure, Vultr, Linode, and Scaleway are recognized but not built yet — naming one tells you it's planned rather than failing as unknown. Until then, create the VM yourself and deploy with --provider ssh --host <ip>.

Managed providers

nuzur never handles your cloud credentials. The adapters shell out to the provider's own CLI, so authentication stays wherever you already set it up:

# DigitalOcean — install doctl, then:
doctl auth init

# Hetzner — install hcloud, then:
hcloud context create

Deploy without a --host and the CLI creates the VM, waits up to 3 minutes for SSH, then runs the same bootstrap as any other server:

nuzur-cli deploy --provider hetzner --region nbg1 --project my-project --domain api.example.com

--region is required. The rest have sensible defaults you can override:

DigitalOcean Hetzner
--region a region, e.g. nyc3, fra1 a location, e.g. nbg1, fsn1, hel1, ash
--size s-1vcpu-1gb cpx22
--image ubuntu-22-04-x64 ubuntu-22.04

The VM is named nuzur-{identifier} and always logs in as root.

Hetzner: no single server type is offered in every location (the US locations use a different line), so if cpx22 isn't available where you're deploying, run hcloud server-type list and pass a supported one with --size.

SSH keys

Pass --ssh-key-name to attach a key already registered with the provider. Otherwise the CLI uploads the public half of your --ssh-key — or your default ~/.ssh/id_ed25519 or ~/.ssh/id_rsa — under the name nuzur-deploy and reuses it on later deploys.

Provider firewall

Managed deploys also get a provider-level firewall (nuzur-fw-{instance-id}) mirroring the box's own ufw: SSH, plus 80/443 and the public-port range for a full app, or SSH alone for --db-only. It's defense in depth and best-effort — the box's ufw stays the authoritative gate, so a firewall hiccup warns rather than failing the deploy.

Your app source

Deploy generates the app into a persistent directory on your machine and builds from it. That directory is yours: read it, edit it, commit it.

./nuzur-my-project/          # default; override with --source-dir

The path is recorded with the deployment, so later deploys reuse it without you re-passing the flag. --db-only deploys generate no app, so they have no workspace.

Re-deploys keep your edits

Re-running deploy regenerates into the same directory — it does not wipe it. Generated files (the ones carrying a Code generated ... DO NOT EDIT header) are refreshed to match your latest model; any file you've edited that isn't marked generated is left exactly as you wrote it. So your model changes and your hand-written code both survive a re-deploy.

That's what makes the workspace safe to keep long-term: nuzur-cli deploy is both "ship my schema change" and "ship my code change".

Use git

It's a normal directory — put it under version control:

cd nuzur-my-project
git init
git add . && git commit -m "initial generated app"

Now every re-deploy shows up as a reviewable diff: you can see exactly what codegen refreshed and what you changed. On first creation deploy writes a .gitignore and a .dockerignore for you, keeping secrets (config/prod.yaml, .env, *.key, *.pem) and build artifacts out of git and out of the image. Neither file is overwritten if it already exists — once they're in your workspace, they're yours.

The production config on the box (config/prod.yaml, holding the database password) is generated on the server and gitignored here. Committing this workspace does not commit your secrets.

Custom endpoints

Deploy with --custom and the generator emits a user-owned custom layer alongside the generated CRUD, which regeneration never overwrites:

File What it's for
app/rest.go Custom REST routes — no codegen needed
app/grpc.go Override or extend generated gRPC endpoints
app/idl/proto/custom.proto Brand-new gRPC RPCs — fill it in, then run app/idl/proto/gen.sh

Edit, re-run the same deploy command, and your endpoints ship with the rest of the app.

Deployment modes

Full app (default)

Generates the API, builds it on the box, self-hosts the database, and puts Caddy in front of it.

Database only

nuzur-cli deploy --host 203.0.113.10 --project my-project --db-only

Installs the database engine, pairs the agent, registers the connection, and applies the schema — but generates no app, no Docker, and no Caddy. You manage the database entirely through nuzur (Data Manager, SQL Push, queries). Because no code is generated, this works for any project without a Go Code Gen configuration, and only SSH is opened on the firewall.

Use an existing database

nuzur-cli deploy --host 203.0.113.10 --project my-project \
  --db-dsn "postgres://user:pass@db.example.com:5432/mydb?sslmode=require"

The app and agent connect to a database you already have (local or remote, MySQL or Postgres) instead of self-hosting one. The engine is inferred from the DSN — a postgres:// or postgresql:// URL means Postgres, anything else is parsed as a MySQL DSN (user:pass@tcp(host:port)/db?params). The DSN must include a database name.

With --db-dsn, deploy skips the database install, the user creation, and the backup cron — your provider owns those. The database is never dropped on destroy, even with --purge.

Deploy from a stored team connection

nuzur-cli deploy --host 203.0.113.10 --project my-project --connection <connection-uuid>

If the target database is already configured as a team connection in nuzur, deploy against it by uuid instead of retyping a DSN. The CLI resolves the credentials itself — the password is decrypted from KMS server-side, so it never lands in your shell history. --connection and --db-dsn are mutually exclusive, and the connection must belong to the project's team. Everything downstream behaves exactly like --db-dsn.

Save a database as a team connection

An external --db-dsn database registers only a connection scoped to you, so teammates can't open it in the Data Manager. After a successful --db-dsn deploy on an interactive terminal, the CLI offers to save it as a team connection. Decide up front in scripted runs with --save-connection or --no-save-connection — the default is not to save.

Saving requires you to be a team admin, and it never fails the deploy: if it can't be saved, deploy warns and moves on. Note that the team path connects to the database directly from nuzur cloud, so its host and port must be reachable from there — your own agent path works either way. This isn't offered for self-hosted deploys (a 127.0.0.1 database is unreachable from the cloud) or for --connection (already a team connection).

Choosing an engine

--db mysql (default) or --db postgres selects the self-hosted engine. For Postgres, --db-schema targets a namespace (default public); in MySQL the database is the schema, so the flag is ignored.

The self-hosted Postgres role is granted CREATEDB so nuzur's schema diff can materialize throwaway temp databases when pushing changes. It is still a non-superuser, localhost-only role.

HTTPS and domains

Pass --domain api.example.com (with DNS already pointing at the server) and Caddy provisions a real Let's Encrypt certificate automatically, serving the site on 443.

Without a domain, the project gets its own auto-assigned public port on the host IP over plain HTTP — starting at 8443 and counting up, so several projects can coexist on one box without domains. This is fine for testing; use a domain for anything real.

Flags

nuzur-cli deploy

Flag Default Description
--provider ssh Where to deploy: ssh, digitalocean, or hetzner
--host Target server IP or hostname (required for ssh)
--region Region/location to create the VM in (required for managed providers)
--size per provider Instance size/type
--image Ubuntu 22.04 OS image
--ssh-key-name uploads your key Name/id of an SSH key already registered with the provider
--project, -p Project name or UUID
--version latest Project version identifier or UUID
--user root SSH user
--ssh-key ssh-agent / ~/.ssh/config Path to an SSH private key
--port 22 SSH port
--sudo off Run the bootstrap via sudo (auto-enabled for non-root users)
--domain Domain for automatic HTTPS via Caddy
--identifier from the generator config, or the project name Names the DB, service, and config on the box
--db mysql Self-hosted engine: mysql or postgres
--db-only off Database + agent only — no app, Docker, or Caddy
--db-dsn Use an existing database instead of self-hosting one
--connection Deploy against a database saved as a team connection (mutually exclusive with --db-dsn)
--save-connection / --no-save-connection prompt on a TTY, otherwise no Save an external --db-dsn database as a team connection
--db-schema public Postgres schema to target (ignored for MySQL)
--api project's last config API surface: rest, grpc, or both
--auth project's last config Auth middleware: disabled, jwt, or keycloak
--custom off Generate the custom application layer for custom endpoints
--source-dir ./nuzur-<identifier> Directory for the app's source — where deploy generates, and you edit
--config-file last-used config Path to a JSON Go Code Gen config
--cli-install-cmd GitHub releases Override how the nuzur CLI is installed on the box
--schema-push-extension sql-push-local Extension used to auto-apply the schema
--web-url nuzur cloud nuzur web base URL for the Data Manager link

Pick --api by the consumer: REST for JS, web, and browser clients; gRPC for Go and backend clients.

nuzur-cli destroy <deployment-id>

Flag Description
--purge Also drop the database and app user (irreversible; the default keeps your data)
--keep-vm For a managed-provider deployment, keep the VM instead of deleting it
--skip-server Only revoke the agent and remove local state; leave the server untouched
--user / --port / --ssh-key Override the deployment's recorded SSH settings
--sudo Run the teardown with sudo (auto-enabled for non-root users)

After deploying

Manage your data

The CLI prints a Data Manager deep link that opens the deployed database with the connection preselected. The deployment also appears under Deployments in the nuzur web app, showing its configuration (host, engine, mode, ports, URLs, project version, CLI version) and live status pulled on demand from the agent: whether the app service is active, the container status and restart count, and whether the database is reachable. If the agent is offline, the configuration still shows and the health reads as unknown.

Ship changes

Re-run the same deploy command to roll out a new version — of your model, your code, or both. It regenerates your app source in place (refreshing generated files, keeping your edits), reuses the box's agent and connection, rebuilds the image, restarts the service, and applies the schema diff. Passwords, JWT signing keys, and ports are persisted per project, so a re-deploy never rotates them out from under a running app.

You can also push schema changes from nuzur at any time with SQL Push (local) or a change request — the agent applies them to the deployed database.

List deployments

nuzur-cli deploy list

Deployment records live on your machine at ~/.config/nuzur/deployments/<id>.json, and they're what destroy reads. Deploy from a different laptop and that machine won't know about the earlier deployment — the nuzur Deployments screen is the shared view.

Multiple projects on one box

One server can host several projects. Each gets its own database, config directory, systemd service, container, Caddy site, and public port — all namespaced by identifier — while the box keeps a single shared agent.

Deploying a second project reuses that agent instead of pairing a new one. destroy removes only that project's artifacts; the shared agent survives until you tear down the last project on the box.

Deploying a different project under an identifier already used on that host is refused, since they would collide on the derived database name. Give it a distinct --identifier.

Tearing down

nuzur-cli destroy my-project-a1b2c3d4

This removes the project's service, container, image, config, Caddy snippet, and backup cron; drops its connection from the agent; marks the deployment destroyed in nuzur (kept as history); and deletes the local record. Your database is kept unless you pass --purge, and an external --db-dsn database is never dropped.

For a managed-provider deployment, destroy also deletes the VM it created — but only once it's the last project on that box, so tearing down one of several projects never pulls the server out from under the others. Pass --keep-vm to keep it. A BYO-SSH box is never touched; it's yours.

If the box is unreachable, the teardown warns and still cleans up the nuzur side — re-run it later, or use --skip-server. If the VM deletion itself fails, the CLI tells you the instance id so you can remove it and stop the billing.

Security notes

  • Your SSH key and cloud provider credentials never leave your machine — deploy runs client-side, and managed providers shell out to your own already-authenticated CLI rather than asking nuzur to hold a token.
  • The database password is generated on the server and stored at 0600; nuzur only ever sees connection metadata, never the password.
  • The provisioning token used for headless pairing is single-use and expires in 15 minutes, and is scoped to your user and project.
  • The database binds to 127.0.0.1; the agent is outbound-only; ufw opens only SSH and the front-door ports.
  • The application runs as a least-privilege database user.

Next steps