nextshipnextship
Your own server

Your own server

Deploy to any Ubuntu or Debian server over SSH, behind Caddy with automatic HTTPS, and what stays yours to run.

One target covers any server you can SSH into: a Hetzner or Hostinger VPS, a DigitalOcean Droplet, EC2, Compute Engine, an Azure VM, or your own hardware. It needs Ubuntu 22.04, Ubuntu 24.04 or Debian 12, amd64 or arm64, at least 1 GB of RAM and 5 GB of free disk, and SSH as root or as a user with passwordless sudo.

A recording of nextship server add, which plans and then prepares a fresh Ubuntu server over SSH, then nextship deploy, which builds the app on the server, starts it behind Caddy and prints its URL, then curl loading the live page.

One uncut run on a fresh DigitalOcean Droplet, at four times speed. The commands are typed by a script so they can be read; every line of output is what nextship printed.

Set the server up#

Terminal
nextship server add root@203.0.113.10
nextship server add root@203.0.113.10 --yes

The first command changes nothing. It shows the server's host key fingerprint, which you compare with the one your provider shows, and every setup step with what it would change. The second installs Docker if needed, creates a nextship user and proves a login as it, runs Caddy on ports 80 and 443, limits the journal, enables security updates and ufw, installs a watchdog for containers that stay unhealthy, a cache guard that keeps what each app writes at runtime under a limit and a timer that stops a replaced deployment after its hour, and turns off SSH password logins last. From then on the host key is pinned in nextship.json and a server presenting any other key is refused.

--no-firewall, --no-auto-updates, --no-swap and --no-ssh-hardening opt out of those steps. Running server add again changes nothing.

Deploy#

Terminal
nextship deploy --yes

The image is built on the server's own Docker by default, reached over SSH with the pinned host key, through a private socket or, on Windows, a named pipe. No port is opened. --build local builds on your machine instead and streams the image over SSH, which suits a server with little memory. If the build's connection to the server's Docker drops mid-build, which has happened right after server add on a fresh server, deploy says so and builds once more; any other build failure is reported at once.

A new container starts with no published port. Caddy moves traffic to it only once Docker reports it healthy, then the previous container stops. A deployment that fails leaves the previous one serving; one that succeeds drops no request. Measured on a DigitalOcean Droplet: 579 of 579 requests and 51 of 51 streamed responses succeeded across a deployment.

The first app deployed answers http://<server>. Attach a domain and Caddy obtains its certificate:

Terminal
nextship domain add app.example.com --yes

What is different from App Platform#

App PlatformYour server
Cache after a restartStarts coldRegenerated pages and optimized images kept in volumes
Logs of a replaced deploymentGonenextship logs --deployment <id> reads the journal
CDNIn front, keeps static pagesNone unless you add one
RollbackRestores the old spec, env includedWithin an hour, switches back to the replaced deployment, still running with its env. Older, starts the old image with the current env file
A tab open across a deploymentLoads the new build on its next navigationKeeps working for an hour: requests naming its build reach it
Server Actions keyIn a local fileOn the server, shared by every machine that deploys
AppsOne per App Platform appMany per server, each with its own domains
FailoverManagedNone: one server

Keep it running#

Terminal
nextship server status   # the server and every app on it, with what needs attention
nextship server reboot --yes
nextship server move root@198.51.100.7 --yes

server reboot reboots and waits until every app is healthy again. server move copies the app, its env and its key to another server, deploys it there, and records the new server only once it serves.

Two things on the server exist nowhere else: /etc/nextship/apps/<name>/env and /etc/nextship/apps/<name>/secrets. Keep a copy of both. The nextship user is in the docker group, which is root-equivalent, so treat its keys as root's.

A tab opened before a deployment keeps working after it: the deployment a new build replaces keeps running for an hour and gets every request that names its build. A rollback within that hour switches back to it at once.

nextship deploy --preview pr-42 deploys a preview: a second app on the same server with its own env file and domains, which asks crawlers not to index it. A workflow that deploys one per pull request is in the server guide.

Each app's container is read-only apart from .next and a 64 MB /tmp that cannot run a binary, with no Linux capabilities and a limit of 512 processes. Code an attacker runs through a flaw in your app can neither rewrite it nor leave a program behind. An app that keeps a SQLite file or uploads beside its code lists the folders in nextship.json, as "writable": ["data"]: each becomes a volume it can write to, kept across deployments.

The full guide, including GitHub Actions and recovery, is docs/vm.md.