Why self-host a Go API on a VPS
Unlike Java or Node, a Go application (with the Gin framework) compiles into a single static binary that needs neither an interpreter, nor a runtime, nor a shared library. You can literally drop the binary onto a VPS and run it. This lightness changes everything for self-hosting: a Gin API runs in 15 to 30 MB of RAM, handles thousands of requests per second on a single core, and starts in a few milliseconds. On a VPS, you make the most of this efficiency: an entry-level server is enough to serve a load that interpreted languages would require a much bigger machine for. You also avoid the complexity of bulky Docker images: a 10 MB scratch image is enough.
Concrete benefits
- Single static binary: no runtime to install, deployment by a simple file copy
- Tiny memory footprint (15-30 MB) allowing you to saturate a small VPS with requests
scratchordistrolessDocker image of 10-15 MB: near-instant builds and pulls- Startup in milliseconds: redeployments and restarts with no perceptible interruption
- Native concurrency via goroutines: a single vCPU handles thousands of simultaneous connections
- Reduced attack surface: no interpreter or system packages to patch
Hardware and software prerequisites
This is where Go shines: a Gin API without a database fits on a VPS with 1 vCPU and 1 GB of RAM, and stays at ease. With co-located PostgreSQL, aim for 2 GB to be comfortable. You don't even need Go on the server if you compile in CI or by local cross-compilation (GOOS=linux GOARCH=amd64 go build). Install Docker if you containerize, or nothing at all if you launch the binary directly under systemd. Point an api.mydomain.com domain at the VPS IP via an A record. 5 GB of disk is more than enough.
Step-by-step deployment
Cross-compile the binary for Linux
From your workstation, run CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app .. The CGO_ENABLED=0 flag guarantees a 100% static binary, indispensable for a scratch image. You get an app executable ready to run on any Linux.
Build a minimal Docker image
Write a multi-stage Dockerfile: a golang:1.22-alpine stage to compile, then FROM scratch which copies only the binary and the CA certificates (COPY --from=builder /etc/ssl/certs ...). The final image weighs about ten MB. Expose port 8080.
Configure Gin for production
Switch Gin to release mode with gin.SetMode(gin.ReleaseMode) or the GIN_MODE=release variable to disable the verbose debug logs. Read the port and the secrets from the environment (os.Getenv), never hard-coded in the code.
Launch the container
Start with docker run -d --restart unless-stopped -p 127.0.0.1:8080:8080 -e GIN_MODE=release monapp:latest. By binding on 127.0.0.1, you expose Gin only to the local reverse proxy, not directly to the Internet.
Place Nginx or Caddy in front
Configure a reverse proxy to http://127.0.0.1:8080. Caddy is particularly well suited here: with a simple reverse_proxy localhost:8080 directive, it automatically provisions the HTTPS certificate without Certbot.
Secure with TLS
With Caddy, HTTPS is automatic as soon as the domain points to the VPS. With Nginx, run certbot --nginx -d api.mydomain.com. Then check the TLS chain with curl -I https://api.mydomain.com.
Enable Go's pprof profiling behind a protected endpoint: import _ "net/http/pprof" exposed on an internal port only accessible via an SSH tunnel. You get CPU and memory flame graphs in real production without noticeable overhead. Also consider compiling with -ldflags="-s -w" to reduce the binary size by 30% by removing the debug tables.