How to Deploy Next.js to a VPS Without Docker
Networking update. The server setup described here uses Tako’s legacy VPN integration. For new installations, see native WireGuard networks. For an existing Tailscale or NetBird host, follow the migration guide.
This guide takes an existing Next.js app to a Linux VPS with Tako, using a native Node process and automatic HTTPS. If you are still choosing a host, start with Vercel versus Tako for Next.js.
What you need
- A Linux VPS with glibc, systemd, and cgroup v2 resource controllers. See the server requirements; not every Linux distribution is supported.
- Tailscale installed on your workstation and VPS, connected to the same tailnet, plus admin SSH access for server setup.
- A domain with an A record pointing to the VPS public IP. For this direct HTTPS setup, allow inbound traffic on ports 80 and 443.
- A working Next.js project with Node.js and its package manager installed locally. Builds run on your workstation.
You operate the VPS, including updates, capacity, and data recovery. Choose its size for your application’s memory needs and leave room for old and new instances to overlap during a rolling deploy.
Step 1 — Install the CLI and register the server
On your workstation:
curl -fsSL https://tako.sh/install.sh | sh
tako --version
With both machines connected to Tailscale, register the VPS using its MagicDNS name. This example assumes the host is named prod and its admin SSH user is ubuntu:
tako servers add ubuntu@prod
tako status
Replace ubuntu and prod with your actual admin user and Tailscale host. The admin-user form can install or repair tako-server; the saved server name defaults to the host’s first DNS label. Use that saved name in the configuration below.
The installer sets up the service, runtime dependencies, app isolation, and private management. The remote setup guide also covers installing from the host itself.
Step 2 — Wire up Next.js
From your Next.js project directory, install the SDK using your project’s package manager:
npm install tako.sh
Wrap your existing configuration in next.config.ts, preserving the options your app needs:
import { withTako } from "tako.sh/nextjs";
export default withTako({});
withTako() switches Next.js to standalone output, installs the Tako adapter, and lets next dev accept Tako’s local HTTPS hostnames (*.test). It also configures next/image to use Tako’s image optimizer; review image handling and remote sources if your app uses images. On build it emits .next/tako-entry.mjs — the small file Tako launches in production. The framework guide has the full breakdown.
Step 3 — tako init
In the project directory:
tako init
Init reads your package.json, sees next, and offers the nextjs preset. Use your production domain and the saved server name. The relevant parts of tako.toml are:
name = "my-app"
runtime = "node"
preset = "nextjs"
[envs.production]
route = "my-app.example.com"
servers = ["prod"]
Init handles SDK setup and tunes .gitignore so .tako/* is ignored — except .tako/secrets.json, which stays tracked. The nextjs preset bakes in main = ".next/tako-entry.mjs" so you don’t write that line yourself.
Keep any runtime pin generated by init; version pins use runtime = "node@<version>" in the same field. The unpinned example above selects the Node runtime.
Change route to your domain and servers to the registered server name. Check the app locally before deploying:
tako dev
Open the .test URL shown by Tako and check a page that renders on the server, a static asset, and an image if your app uses next/image.
Step 4 — tako deploy
tako deploy
Follow any deployment prompts, then open https://my-app.example.com/ using your configured domain. Verify a server-rendered page, static assets, images, and any authenticated flow your app needs. If startup or routing fails, use the troubleshooting guide.
Subsequent deploys use health-checked rolling updates. A successful deploy checks application readiness; it does not prove every application route works.
What just happened
tako deploy ran next build locally, packaged the result (excluding .git, .tako, .env*, and node_modules) into a .tar.zst artifact, and uploaded it over signed private HTTP through Tailscale. tako-server unpacked it under /opt/tako/apps/my-app/production/, ran a production install, and started the Next.js standalone server as a regular Node process. Pingora terminates TLS on :443 and routes to it.
What you didn’t write
- A
Dockerfile - A
docker-compose.yml - An
nginx.confand acertbotcron - A
.envfile copied into a container at build time - A GitHub Actions workflow that builds, tags, and pushes an image
That’s the difference. Next.js doesn’t need to be containerized to run; it’s a Node program that listens on a port. Tako treats it like one. The proxy, the TLS, the secrets store, and the rolling-restart coordinator all live in a single Rust binary on the box. Your Next.js app is a process, not a container image.
When you’re ready for more — secrets that don’t sit in env files, multiple environments on the same box, or durable workflows from inside your routes — it’s all the same tako deploy. Continue with the deployment guide for production operations and the backup guide before relying on persistent app data.
