{"slug":"how-to-deploy-a-go-gin-echo-or-chi-app-to-a-vps-without-docker","url":"https://tako.sh/blog/how-to-deploy-a-go-gin-echo-or-chi-app-to-a-vps-without-docker/","canonical":"https://tako.sh/blog/how-to-deploy-a-go-gin-echo-or-chi-app-to-a-vps-without-docker/","title":"How to Deploy a Go Gin, Echo, or Chi App to a VPS Without Docker","date":"2026-05-03T13:27","description":"A concrete Go walkthrough: pass Gin, Echo, or Chi to tako.ListenAndServe, then deploy a native binary to a VPS without Docker.","author":null,"image":"4b145b6a1bd7","imageAlt":null,"headings":[{"depth":2,"slug":"step-1---build-a-gin-app","text":"Step 1 - Build a Gin app"},{"depth":2,"slug":"step-2---echo-and-chi-use-the-same-shape","text":"Step 2 - Echo and Chi use the same shape"},{"depth":2,"slug":"step-3---prepare-tako-and-the-vps","text":"Step 3 - Prepare Tako and the VPS"},{"depth":2,"slug":"step-4---add-takotoml","text":"Step 4 - Add tako.toml"},{"depth":2,"slug":"step-5---run-it-locally-through-tako","text":"Step 5 - Run it locally through Tako"},{"depth":2,"slug":"step-6---deploy-the-binary","text":"Step 6 - Deploy the binary"},{"depth":2,"slug":"what-tako-adds-around-your-go-app","text":"What Tako adds around your Go app"}],"markdown":"Go web apps already have the interface Tako wants: `http.Handler`. Gin can be served by `net/http`. Echo has a server-compatible handler. Chi is proudly built around the standard library. That means the path from framework router to VPS deploy is small enough to fit in one sentence: build your router, pass it to `tako.ListenAndServe`, run `tako deploy`.\n\nNo Dockerfile. No image registry. No Nginx side quest. Just a Go binary behind [Tako's deployment layer](/docs/deployment): HTTPS, routing, readiness, health checks, rolling updates, logs, secrets, and scaling commands.\n\nLet's walk through Gin first, then swap the framework for Echo or Chi.\n\n## Step 1 - Build a Gin app\n\nStart with a normal Go module:\n\n```bash\nmkdir gin-on-tako\ncd gin-on-tako\ngo mod init example.com/gin-on-tako\ngo get github.com/gin-gonic/gin\ngo get tako.sh\n```\n\nCreate `main.go`:\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"tako.sh\"\n)\n\nfunc main() {\n\tr := gin.Default()\n\n\tr.GET(\"/\", func(c *gin.Context) {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"message\": \"Hello from Gin on Tako\",\n\t\t\t\"pid\":     os.Getpid(),\n\t\t})\n\t})\n\n\tr.GET(\"/api/health\", func(c *gin.Context) {\n\t\tc.JSON(http.StatusOK, gin.H{\"ok\": true})\n\t})\n\n\tif err := tako.ListenAndServe(r); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"server error: %v\\n\", err)\n\t\tos.Exit(1)\n\t}\n}\n```\n\nThe important line is the last one. In a typical Gin quickstart you would call `r.Run()`. On Tako, hand the router to `tako.ListenAndServe(r)` instead.\n\nGin's engine works with the standard `net/http` server shape, so Tako can wrap it the same way it wraps a plain `http.ServeMux`. The wrapper binds the port Tako gives the process, writes readiness back to Tako, intercepts internal `Host: tako.internal` status checks, and drains in-flight requests during rolling deploys.\n\nRun it directly once if you want a local smoke test:\n\n```bash\ngo run .\ncurl http://localhost:3000/api/health\n```\n\nOutside Tako, the SDK defaults to a normal local address. Under `tako dev` or `tako deploy`, Tako controls the private loopback port and the SDK reports it back when the app is actually listening.\n\n## Step 2 - Echo and Chi use the same shape\n\nThe Gin version is not special. The Go SDK is intentionally boring here: anything that implements `http.Handler` can be passed to `tako.ListenAndServe`.\n\n| Framework | Create the router      | Add a route           | Serve with Tako          |\n| --------- | ---------------------- | --------------------- | ------------------------ |\n| Gin       | `r := gin.Default()`   | `r.GET(\"/\", handler)` | `tako.ListenAndServe(r)` |\n| Echo      | `e := echo.New()`      | `e.GET(\"/\", handler)` | `tako.ListenAndServe(e)` |\n| Chi       | `r := chi.NewRouter()` | `r.Get(\"/\", handler)` | `tako.ListenAndServe(r)` |\n\nFor Echo:\n\n```bash\ngo get github.com/labstack/echo/v4\n```\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/labstack/echo/v4/middleware\"\n\t\"tako.sh\"\n)\n\nfunc main() {\n\te := echo.New()\n\te.Use(middleware.Logger())\n\te.Use(middleware.Recover())\n\n\te.GET(\"/\", func(c echo.Context) error {\n\t\treturn c.JSON(http.StatusOK, map[string]any{\n\t\t\t\"message\": \"Hello from Echo on Tako\",\n\t\t\t\"pid\":     os.Getpid(),\n\t\t})\n\t})\n\n\tif err := tako.ListenAndServe(e); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"server error: %v\\n\", err)\n\t\tos.Exit(1)\n\t}\n}\n```\n\nFor Chi:\n\n```bash\ngo get github.com/go-chi/chi/v5\n```\n\n```go\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n\t\"tako.sh\"\n)\n\nfunc main() {\n\tr := chi.NewRouter()\n\tr.Use(middleware.Logger)\n\tr.Use(middleware.Recoverer)\n\n\tr.Get(\"/\", func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tjson.NewEncoder(w).Encode(map[string]any{\n\t\t\t\"message\": \"Hello from Chi on Tako\",\n\t\t\t\"pid\":     os.Getpid(),\n\t\t})\n\t})\n\n\tif err := tako.ListenAndServe(r); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"server error: %v\\n\", err)\n\t\tos.Exit(1)\n\t}\n}\n```\n\nThat's the whole framework adapter story for these three frameworks. There is no adapter. The adapter is Go's standard interface.\n\nThe exception to remember is Fiber, because Fiber is built on `fasthttp` instead of `net/http`. For frameworks that own their own server loop, the Go SDK exposes `tako.Listener()` so you can pass in a pre-bound listener. Gin, Echo, and Chi do not need that path.\n\n## Step 3 - Prepare Tako and the VPS\n\nInstall the CLI on your laptop:\n\n```bash\ncurl -fsSL https://tako.sh/install.sh | sh\n```\n\nInstall `tako-server` on the VPS:\n\n```bash\nsudo sh -c \"$(curl -fsSL https://tako.sh/install-server.sh)\"\n```\n\nThe server installer sets up the service user, installs the server binary, prepares `/opt/tako`, and gives the proxy permission to bind ports 80 and 443. From there, `tako-server` owns routing, TLS certificates, process supervision, health checks, release directories, and the encrypted secrets store. The [deployment docs](/docs/deployment) cover the full production model.\n\nPoint DNS at the VPS before deploying:\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\nRegister the server once:\n\n```bash\ntako servers add 203.0.113.10 --name prod\n```\n\nThat stores the server in your global Tako config. Your project config can now refer to `prod` by name.\n\n## Step 4 - Add `tako.toml`\n\nRun init inside the Go project:\n\n```bash\ntako init\n```\n\nFor Go projects, init detects the runtime from `go.mod` and installs the `tako.sh` module with `go get`. Keep the resulting config explicit:\n\n```toml\nname = \"gin-on-tako\"\nruntime = \"go\"\nmain = \"app\"\n\n[build]\nrun = \"CGO_ENABLED=0 go build -o app .\"\n\n[envs.production]\nroute = \"api.example.com\"\nservers = [\"prod\"]\n```\n\n`runtime = \"go\"` selects Tako's Go runtime plugin. `main = \"app\"` tells the server which binary to execute after upload. The default Go build is `CGO_ENABLED=0 go build -o app .`, which produces the binary named by `main`.\n\nDuring deploy, Tako builds for Linux and injects the target `GOARCH` for the selected server. On the server, there is no Go runtime download and no production dependency install. The compiled binary runs directly.\n\nThis is the key difference from JavaScript frameworks: a Bun or Node app needs a runtime on the server; a Go app ships as the thing that runs.\n\n## Step 5 - Run it locally through Tako\n\nBefore the first deploy, run:\n\n```bash\ntako dev\n```\n\nFor Go, `tako dev` uses `go run .` by default. The SDK still speaks the same readiness protocol as production, so the local Tako daemon does not guess from stdout. It waits for the app to bind, receives the actual port, and serves your route through the local HTTPS proxy.\n\nYou should get a `.test` URL for the app, for example:\n\n```text\nhttps://gin-on-tako.test/\n```\n\nThat local HTTPS path is useful for cookies, OAuth callbacks, browser APIs, and testing the same routing shape you will use in production. The [development docs](/docs/development) explain the local proxy, route activation, and LAN mode.\n\n## Step 6 - Deploy the binary\n\nNow run:\n\n```bash\ntako deploy\n```\n\nOn the first deploy, Tako builds the Go binary, uploads a release artifact over SFTP, extracts it on the VPS, starts the new app instance, waits for SDK readiness, probes the internal status endpoint, and then routes traffic through Pingora on port 443.\n\n```d2\ndirection: right\n\nlocal: \"Laptop\" {\n  code: \"Gin / Echo / Chi\\nhttp.Handler\"\n  binary: \"Go binary\\napp\"\n  code -> binary: \"go build\"\n}\n\nartifact: \".tar.zst artifact\" {\n  style.fill: \"#FFF9F4\"\n  style.stroke: \"#2F2A44\"\n}\n\nvps: \"VPS\" {\n  proxy: \"Pingora proxy\\nHTTPS :443\" {\n    style.fill: \"#E88783\"\n  }\n\n  app: \"Native Go process\\nTako SDK + router\" {\n    style.fill: \"#9BC4B6\"\n  }\n\n  proxy -> app: \"loopback request\"\n  app -> proxy: \"response\"\n}\n\nlocal.binary -> artifact: \"tako deploy packages\"\nartifact -> vps: \"SFTP upload\"\n```\n\nOpen:\n\n```bash\ncurl https://api.example.com/api/health\n```\n\nThe app is now a native Go process on your VPS, managed by Tako. Future deploys are rolling updates: start a new instance, wait for it to become healthy, add it to the load balancer, drain an old instance, and move the release pointer. If the new binary cannot start, the old release keeps serving.\n\n## What Tako adds around your Go app\n\nThe code stays normal Go. Tako handles the deployment chores around it:\n\n| Usual VPS chore                 | What happens with Tako                                                                  |\n| ------------------------------- | --------------------------------------------------------------------------------------- |\n| Write a `Dockerfile`            | Skip it; deploy the compiled Go binary                                                  |\n| Push an image registry artifact | Skip it; Tako uploads a compressed release artifact over SFTP                           |\n| Configure Nginx and Certbot     | Skip it; `tako-server` handles HTTPS routing and certificates                           |\n| Poll logs over SSH              | Use [`tako logs`](/docs/cli#tako-logs)                                                  |\n| Copy `.env` files               | Use [`tako secrets`](/docs/cli#tako-secrets) and typed Go accessors from `tako typegen` |\n| Restart processes by hand       | Deploys and scaling commands manage instances                                           |\n\nIf you want to see complete working versions, the [Tako GitHub repo](https://github.com/lilienblum/tako/tree/master/examples/go) includes Gin, Echo, Chi, and plain `net/http` examples. The [Go SDK launch post](/blog/the-go-sdk-is-here) goes deeper on secrets, metadata helpers, channels, and why `http.Handler` is the right interface.\n\nStart with one router and one VPS. When the app grows, add more routes, secrets, environments, or servers in `tako.toml`. The deployment command stays the same: `tako deploy`."}