Deployment guide

Deploying a Go (Gin) application on a VPS

Deploy on a VPS Cloud →

Tutorial

Development

Deploying a Go (Gin) application on a VPS

Go compiles into a single static binary with no dependencies: it's the ideal language for self-hosting. A Gin API fits in 15 to 30 MB of RAM and starts in milliseconds. This guide takes you from zero to a complete production stack: multi-stage Dockerfile with a `scratch` image, Docker Compose with Nginx and Certbot, live reload in development via `air`, secrets management, and troubleshooting the most common errors.

8 min read7 steps
Contents· Why self-host a Go API on a VPS1/10
  1. 01Why self-host a Go API on a VPS
  2. 02Concrete benefits
  3. 03Hardware and software prerequisites
  4. 04Multi-stage Dockerfile with scratch image
  5. 05Production Docker Compose with Nginx and Certbot
  6. 06Live reload in development with air
  7. 07Environment variables and secrets
  8. 08Troubleshooting: common errors
  9. 09Deploy Go (Gin) in one click from the ServOrbit Marketplace
  10. 10Going further

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 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, and builds and pulls are near-instant.

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
  • scratch or distroless Docker 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 and Docker Compose on the VPS. Point an api.mydomain.com domain at the VPS IP via an A record. 5 GB of disk is more than enough. On your development machine, Go 1.27+ is recommended — download from go.dev/dl.

Multi-stage Dockerfile with scratch image

The multi-stage Dockerfile is the key to a minimal final image. The first stage compiles the binary inside a full Go container; the second copies it into an empty scratch image — no shell, no OS, just your binary and the CA certificates for outbound HTTPS calls. Here is a complete Dockerfile for a Gin project:

FROM golang:1.27-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o server .

FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

The CGO_ENABLED=0 flag forces a 100% static binary, essential for FROM scratch to work. The -ldflags="-s -w" flags strip debug tables and reduce binary size by about 30%. The CA certificates COPY is essential as soon as your API contacts external HTTPS services (Stripe, SendGrid, etc.). If you need a shell for debugging, replace FROM scratch with FROM gcr.io/distroless/static-debian12 — same footprint, but with a few basic tools.

