Skip to content
Subnomic
← Back to blog
Sites Oct 27, 2026 · 3 min read

When the build is a server

The command that publishes a folder of files also deploys a Nuxt server or a Go binary. It works out which by reading the build, and says so before anything is uploaded.

subnomic sites deploy answers one question before it touches a file: is this build a site or a service? A site is files, served as they are. A service is a process, run in a microVM of its own with the site’s address in front of it. The answer comes from the build, not from a flag, and the CLI prints it:

$ subnomic sites deploy --prod
→ Detected a compiled service
  bin   ./api   arm64

So if you expected a static publish, you find out before the upload, not after it.

What counts as a server

A Nitro build with the node-server preset, the server half of a Nuxt build. The CLI finds .output/nitro.json, starts the server with the command recorded there, and checks before uploading that the file it names exists and that .output/server/node_modules does too: Nitro traces its dependencies into that folder, and the server cannot start without them. A static preset, which is what nuxt generate writes, is files and is published as files.

A Linux executable. If the build folder holds an index.html, it is a site and the search for a binary does not run, so a static site generated by a Go program publishes the site, not the generator. Otherwise the CLI looks in bin, dist, build, out and the project root, in that order, and the first of them holding a Linux (ELF) executable decides. It reads each file’s first bytes rather than trusting the executable bit, because a repository root is full of executable shell scripts.

Two candidates in that folder is a refusal, not a guess:

✗ error: bin holds 2 Linux executables (api, worker), so there is no obvious one to start.
Name the one to deploy with --dir, pointing at a directory that holds only it.

Refused before the upload

A build that cannot start is refused on your machine, before a byte moves, each case with its fix. A Mac or Windows binary:

✗ error: bin/api is not a Linux executable, so the microVM cannot start it.
Build for the guest instead:

    GOOS=linux GOARCH=arm64 go build -o bin/app .

Use GOARCH=amd64 if your workspace runs on x86 hosts.

A Nitro build made for another platform, such as the vercel or cloudflare-pages preset, with no static site to fall back on: the error names the preset and says to rebuild with NITRO_PRESET=node-server. A missing .output/server/node_modules, with the line “If a deploy step pruned it, stop pruning it.”

The architecture is printed, never enforced: a workspace’s hosts can be x86_64 or arm64, and arm64 beside the file is how you notice a build for the wrong one.

What goes up, and how it starts

A service is uploaded as one gzipped tar, with timestamps zeroed so the same build always has the same SHA-256, and the machine checks that digest against the bytes it downloads before anything runs. node_modules stays in; .git, .env files, .DS_Store, .svn and .hg are left out, and symlinks are neither followed nor stored.

The process is told where to listen through PORT, which is 3000. The CLI has no flag for it. Its other variables are the connection strings of managed datastores bound to it, DATABASE_URL for a Postgres and REDIS_URL for a Redis by default; there is no free-form list of environment variables.

Sizes and the plan

From a terminal, deploying a service that has no size yet asks which one to run on: the sizes the fleet sells, cut to what your plan reaches, with automatic first. --size <name> sets one without asking, and --size "" hands the choice back to the platform. Without a terminal or --size, nothing is asked, and a service with no size is left to the platform.

Three limits bound services: how many may run, how large one may be, and how much memory all of them may hold together. On the Free plan they are one service, 256 MiB, and 256 MiB. They are checked when a build is promoted, and only when that starts a service where none was running, so a redeploy or a rollback is never refused for a slot it already holds. A service is also a site, and counts toward the plan’s sites.

Left to the platform, a compiled binary asks for 256 MiB and a Node server for 512. On Free a Go binary fits, and a Nuxt server’s first promotion is refused until you choose a size the plan reaches:

✗ error: sites.services.error.total_memory_above_plan (402)

Live means promoted, not answering

With --prod the command returns once the build is promoted. Starting a microVM takes longer than a deploy should wait, so it says so:

✓ Live at https://orders.subnomic.app/
  the server is starting — give it a moment
https://orders.subnomic.app/

Promoting a new build releases the machine of the one it replaced. Retention keeps one earlier service build, not a static site’s ten: rolling a service back ten builds would run old code against data that has moved on.

What the process writes to its output is on the Logs tab of the service’s page in the console. It is read from the host’s journal, with no connection into the machine.

Sizes, managed datastores and egress are in Services, and every flag of sites deploy is in the CLI reference.