How to Run a Full-Stack Next.js App on One $5 VPS: HTTPS, Jobs, WebSockets, and Images

How to Run a Full-Stack Next.js App on One $5 VPS: HTTPS, Jobs, WebSockets, and Images

Tako-kun ·

One cheap VPS can run more than a homepage.

The interesting question is what happens after the first deploy. A real Next.js app wants HTTPS, static assets, API routes, background jobs, WebSocket or SSE updates, image optimization, secrets, logs, and a deploy path that does not turn every change into a tiny incident. You can bolt those together from separate tools. Or you can make the VPS run like a small app platform.

This is the Tako version: one server, one Next.js app, one tako.toml, and the full-stack pieces most apps reach for after week two. The deeper reference pages are the Next.js framework guide, deployment docs, development guide, and CLI reference.

Start with the Next.js runtime

Next.js is portable because it can run as a Node.js server. The current Next.js docs describe output: "standalone" as a deployment mode that writes a minimal .next/standalone/server.js; running that server starts the production app. Tako wraps that path instead of inventing a separate Next runtime.

Install the SDK and wrap your config:

bun add tako.sh
// next.config.ts
import { withTako } from "tako.sh/nextjs";

export default withTako({
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "cdn.example.com",
        pathname: "/uploads/**",
      },
    ],
  },
});

withTako() enables standalone output, installs the Tako adapter, adds *.test and *.tako.test to Next’s allowed dev origins, writes .next/tako-entry.mjs, and configures next/image to use Tako’s public optimizer. If Next emits standalone output, Tako uses it; otherwise the wrapper falls back to next start.

Then keep the deployment config small:

runtime = "bun"
preset = "nextjs"
app_root = "."

[envs.production]
routes = ["app.example.com"]
servers = ["vps-1"]

[images]
remote_patterns = ["https://cdn.example.com/uploads/**"]
formats = ["webp"]

app_root = "." is useful for root-level Next projects where channels/, workflows/, instrumentation.ts, and tako.d.ts live next to next.config.ts. If you keep backend definitions under src/, leave the default alone.

PieceWhere it livesWhat Tako does with it
Next server.next/tako-entry.mjsStarts the standalone server or next start
Public/static assetspublic/ and Next build outputServes matching files directly after route match
Image optimizer/_tako/imageValidates sources, transforms with libvips, caches variants
Durable channels<app_root>/channels/*.tsServes WebSocket/SSE endpoints under /_tako/channels/<name>
Durable workflows<app_root>/workflows/*.tsRuns jobs in supervised worker processes with persisted state
Routes, TLS, rollouts[envs.production] in tako.tomlMaps hostnames, manages certificates, and rolls new instances

Add jobs, WebSockets, and images

The one file Next.js apps need for backend primitives is instrumentation.ts. Next standalone runs routes in a child process, so the Tako runtime needs to initialize there before a route or server action enqueues a workflow or publishes a channel message.

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const { initServerRuntime } = await import("tako.sh/internal");
    initServerRuntime();
  }
}

Now add a workflow:

// workflows/send-receipt.ts
import { defineWorkflow } from "tako.sh";

export default defineWorkflow<{ orderId: string; email: string }>("send-receipt", {
  retries: 4,
  async handler(payload, ctx) {
    await ctx.run("email", async () => {
      ctx.logger.info("sending receipt", { orderId: payload.orderId });
      // send the email here
    });
  },
});

Workflows give you named step checkpoints, retries, cron schedules, sleeps, signals, and workers that scale to zero by default. They ship with the app on tako deploy, read the same secrets, and run next to the HTTP process without a separate Redis queue.

Then add a channel:

// channels/orders.ts
import { defineChannel } from "tako.sh";

export default defineChannel("orders", {
  auth: "public",
}).$messageTypes<{
  updated: { orderId: string; status: string };
}>();

Channels are served at /_tako/channels/orders. The proxy owns the WebSocket/SSE connection, stores published messages in a bounded replay log, and lets reconnecting clients catch up from a retained cursor. Use your product database for canonical history; use channels for live delivery and short reconnect replay.

Your Next route can now use both:

// app/api/orders/route.ts
import sendReceipt from "@/workflows/send-receipt";
import orders from "@/channels/orders";

export async function POST(req: Request) {
  const order = await req.json();

  await sendReceipt.enqueue({ orderId: order.id, email: order.email });
  await orders().publish({
    type: "updated",
    data: { orderId: order.id, status: "placed" },
  });

  return Response.json({ ok: true });
}

Images stay ordinary in React:

import Image from "next/image";

export function ProductHero() {
  return <Image src="/images/product.jpg" width={1200} height={800} alt="Product" priority />;
}

Because withTako() configured the loader, the browser requests /_tako/image?... instead of sending image work through a custom app route. Local sources are allowed by default, remote sources must match [images].remote_patterns, and WebP is the default output format. The optimizer uses source and transform caches, so the first request for a size is the expensive one and repeated requests are cheap.

Diagram

Deploy the whole app

Install the server once:

sudo sh -c "$(curl -fsSL https://tako.sh/install-server.sh)"
tako servers add prod-a.tailnet.ts.net --install

Then deploy:

tako generate
tako deploy --env production

Deploy validates the production environment, routes, secrets, server metadata, image and channel/workflow requirements, then builds and packages the app. The server extracts the release under /opt/tako/apps/{app}/{env}/releases/{version}/, runs the runtime plugin’s production install, starts a fresh instance, waits for health, adds it to the load balancer, and drains the old instance. Exact public routes use Let’s Encrypt by default; wildcard routes can use Cloudflare DNS-01, and Cloudflare-proxied apps can use Cloudflare Origin CA.

For local development, run:

tako dev

You get local HTTPS, .test hostnames, watched channel/workflow definitions, generated TypeScript declarations, local image optimizer routes, and the same shape as production. If something feels off, tako doctor checks the daemon, DNS, proxy, CA trust, and repair hints.

The point is not that one $5 VPS should run every company. The point is that a small box can be a complete application environment for a surprising amount of work. With Tako, the cheap server is not just a place where next start happens. It is the boundary that handles HTTPS, rollouts, jobs, WebSockets/SSE, images, logs, secrets, and routing for the app you already wrote.

That gives you a practical default shape:

App needOne-box answer
HTTPS and host routingTako proxy, routes, certificates, redirects
API routes and SSRNext.js standalone server behind the proxy
Background workDurable workflow workers, idle when unused
Live updatesDurable channels with bounded reconnect replay
Product and marketing medianext/image through Tako’s optimizer and cache
Operational looptako logs, tako status, tako releases rollback

When the app outgrows one machine, the same config model can add servers. Until then, the nice part is how little infrastructure vocabulary you need to introduce before shipping the full-stack version.

Start with the quickstart, skim the Next.js guide, or inspect the open-source repo. The box is small. The app does not have to be.