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.
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#
nextship server add root@203.0.113.10
nextship server add root@203.0.113.10 --yesThe 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#
nextship deploy --yesThe 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:
nextship domain add app.example.com --yesWhat is different from App Platform#
| App Platform | Your server | |
|---|---|---|
| Cache after a restart | Starts cold | Regenerated pages and optimized images kept in volumes |
| Logs of a replaced deployment | Gone | nextship logs --deployment <id> reads the journal |
| CDN | In front, keeps static pages | None unless you add one |
| Rollback | Restores the old spec, env included | Within 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 deployment | Loads the new build on its next navigation | Keeps working for an hour: requests naming its build reach it |
| Server Actions key | In a local file | On the server, shared by every machine that deploys |
| Apps | One per App Platform app | Many per server, each with its own domains |
| Failover | Managed | None: one server |
Keep it running#
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 --yesserver 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.