Production Docker Compose with Nginx and Certbot

  1. Create the file structure on the VPS

    Connect via SSH and create the project directory: mkdir -p /opt/myapp/{nginx/conf.d,certbot/www,certbot/conf} && cd /opt/myapp. This structure will hold the Nginx configuration and Certbot volumes.

  2. Write the docker-compose.yml file

    Create /opt/myapp/docker-compose.yml with three services: app (your Go image), nginx (reverse proxy) and certbot (SSL renewal). Minimal example:

    services:
      app:
        image: monapp:latest
        restart: unless-stopped
        env_file: .env
        expose:
          - "8080"
      nginx:
        image: nginx:1.27-alpine
        restart: unless-stopped
        ports:
          - "80:80"
          - "443:443"
        volumes:
          - ./nginx/conf.d:/etc/nginx/conf.d:ro
          - ./certbot/www:/var/www/certbot:ro
          - ./certbot/conf:/etc/letsencrypt:ro
        depends_on:
          - app
      certbot:
        image: certbot/certbot
        volumes:
          - ./certbot/www:/var/www/certbot
          - ./certbot/conf:/etc/letsencrypt
  3. Configure Nginx as a front-end proxy

    Create nginx/conf.d/app.conf. Start with an HTTP-only block to validate the ACME challenge, then add the HTTPS block after obtaining the certificate:

    server {
        listen 80;
        server_name api.mydomain.com;
        location /.well-known/acme-challenge/ {
            root /var/www/certbot;
        }
        location / {
            return 301 https://$host$request_uri;
        }
    }
    server {
        listen 443 ssl;
        server_name api.mydomain.com;
        ssl_certificate /etc/letsencrypt/live/api.mydomain.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/api.mydomain.com/privkey.pem;
        location / {
            proxy_pass http://app:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
  4. Obtain the Let's Encrypt certificate

    Start Nginx alone first, then run Certbot in webroot mode: docker compose up -d nginx && docker compose run --rm certbot certonly --webroot -w /var/www/certbot -d api.mydomain.com --email [email protected] --agree-tos. Once the certificate is issued, restart Nginx: docker compose restart nginx.

  5. Build and start the application

    Build the Go image on the VPS (or transfer it via docker save/load or a private registry): docker build -t monapp:latest .. Then start the full stack: docker compose up -d. Check logs with docker compose logs -f app.

  6. Automate SSL renewal

    Add a crontab on the VPS to automatically renew the certificate weekly: (crontab -l 2>/dev/null; echo "0 3 * * 0 cd /opt/myapp && docker compose run --rm certbot renew && docker compose restart nginx") | crontab -. Certbot only actually renews if expiration is within 30 days.

  7. Configure Gin for production

    In your code, switch Gin to release mode with gin.SetMode(gin.ReleaseMode) or the GIN_MODE=release environment variable to disable verbose debug logs. Read the port and secrets from the environment (os.Getenv), never hard-coded in the code.

Live reload in development with air

In development, manually recompiling and restarting on each change is tedious. air (github.com/air-verse/air, currently v1.67.4) watches your source tree and restarts the binary on every .go file change. Installation: go install github.com/air-verse/air@latest. Simply run air at your project root — a default .air.toml config is generated automatically on first run. To customize watched directories or exclude vendor/:

[build]
  cmd = "go build -o ./tmp/server ."
  bin = "./tmp/server"
  include_ext = ["go", "html", "tmpl"]
  exclude_dir = ["vendor", "testdata"]

air is for local development only: never install air in your production Docker image.

Environment variables and secrets

Never hard-code secrets (API keys, database passwords, JWT tokens) in source code. The standard Go practice is to read them from the environment with os.Getenv("DB_PASSWORD"). For local development, a .env file loaded by godotenv simplifies the workflow: go get github.com/joho/godotenv, then at startup:

import "github.com/joho/godotenv"
func main() {
    _ = godotenv.Load() // loads .env if present, silent in prod
    db := os.Getenv("DB_DSN")
    port := os.Getenv("PORT")
    if port == "" { port = "8080" }
    // ...
}

In production, pass variables via Docker Compose's env_file: .env directive (this file must never be committed — add .env to your .gitignore). For highly sensitive secrets (tokens, private keys), prefer Docker secrets or an external vault. The golden rule: a secret in a git log is a compromised secret.

Enable Go's pprof profiling behind a protected endpoint: import _ "net/http/pprof" exposed on an internal port accessible only via SSH tunnel (ssh -L 6060:localhost:6060 user@vps). 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 stripping the debug tables.

Troubleshooting: common errors

Here are the five most frequent errors on a first Go VPS deployment, with their exact error messages and the fix to apply.

1. standard_init_linux.go:228: exec user process caused: no such file or directory — The binary was compiled with CGO enabled and the scratch image contains no libc. Fix: recompile with CGO_ENABLED=0 go build -o server ..

2. exec format error — The binary was compiled for a different architecture (e.g., macOS ARM → Linux AMD64). Fix: verify GOOS=linux GOARCH=amd64 before building, or build directly on the VPS.

3. bind: address already in use (port 8080) — Another process is already listening on that port. Check with ss -tlnp | grep 8080 and stop the conflicting process, or change the port in your .env.

4. x509: certificate signed by unknown authority or certificate has expired — Your scratch container lacks CA certificates (/etc/ssl/certs/ca-certificates.crt), or the Let's Encrypt certificate has expired. Fix: verify the COPY --from=build /etc/ssl/certs/ca-certificates.crt is present in your Dockerfile, and re-run Certbot to renew.

5. permission denied on binary startup — The binary is not executable. Fix: check permissions with ls -la server and apply chmod +x server if needed. In a Dockerfile, COPY --from=build normally preserves permissions, but an explicit RUN chmod +x /server guarantees the result.

Deploy Go (Gin) in one click from the ServOrbit Marketplace

The ServOrbit Marketplace offers a preconfigured Go (Gin) Stack template: Go from the official go.dev binary, Nginx and PostgreSQL 16 are installed automatically on your VPS from the order form. No SSH session for the base setup — you arrive directly at the step of compiling your project and uploading the binary. The marketplace page contains the exact specifications, use cases, and a first-connection guide.

Going further

You now have a complete Go VPS deployment under your belt. To go even further: configure Traefik v3 as your reverse proxy instead of Nginx — it handles routing, Let's Encrypt certificates and load balancing in a single container with declarative Docker label configuration (see our [Traefik v3 on VPS guide](/blog/traefik-v3-vps-reverse-proxy)). For zero-downtime production updates, check our article on [Docker Compose update best practices](/blog/docker-compose-mise-a-jour-best-practices-vps): rolling deployments, health checks and rollback strategies. If you prefer to delegate the infrastructure, our [Go (Gin) template in the Marketplace](/applications/developpement/golang) provisions the entire stack in minutes.

Deploy your Go (Gin) Stack in minutes

The ServOrbit Go (Gin) Stack template installs Go 1.22+, Nginx, and PostgreSQL 16 on your VPS. Cross-compile your project and deploy the binary immediately.