Go compiles to a single static binary — which makes deploying Go services to a VPS refreshingly simple: copy one file, run it. You only need the Go toolchain where you build; servers just run the binary.
Option 1: official tarball (latest version)
Ubuntu’s golang package often lags behind. The official release is easy to install:
GO_VERSION=1.23.4 # check go.dev/dl for the current version
wget https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> ~/.bashrc
source ~/.bashrc
go version
Use linux-arm64 on ARM servers. Remove the old version first (as above) — extracting over an existing install mixes files and breaks things.
Since Go 1.21, a project’s go.mod can request a newer toolchain, and the go command downloads it automatically.
Option 2: apt
sudo apt install -y golang-go
go version
Fine if the packaged version meets your project’s go directive. The ppa:longsleep/golang-backports PPA offers newer versions on Ubuntu.
Your first program
mkdir -p ~/hello && cd ~/hello
go mod init example.com/hello
cat > main.go <<'EOF'
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello from Go")
})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
log.Printf("listening on 127.0.0.1:%s", port)
log.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil))
}
EOF
go run .
Build a deployable binary
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o hello .
ls -lh hello
CGO_ENABLED=0— a fully static binary, no glibc dependency; runs on any Linux.-ldflags="-s -w"— strip debug info for a smaller file.
Cross-compile from your laptop
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o hello-linux .
scp hello-linux [email protected]:/opt/hello/hello
Build on macOS or Windows, deploy to Linux — no Go needed on the server.
Run it with systemd
sudo useradd --system --no-create-home --shell /usr/sbin/nologin hello
sudo mkdir -p /opt/hello && sudo cp hello /opt/hello/
/etc/systemd/system/hello.service:
[Unit]
Description=Hello Go service
After=network.target
[Service]
User=hello
ExecStart=/opt/hello/hello
Environment=PORT=8080
Restart=on-failure
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now hello
curl http://127.0.0.1:8080
The Protect* options sandbox the service. See systemd unit files.
Put Nginx in front
server {
server_name hello.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Then sudo certbot --nginx -d hello.example.com. Or let Caddy handle HTTPS — see Caddy reverse proxy.
Deploy updates
scp hello-linux deploy@server:/opt/hello/hello.new
ssh deploy@server 'sudo mv /opt/hello/hello.new /opt/hello/hello && sudo systemctl restart hello'
Moving the new file into place (rather than overwriting the running binary) avoids “text file busy” errors. For zero downtime, run two instances behind Nginx or use socket activation.
Docker option
A Go binary fits in a distroless or scratch image of a few megabytes — see multi-stage builds.
Useful Go tooling
go vet ./...
go test ./...
go install golang.org/x/vuln/cmd/govulncheck@latest && govulncheck ./...
govulncheck reports known vulnerabilities in your dependencies that your code actually calls.
Go services are lightweight; even the smallest VPS plan runs several comfortably.