{"slug":"how-to-deploy-a-bun-hono-app-to-a-vps-without-docker","url":"https://tako.sh/blog/how-to-deploy-a-bun-hono-app-to-a-vps-without-docker/","canonical":"https://tako.sh/blog/how-to-deploy-a-bun-hono-app-to-a-vps-without-docker/","title":"How to Deploy a Bun Hono App to a VPS Without Docker","date":"2026-05-03T13:18","description":"A literal Bun + Hono walkthrough: export app.fetch, run tako init, and ship to a VPS with HTTPS and rolling deploys. No Dockerfile required.","author":null,"image":"dccf295808ef","imageAlt":null,"headings":[{"depth":2,"slug":"step-1---build-the-hono-app","text":"Step 1 - Build the Hono app"},{"depth":2,"slug":"step-2---install-tako-and-prepare-the-vps","text":"Step 2 - Install Tako and prepare the VPS"},{"depth":2,"slug":"step-3---run-tako-init","text":"Step 3 - Run tako init"},{"depth":2,"slug":"step-4---test-the-same-shape-locally","text":"Step 4 - Test the same shape locally"},{"depth":2,"slug":"step-5---deploy","text":"Step 5 - Deploy"},{"depth":2,"slug":"what-you-did-not-need","text":"What you did not need"}],"markdown":"[Hono](https://hono.dev) is a tiny web framework with a very useful property: a Hono app already speaks the web `fetch` shape. On Bun, that means your server can be one file that exports `fetch: app.fetch`. On [Tako](/docs), that also means your deploy target is just a Bun process behind Pingora, TLS, health checks, and rolling updates.\n\nNo Dockerfile. No image registry. No Nginx config. Let's walk the whole thing from `app.fetch` to `tako deploy`.\n\n## Step 1 - Build the Hono app\n\nStart with a plain Bun project:\n\n```bash\nmkdir hono-on-tako\ncd hono-on-tako\nbun init -y\nbun add hono\n```\n\nCreate `src/index.ts`:\n\n```typescript\nimport { Hono } from \"hono\";\n\nconst app = new Hono();\n\napp.get(\"/\", (c) => c.text(\"Hello from Hono on Tako\"));\n\napp.get(\"/api/health\", (c) =>\n  c.json({\n    ok: true,\n    runtime: \"bun\",\n  }),\n);\n\nexport default {\n  port: Number(process.env.PORT ?? 3000),\n  fetch: app.fetch,\n};\n```\n\nThat final export is the whole trick. Hono's `app.fetch` is the request handler. Bun can run that object directly for local smoke tests, and Tako's JavaScript SDK can import the same module, grab its `fetch` function, and run it under the port Tako chooses for the process.\n\nAdd a script to `package.json` if you want a direct Bun run command:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"bun --hot src/index.ts\"\n  }\n}\n```\n\nThen check it:\n\n```bash\nbun run dev\ncurl http://localhost:3000/api/health\n```\n\nYou now have a Hono API that is already shaped like a deployable Tako app. The [fetch handler pattern](/blog/the-fetch-handler-pattern) is doing the heavy lifting here: `Request` in, `Response` out, no framework adapter required.\n\n## Step 2 - Install Tako and prepare the VPS\n\nOn your laptop, install the CLI:\n\n```bash\ncurl -fsSL https://tako.sh/install.sh | sh\n```\n\nOn the VPS, install `tako-server` as root:\n\n```bash\nsudo sh -c \"$(curl -fsSL https://tako.sh/install-server.sh)\"\n```\n\nThe server installer creates the `tako` service user, installs the `tako-server` binary, registers the service, prepares `/opt/tako`, and gives the proxy permission to bind ports 80 and 443. That one server process owns routing, ACME certificates, process supervision, rolling updates, and the encrypted secrets store. The [deployment guide](/docs/deployment) has the longer day-two version; for this tutorial, the installer is enough.\n\nPoint a DNS A record at the VPS before the first deploy:\n\n| Thing            | Example                          |\n| ---------------- | -------------------------------- |\n| VPS public IP    | `203.0.113.10`                   |\n| DNS record       | `api.example.com A 203.0.113.10` |\n| Tako server name | `prod`                           |\n| Tako route       | `api.example.com`                |\n\nBack on your laptop, register the server once:\n\n```bash\ntako servers add 203.0.113.10 --name prod\n```\n\n`tako servers add` verifies SSH, detects the server target, and stores that server in your global Tako config. Future projects can reuse the same server name.\n\n## Step 3 - Run `tako init`\n\nInside the Hono project:\n\n```bash\ntako init\n```\n\nInit detects Bun from the project, writes `tako.toml`, updates `.gitignore`, pins your local Bun runtime version when it can, and installs the `tako.sh` SDK with Bun. For a small Hono app, keep the config explicit:\n\n```toml\nname = \"hono-on-tako\"\nruntime = \"bun\"\npackage_manager = \"bun\"\nmain = \"src/index.ts\"\n\n[envs.production]\nroute = \"api.example.com\"\nservers = [\"prod\"]\n```\n\nThere is no Hono preset because Hono does not need one. Presets are useful when a framework needs build output normalization, assets, or a special dev command. Hono is already a fetch handler, so `main = \"src/index.ts\"` is enough. The [framework guide](/docs/framework-guides#fallback-fetch-handler-no-preset) calls this the fallback fetch-handler path, but for Hono it is the natural path.\n\nIf your API has a build step, add it. If it does not, leave it out:\n\n```toml\n[build]\nrun = \"bun run build\"\n```\n\nFor a simple Bun API that runs TypeScript directly, you usually do not need that block. `tako deploy` will still package your source, upload it, run a production dependency install on the server, and launch the configured `main` under Bun.\n\n## Step 4 - Test the same shape locally\n\nBefore deploying, run the app through Tako:\n\n```bash\ntako dev\n```\n\nThis is not just a convenience wrapper around `bun run dev`. For JavaScript apps, `tako dev` uses the same SDK entrypoint shape as production: it imports your `main`, wraps the fetch handler, exposes the built-in status endpoint, and reports the bound port back to the local Tako daemon. The local proxy then serves the app on a `.test` hostname with HTTPS.\n\nFor this project you should see a route like:\n\n```text\nhttps://hono-on-tako.test/\n```\n\nThat local HTTPS path is useful for OAuth callbacks, secure cookies, service workers, and any code that behaves differently on plain `http://localhost`. The [development docs](/docs/development) cover the local proxy, DNS, and LAN mode pieces.\n\n## Step 5 - Deploy\n\nRun:\n\n```bash\ntako deploy\n```\n\nConfirm the production prompt, then watch the task tree:\n\n```text\nConnecting     ✓\nBuilding       ✓\nDeploying to prod\n  Uploading    ✓\n  Preparing    ✓\n  Starting     ✓\n\n  https://api.example.com/\n```\n\nOpen `https://api.example.com/api/health`. The first deploy issues a Let's Encrypt certificate automatically for the public route, starts one Bun instance, waits for the SDK readiness signal, and only then sends traffic to it.\n\nOn the server, the app is not a container. It is a native Bun process. Tako launches the Bun runtime entrypoint from `tako.sh`, imports `src/index.ts`, extracts the default `fetch` function from your Hono export, and serves it on `127.0.0.1` with an assigned port. The process reports that port back to `tako-server`; Pingora terminates HTTPS on `:443` and routes requests to the healthy instance.\n\n```d2\ndirection: right\n\nlocal: \"Laptop\" {\n  code: \"src/index.ts\\nHono app.fetch\"\n}\n\nartifact: \".tar.zst artifact\" {\n  style.fill: \"#FFF9F4\"\n  style.stroke: \"#2F2A44\"\n}\n\nserver: \"VPS\" {\n  proxy: \"Pingora proxy\\nHTTPS :443\" {\n    style.fill: \"#E88783\"\n  }\n\n  bun: \"Bun process\\nTako SDK + Hono fetch\" {\n    style.fill: \"#9BC4B6\"\n  }\n\n  proxy -> bun: \"Request -> Response\"\n}\n\nlocal.code -> artifact: \"tako deploy packages\"\nartifact -> server: \"SFTP upload\"\n```\n\nThat same flow is what gives you rolling deploys. On the next `tako deploy`, each server starts a new instance, waits for it to become healthy, adds it to the load balancer, drains an old instance, then moves the `current` symlink to the new release. If the new process cannot start, the old release keeps serving. The [CLI reference](/docs/cli#tako-deploy) lists the flags, and [the rolling update section](/docs/deployment#rolling-updates) explains the production behavior.\n\n## What you did not need\n\nThe Hono app is already the server interface Tako wants, so the deployment stack stays small:\n\n| Usual VPS chore             | What happens here                                                             |\n| --------------------------- | ----------------------------------------------------------------------------- |\n| Write a `Dockerfile`        | Skip it; Bun runs the app as a native process                                 |\n| Push an image to a registry | Skip it; Tako uploads a deploy artifact over SFTP                             |\n| Configure Nginx and Certbot | Skip it; Pingora and ACME live in `tako-server`                               |\n| Hand-roll restart scripts   | Skip it; deploys are health-checked rolling updates                           |\n| Copy `.env` files around    | Use [`tako secrets`](/docs/cli#tako-secrets) when you need production secrets |\n\nThis is why Hono is such a clean fit for Tako. The app code stays portable: remove Tako later and the handler still works on Bun. While it runs on Tako, you get the platform pieces around it: HTTPS, routing, logs, deploy history, rollbacks, secrets, and scaling commands.\n\nStart with one endpoint and one VPS. When the app grows, add [multiple environments or servers](/docs/deployment#configure-the-project), scale the desired instance count with `tako scale`, and keep shipping with the same `tako deploy`."}