Self-host a Node.js app with a database, secrets and workers
Deploy a Node.js app with MariaDB, a session secret, a migration and two workers, without Docker. Then an upgrade, and one that rolls back.
· about 9 minutes to read
Most Node.js apps need more than a port: a database and its password, a session secret, a migration before each new version, and more than one process. This page deploys one that needs all of them, a small notes app built on Express and MariaDB, on a Linux x64 machine with no Docker and no sudo, then upgrades it and watches a broken upgrade roll back on its own. Every block of output below is from a run on 22 October 2026 on Nextweb 1.0.0-beta.1.rc.4 (build 170), cut with ... where the lines are progress. The addresses are the first instance's on a machine; yours may differ.
What you need
A Linux x64 machine with glibc (Ubuntu, Debian, Fedora and the like), Nextweb in a folder of its own, and a developer account to build packages with: Package your application sets one up. The app is in the Nextweb repository under examples/notes; copy it into a notes folder inside the Nextweb folder.
The app
notes/
server.js starts two workers with cluster.fork()
lib/app.js Express: users, notes, uploads
lib/sessionStore.js sessions in the database, so either worker can answer a signed-in user
lib/db.js a MariaDB pool from the NEXTWEB_DB_ variables
lib/checksum-job.js a forked job that checksums an upload
scripts/migrate.js the migrations, run before each new version starts
package.json express, express-session, bcrypt, mysql2The app reads everything it needs from its environment. Its database comes from NEXTWEB_DB_SOCKET, NEXTWEB_DB_USER, NEXTWEB_DB_PASSWORD and NEXTWEB_DB_NAME, its session secret from SESSION_SECRET, and its uploads go to NEXTWEB_DATA_DIR, a folder an upgrade does not touch. Sessions are kept in the database rather than in one worker's memory, because the next request may reach the other worker.
What Nextweb does with it is in .nextwebrc:
{
"owner": "example",
"runtime": "node",
"site": {
"entry": "server.js",
"verify": { "path": "/health", "statusCode": 200, "marker": "notes-ok" },
"release": "scripts/migrate.js",
"startTimeout": 60
},
"database": "mariadb",
"include": ["server.js", "lib/**", "scripts/**", "README.md"]
}"database": "mariadb" gives each site deployed from the package a database and a user of its own. release is the migration. verify is what apply checks before it calls the site live.
Build it
The package carries its node_modules, because nothing installs dependencies on the server. From the notes folder:
npm install --omit=dev
../nextweb pkg create example-notes 0.3.0
../nextweb pkg publishBuilding example-notes 0.3.0 for example (internal)...
warning: 10 prebuilt native addon(s) are carried, under prebuilds/ or in per-platform packages; Node loads the one that fits the machine
warning: left out 2 path(s): .nextwebrc, package.json (all of them: dist/example-notes_0.3.0_left-out.txt)
Built and registered dist/example-notes_0.3.0_all.zip.enc
owner example (you)
files 7 app files + 1057 in node_modules; source not encrypted
package key akek-2604fb770018
sha256 6753b3fdc03b4d0b9f72742856c5c493e79cdc37c13f006cba814be38c0b2373
size 2620168 bytes
Next: pkg validate, then pkg publish.
note: no VM check report for this file (optional: pkg validate --vm)
Publishing example-notes-0.3.0-all to https://cdn.nextwebjs.com...
Published: example-notes-0.3.0-all (2620168 bytes, sha256 6753b3fdc03b4d0b9f72742856c5c493e79cdc37c13f006cba814be38c0b2373)
It installs only on your own license: pkg install example-notespkg create checks the native addons the package carries. bcrypt ships builds for several systems under prebuilds/, and Node loads the one for Linux: that is the first warning. An addon built for another Node, such as Node 20, stops the build with its name. The second warning names the two files the build turns into the package's own package.json: the app's package.json, kept as it is so the app reads its own name and version, and .nextwebrc, whose settings are added to it. pkg publish puts the package on the content server, where only your own license can install it.
Deploy it
pkg install example-notes
deploy notes to notes.example.com using example-notesResolving dependencies...
Architecture: x64 (auto-detected)
Installation order: mariadb → example-notes
Installing 2 package(s)...
Installing mariadb...
downloading
verifying cache [##########] 100% 118.7/118.7 MB
extracting 435.2 MB, 1044 files (1s)
postinstall
...
mariadb-11.4.5-linux-x64 11.4.5 installed successfully
Installing example-notes...
downloading
verifying cache [##########] 100% 2.5/2.5 MB
extracting
postinstall
example-notes 0.3.0 installed successfully
Site "notes" deployed for notes.example.com (template: example-notes).
Site root: /home/you/nextweb/nextweb_server/sites/notes
Runs: /home/you/nextweb/nextweb_server/pkg/app/example-notes
Status: saved, apply to start it
Next step:
applyThe package brought MariaDB with it, because it declares a database. The deploy saves the site and says the one next step. There is nothing to configure first and nothing to commit.
Give it its secret
A session secret is not configuration: it is never shown and never on a command line, where ps, shell history and the activity feed would keep it. It is piped in, or typed at a prompt that does not show it:
printf %s "$(openssl rand -base64 32)" | ./nextweb secret notes set SESSION_SECRETSaved SESSION_SECRET. It reaches notes at its next start: apply -y (or reload notes).Start it
apply -yApplying configuration...
Applying site configuration...
Service mariadb: running (manage with "service mariadb status" / "service mariadb restart")
MariaDB: removed anonymous user @localhost, anonymous user @mac, grants on test and test_%, the empty test database
Site: notes (notes.example.com)
Allocated site listen port 4001 for notes
example-notes 0.3.0 runs from /home/you/nextweb/nextweb_server/pkg/app/example-notes
Database nw_notes and user nw_notes ready
Release: scripts/migrate.js (example-notes 0.3.0); the running version keeps serving
Release finished in 0.5 s
Service notes: running example-notes 0.3.0 (manage with "service notes status" / "service notes restart")
Visit: http://127.0.1.10:4001/
Site apply complete.
Configuration changes complete!
Changes 1, Warnings 0, Errors 0
┌──────────────────────────────────────────────────────────┐
│ notes is live → http://127.0.1.10:4001/ │
│ verify: passed · applied in 4.1 s │
└──────────────────────────────────────────────────────────┘
Next steps:
service logs notesApply starts MariaDB, and on its first start removes the anonymous accounts and the test database a new MariaDB comes with; the name after the second @ is your computer's. It makes the site's database and user, runs the migration while nothing is serving yet, starts the app, and runs its verify before it says the site is live. The migration and the two workers are in its log:
service logs notes--- 2026-10-22T08:39:54.069Z notes started ---
--- release example-notes 0.3.0 ---
migrated 001-users
migrated 002-notes
migrated 003-sessions
--- 2026-10-22T08:39:55.548Z notes started ---
notes 0.3.0 worker 2118966 on 127.0.1.10:4001
notes 0.3.0 worker 2118967 on 127.0.1.10:4001Upgrade it
Version 0.3.1 adds a migration, a pinned column on notes. Add it to scripts/migrate.js, set the version in package.json to 0.3.1, and build and publish it from the notes folder as before:
../nextweb pkg create example-notes 0.3.1
../nextweb pkg publishThen upgrade the package and apply:
pkg upgrade example-notes
apply -yResolving dependencies...
Architecture: x64 (auto-detected)
Installation order: mariadb → example-notes
mariadb 11.4.5 is already installed
example-notes 0.3.0 is installed; 0.3.1 is available
Installing 1 package(s)...
Installing example-notes...
downloading [##########] 100% 2.5/2.5 MB
extracting
postinstall
kept 0.3.0 to roll back to (7.5 MB)
example-notes upgraded from 0.3.0 to 0.3.1
Sites that run example-notes: notes. They run the new version once applied.
Next step:
apply -y
Applying configuration...
Applying site configuration...
Service mariadb: running (manage with "service mariadb status" / "service mariadb restart")
Site: notes (notes.example.com), example-notes 0.3.0 -> 0.3.1
Reusing site listen port 4001 for notes
example-notes 0.3.1 runs from /home/you/nextweb/nextweb_server/pkg/app/example-notes
Database nw_notes and user nw_notes ready
Release: scripts/migrate.js (example-notes 0.3.1); the running version keeps serving
Release finished in 0.5 s
Service notes: running example-notes 0.3.1 (manage with "service notes status" / "service notes restart")
Visit: http://127.0.1.10:4001/
Site apply complete.
Configuration changes complete!
Changes 1, Warnings 0, Errors 0
┌──────────────────────────────────────────────────────────┐
│ notes is updated → http://127.0.1.10:4001/ (0.3.1) │
│ verify: passed · applied in 2.9 s │
└──────────────────────────────────────────────────────────┘The upgrade keeps 0.3.0 to roll back to. The migration runs while 0.3.0 still serves, then the new version starts, passes verify, and the site is updated.
When an upgrade fails
Version 0.3.2 has a migration with a mistake in it: it alters notez, a table that does not exist. It is built and published the same way, and upgraded the same way:
pkg upgrade example-notes
apply -yResolving dependencies...
Architecture: x64 (auto-detected)
Installation order: mariadb → example-notes
mariadb 11.4.5 is already installed
example-notes 0.3.1 is installed; 0.3.2 is available
Installing 1 package(s)...
Installing example-notes...
downloading [##########] 100% 2.5/2.5 MB
extracting
postinstall
kept 0.3.1 to roll back to (7.5 MB)
example-notes upgraded from 0.3.1 to 0.3.2
Sites that run example-notes: notes. They run the new version once applied.
Next step:
apply -y
Applying configuration...
Applying site configuration...
Service mariadb: running (manage with "service mariadb status" / "service mariadb restart")
Site: notes (notes.example.com), example-notes 0.3.1 -> 0.3.2
Reusing site listen port 4001 for notes
example-notes 0.3.2 runs from /home/you/nextweb/nextweb_server/pkg/app/example-notes
Database nw_notes and user nw_notes ready
Release: scripts/migrate.js (example-notes 0.3.2); the running version keeps serving
Release failed: it exited with code 1: migration failed: Table 'nw_notes.notez' doesn't exist
notes: 0.3.2 release failed: it exited with code 1: migration failed: Table 'nw_notes.notez' doesn't exist
Reusing site listen port 4001 for notes
example-notes 0.3.1 runs from /home/you/nextweb/nextweb_server/pkg/app/example-notes
notes: rolled back to 0.3.1, which is running
Visit: http://127.0.1.10:4001/
Site apply finished with 1 error(s).
Configuration changes applied with errors.
Changes 0, Warnings 0, Errors 1
Next steps:
service logs notes
fix it and publish 0.3.3, or retry with pkg install example-notes@0.3.2
[NW] notes: 0.3.2 release failed; rolled back to 0.3.1The release failed, so 0.3.2 never started: apply put 0.3.1 back, and it never stopped serving. Apply exits with an error, and the next steps say how to go on. A migration that fails part way is not undone, so write each one so it can run again.
What does not work yet
- Outbound mail. Nextweb runs no mail server: configure SMTP in the app, or turn its mail features off.
- Separate users. Each site has its own database user, which keeps sites apart from mistakes, not from each other: every site runs as the user that started Nextweb. Give an app you do not trust a Nextweb folder, and a user, of its own.
- Swaps without a gap. An upgrade restarts the app, and for one to three seconds new connections are refused.
npmandnpxon the server, and keys for code started withspawn(process.execPath, ...)instead offork.
What changed in 1.0.0-beta.1.rc.4
This release is the first release candidate of beta 1. What this page uses is new in it: a database for any site, site variables and secrets, workers, start and verify timeouts, the release command and rollback. A deploy now saves its own site, and apply verifies every site it applies. Site processes no longer inherit the environment of the shell that started Nextweb; an app that read a variable from there now gets it with set <site> env NAME value.
A Nextweb from before, build 168 or older, stays on the version it has until it is updated:
./nextweb updateQuestions
Does the app need Docker?
No. Nextweb runs the app with the Node.js inside its own program, and installs MariaDB as a package into its folder.
Where does the database password come from?
Nextweb makes a database and a user for the site at its first apply, and gives the app their names and the password in environment variables, with a database URL for libraries that take one. The password stays in the site's private state.
Do cluster workers work?
Yes. Workers started with cluster.fork(), and children started with fork(), run on the same Node.js inside the nextweb program, with the package's keys.
What happens when an upgrade fails?
When the release command or the new version's start fails, apply puts the previous version back, says so and exits with an error. A failed migration is not undone.
Related: Your application on Nextweb, .nextwebrc, Package your application.