How to Install Go (Golang) on Ubuntu 24.04 VPS: Latest Version + systemd Setup
Go (often called Golang to make it searchable) is the language behind Docker, Kubernetes, Terraform, Caddy, Traefik, and thousands of production server binaries. Its combination of static compilation, built-in concurrency, and near-zero runtime dependencies makes it an exceptional choice for building network services that you plan to deploy on a VPS. This guide walks you through installing the latest Go toolchain on Ubuntu 24.04 from the official tarball, configuring your shell environment, building a production binary with proper compiler flags, and running it as a hardened systemd service.
Want a ready-to-code VPS? Deploy Ubuntu 24.04 with root SSH in 60 seconds on our CloudCore Starter plan and be building Go services by the end of this article.
Table of Contents
Why Go for Server Workloads
Go was designed at Google specifically to solve the problems of building and deploying large networked services. The language choices pay off in every Go VPS project you will ever run.
- Single static binary --
go buildproduces one self-contained executable. No runtime to install, no virtualenv, nonode_modules, no interpreter version mismatches. Copy the binary to any Linux server with a compatible kernel and it runs. - Tiny container images -- Because Go binaries link statically, you can ship them inside a
FROM scratchDocker image of 8-15 MB. Compare that to a typical Node or Python base image at 500 MB+. - Native concurrency -- Goroutines and channels let a single Go process handle tens of thousands of concurrent connections on a modest VPS. A 2 vCPU / 4 GB server can easily push 20k+ concurrent HTTP connections with a well-written handler.
- Fast cold start -- Go binaries start in milliseconds. Ideal for systemd-restarted services, cron jobs, and serverless-style tasks.
- Low memory footprint -- An idle Go HTTP server typically uses 10-30 MB of RAM. Perfect for resource-constrained VPS plans.
- Rich standard library --
net/http,crypto/tls,encoding/json,database/sql, andcontextcover most of what a backend service needs without external packages. - Excellent tooling --
go fmt,go vet,go test,go mod, andgo installare built in. No separate build tools, test runners, or package managers.
Prerequisites
Before starting, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access (PuTTY on Windows, the native terminal on macOS/Linux)
- At least 1 GB RAM and 5 GB free disk for the Go toolchain and a typical module cache
- Outbound internet access to reach go.dev and proxy.golang.org
Recommended Plan: CloudCore Starter>
Go compiles quickly and runs lean, so you do not need a heavy box. Our CloudCore Starter plan is ideal:>
- 2 vCPU cores
- 4 GB RAM
- 50 GB NVMe SSD
- Unmetered bandwidth
- Ubuntu 24.04 LTS image>
That is more than enough for building production APIs, CLI tools, microservices, and background workers.
Connect to the server:
ssh root@your-server-ipStep 1: Update System Packages
Refresh the package index and apply any pending upgrades before installing new software:
sudo apt update && sudo apt upgrade -yInstall a few utilities we will use for downloading and verifying the tarball:
sudo apt install -y wget curl git ca-certificates tar build-essentialbuild-essential is only required if you plan to use cgo (Go calling into C libraries such as SQLite's native driver). You can skip it for pure-Go projects.
If the kernel was updated, reboot before continuing:
sudo rebootStep 2: Download the Latest Go Tarball
Ubuntu's apt repositories ship an older Go version (often 1.22.x on 24.04), and the version lags behind upstream by several months. For security patches, new language features, and the latest standard library, install directly from go.dev/dl.
At the time of writing, the current stable release is Go 1.22+. Check go.dev/dl for the newest version and replace the version number below if needed.
cd /tmp
GO_VERSION="1.22.5"
wget https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gzIf your VPS is ARM-based (some cloud providers offer Ampere/Graviton nodes), download the ARM64 build instead:
wget https://go.dev/dl/go${GO_VERSION}.linux-arm64.tar.gzVerify the Download (Recommended)
Every release on go.dev/dl lists a SHA-256 checksum. Verify the tarball before extracting:
sha256sum go${GO_VERSION}.linux-amd64.tar.gzCompare the output with the checksum shown on go.dev/dl for that exact filename. They must match character-for-character.
Step 3: Extract Go to /usr/local/go
The official recommendation from the Go team is to extract the archive into /usr/local, which produces a complete Go tree at /usr/local/go. Remove any previous install first so you do not end up with stale files:
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf /tmp/go${GO_VERSION}.linux-amd64.tar.gzAfter extraction, the directory layout looks like this:
/usr/local/go/
├── api/
├── bin/ # go, gofmt
├── doc/
├── lib/
├── misc/
├── pkg/
├── src/ # standard library source
└── test/The two binaries that matter day-to-day are go (the toolchain entrypoint) and gofmt (the auto-formatter), both in /usr/local/go/bin.
Clean up the downloaded tarball:
rm /tmp/go${GO_VERSION}.linux-amd64.tar.gzStep 4: Configure PATH with /etc/profile.d/go.sh
Adding Go to PATH via a file in /etc/profile.d/ makes the toolchain available to every login shell for every user on the server, which is much cleaner than editing each user's ~/.bashrc.
sudo tee /etc/profile.d/go.sh > /dev/null <<'EOF'
export GOROOT=/usr/local/go
export GOPATH=$HOME/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
EOF
sudo chmod 644 /etc/profile.d/go.shWhat each variable does:
GOROOT=/usr/local/go-- The install location of the Go toolchain itself. Go normally detects this automatically, but setting it explicitly avoids ambiguity on systems with older Go versions installed via apt.GOPATH=$HOME/go-- The per-user workspace where Go stores downloaded modules, cached build artifacts, and binaries installed withgo install. Defaults to~/goif unset, but being explicit helps when scripting.PATH=...:$GOROOT/bin:$GOPATH/bin-- Puts thegoandgofmtcommands on your shell path, plus any binaries you install withgo install(they land in$GOPATH/bin).
source /etc/profile.d/go.shFor non-login shells (such as systemd services or some CI runners), you may also need to source this file explicitly or set the variables in the unit file.
Step 5: Verify the Installation
Check the installed version:
go versionExpected output:
go version go1.22.5 linux/amd64Inspect the full Go environment:
go envExpected output (abbreviated):
GO111MODULE=''
GOARCH='amd64'
GOBIN=''
GOCACHE='/root/.cache/go-build'
GOMODCACHE='/root/go/pkg/mod'
GOOS='linux'
GOPATH='/root/go'
GOROOT='/usr/local/go'
GOTOOLCHAIN='auto'
GOVERSION='go1.22.5'The key values to confirm are GOROOT, GOPATH, and GOVERSION. If any look wrong, re-check /etc/profile.d/go.sh and re-source it.
Run a quick smoke test by compiling and executing the smallest valid Go program:
mkdir -p /tmp/hello && cd /tmp/hello
cat > main.go <<'EOF'
package main
import "fmt"
func main() { fmt.Println("Go is working") }
EOF
go run main.goExpected output:
Go is workingGo is installed and functional. Time to build something real.
Step 6: Set Up GOPATH and go install
Modern Go (1.16+) operates primarily in module mode, where each project has its own go.mod file and dependencies are pinned per project. GOPATH no longer dictates where your source code must live -- you can keep projects anywhere. However, GOPATH still plays two important roles:
$GOPATH/pkg/mod.go install github.com/user/tool@latest land in $GOPATH/bin.Install a useful developer tool to verify the workflow:
go install golang.org/x/tools/cmd/goimports@latestAfter completion, the binary is at $GOPATH/bin/goimports. Because we added $GOPATH/bin to PATH in the previous step, you can run it directly:
goimports -hOther common tools worth installing:
# Linter (catches bugs and style issues)
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latestLive reload for development (see Step 11)
go install github.com/air-verse/air@latestDependency graph visualizer
go install github.com/loov/goda@latestMigrations tool
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latestThese all land in $GOPATH/bin and work from anywhere.
Step 7: Create Your First Module (go.mod / go.sum)
Go modules are the standard way to manage dependencies since Go 1.11. Each project directory contains:
go.mod-- declares the module name, Go version, and direct dependencies with version constraintsgo.sum-- cryptographic checksums of every downloaded dependency, committed to git for reproducible builds
mkdir -p ~/projects/hello-api && cd ~/projects/hello-api
go mod init github.com/yourusername/hello-apiThis creates a go.mod file:
module github.com/yourusername/hello-api
go 1.22
Create main.go:
cat > main.go <<'EOF' package mainimport ( "encoding/json" "log" "net/http" "os" "time" )
type Response struct { Message string
json:"message"Time time.Timejson:"time"Host stringjson:"host"}func handler(w http.ResponseWriter, r *http.Request) { hostname, _ := os.Hostname() resp := Response{ Message: "Hello from Go on Ubuntu 24.04", Time: time.Now().UTC(), Host: hostname, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }
func main() { port := os.Getenv("PORT") if port == "" { port = "8080" } http.HandleFunc("/", handler) log.Printf("Listening on :%s", port) log.Fatal(http.ListenAndServe(":"+port, nil)) } EOF
Run it:
go run main.goExpected output:
2026/04/16 12:00:00 Listening on :8080In another SSH session, hit the endpoint:
curl http://localhost:8080/Expected output:
{"message":"Hello from Go on Ubuntu 24.04","time":"2026-04-16T12:00:01.234567Z","host":"your-hostname"}Stop the server with Ctrl+C. No external dependencies were needed because the standard library covers the whole example -- notice go.sum was never created. The moment you import a third-party package and run go mod tidy, Go fetches it and populates go.sum with checksums.
Step 8: Production Build Flags and Static Binaries
Running go run is fine for development, but production deployments use go build to produce a standalone binary. For the smallest, most portable result, use these flags:
CGO_ENABLED=0 go build \
-ldflags="-s -w" \
-trimpath \
-o hello-api \
.Breakdown of each flag:
CGO_ENABLED=0-- Disables cgo. Without this, Go may link against the system's libc, producing a dynamically linked binary that depends on the glibc version of the build host. Disabling cgo produces a fully static binary that runs on any Linux kernel with the same architecture -- Alpine, distroless,FROM scratch, anything.-ldflags="-s -w"-- Strips the symbol table (-s) and DWARF debug information (-w) from the binary. Typically cuts 25-35% off the file size. You lose the ability to debug crashes with a stack trace that includes line numbers, so only use this for release builds, not development.-trimpath-- Removes absolute filesystem paths from the compiled binary, replacing them with module-relative paths. Makes builds reproducible and prevents leaking your build server's directory structure.
ls -lh hello-api
file hello-apiExpected output:
-rwxr-xr-x 1 root root 5.9M Apr 16 12:05 hello-api
hello-api: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ...
A ~6 MB static binary that runs anywhere. Run it:
./hello-apiEmbedding Version Information
You can inject build metadata at compile time using -ldflags -X:
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev") COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
go build \ -ldflags="-s -w \ -X main.version=${VERSION} \ -X main.commit=${COMMIT} \ -X main.buildTime=${BUILD_TIME}" \ -trimpath \ -o hello-api \ .
In your Go code, declare the corresponding variables:
var (
version = "dev"
commit = "unknown"
buildTime = "unknown"
)Now ./hello-api --version can print a rich build identifier.
Step 9: Cross-Compile for Other Platforms
One of Go's party tricks: you can build binaries for any OS and architecture from a single Linux VPS. No cross-compilers to install, no toolchain juggling.
Set GOOS and GOARCH and run the build:
# macOS Apple Silicon
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o hello-api-darwin-arm64 .macOS Intel
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o hello-api-darwin-amd64 .Linux ARM64 (Raspberry Pi, Ampere, Graviton)
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o hello-api-linux-arm64 .Windows 64-bit
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o hello-api.exe .FreeBSD 64-bit
GOOS=freebsd GOARCH=amd64 CGO_ENABLED=0 go build -o hello-api-freebsd .List every supported target:
go tool dist listThis is how distribution channels like GitHub releases deliver binaries for a dozen platforms from one CI pipeline.
Step 10: Run Your Binary as a systemd Service
For production, you want your Go service to start on boot, restart on failure, log to the journal, and run as a non-privileged user. systemd gives you all of that with one unit file.
Create a Dedicated System User
Never run Go services as root. Create a system user with no shell and no home directory writes:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin helloapiPlace the Binary
Copy the compiled binary to a stable path and set ownership:
sudo mkdir -p /opt/hello-api
sudo cp hello-api /opt/hello-api/
sudo chown -R helloapi:helloapi /opt/hello-api
sudo chmod 755 /opt/hello-api/hello-apiWrite the systemd Unit
sudo tee /etc/systemd/system/hello-api.service > /dev/null <<'EOF' [Unit] Description=Hello API (Go) After=network-online.target Wants=network-online.target[Service] Type=simple User=helloapi Group=helloapi WorkingDirectory=/opt/hello-api ExecStart=/opt/hello-api/hello-api Environment="PORT=8080" Environment="GIN_MODE=release"
Logging
StandardOutput=journal StandardError=journal SyslogIdentifier=hello-apiRestart policy
Restart=on-failure RestartSec=5sSecurity hardening
NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true RestrictNamespaces=true RestrictRealtime=true LockPersonality=true MemoryDenyWriteExecute=true SystemCallArchitectures=native SystemCallFilter=@system-service SystemCallFilter=~@privileged @resources CapabilityBoundingSet= AmbientCapabilities= ReadWritePaths=/opt/hello-apiResource limits
LimitNOFILE=65536
[Install] WantedBy=multi-user.target EOF
What each security directive does:
NoNewPrivileges-- Prevents the binary from gaining more privileges via setuid binaries or capabilities.PrivateTmp-- Gives the service an isolated/tmpand/var/tmp.ProtectSystem=strict-- Mounts/usr,/boot,/efi, and/etcread-only. Combined withReadWritePaths=, you whitelist only the directories the service actually needs to write to.ProtectHome-- All home directories become inaccessible.ProtectKernelTunables/ProtectKernelModules/ProtectControlGroups-- Blocks writes to/proc/sys,/sys, and cgroup config. A compromised binary cannot alter kernel state.MemoryDenyWriteExecute-- Prevents mmap'ing pages as both writable and executable. Blocks most runtime code injection attacks. Works with Go; does not work with JIT languages.SystemCallFilter-- Only allows syscalls in the@system-serviceset, minus privileged and resource-manipulation ones. Drastically reduces kernel attack surface.CapabilityBoundingSet=(empty) -- Strips every Linux capability. The service cannot bind to ports below 1024, mount filesystems, change user, etc. If you need to bind to port 80/443, use a reverse proxy (see Step 13) or addAmbientCapabilities=CAP_NET_BIND_SERVICE.LimitNOFILE=65536-- Raises the open-file descriptor limit. HTTP servers handling many concurrent connections hit the default 1024 limit quickly.
Enable and Start
sudo systemctl daemon-reload
sudo systemctl enable hello-api
sudo systemctl start hello-api
sudo systemctl status hello-apiExpected status output:
● hello-api.service - Hello API (Go)
Loaded: loaded (/etc/systemd/system/hello-api.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-04-16 12:10:00 UTC; 2s ago
Main PID: 2345 (hello-api)
Tasks: 5 (limit: 14236)
Memory: 6.2M
CPU: 20ms
CGroup: /system.slice/hello-api.service
└─2345 /opt/hello-api/hello-apiNotice the memory footprint: 6 MB for a full HTTP service. That is the Go advantage.
Tail logs:
sudo journalctl -u hello-api -fFor more on writing robust unit files, see our guide on configuring systemd services for production workloads.
Step 11: Hot Reload for Development (air / reflex)
Rebuilding and restarting after every code change is tedious. Two popular tools watch your source tree and rebuild automatically:
air (most popular)
Install it:
go install github.com/air-verse/air@latestIn your project directory, generate a default config:
air initThis creates .air.toml. Run air:
airEvery time you save a .go file, air recompiles and restarts your binary. Typical rebuild time for a small service is 200-400 ms.
reflex (generic file watcher)
go install github.com/cespare/reflex@latestRun any command on file changes:
reflex -r '\.go$' -s -- sh -c 'go run main.go'Both tools are development-only -- for production, always run a compiled binary under systemd.
Step 12: Popular Web Frameworks (Gin, Echo, Fiber)
The standard library's net/http is perfectly capable of running a production API. But three frameworks dominate when you want richer routing, middleware, and ergonomics.
Gin
Most widely used Go web framework. Mature, fast, great docs.
go get -u github.com/gin-gonic/ginpackage mainimport "github.com/gin-gonic/gin"
func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"message": "pong"}) }) r.Run(":8080") }
Echo
Similar ergonomics to Gin, slightly cleaner middleware API, built-in request validation.
go get -u github.com/labstack/echo/v4package mainimport ( "net/http" "github.com/labstack/echo/v4" )
func main() { e := echo.New() e.GET("/ping", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"message": "pong"}) }) e.Logger.Fatal(e.Start(":8080")) }
Fiber
Built on fasthttp instead of net/http. Benchmarks 2-3x faster than Gin on raw throughput, but less compatible with standard library middleware.
go get -u github.com/gofiber/fiber/v2package mainimport "github.com/gofiber/fiber/v2"
func main() { app := fiber.New() app.Get("/ping", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"message": "pong"}) }) app.Listen(":8080") }
For most VPS deployments, Gin is the safe default: large community, plenty of StackOverflow answers, full net/http compatibility so standard middleware works unchanged.
Step 13: Nginx Reverse Proxy for Your Go Service
Running your Go service on port 8080 with Nginx in front on port 443 is the conventional production pattern. Nginx handles TLS termination, static asset caching, gzip, and rate limiting, leaving your Go process to focus on application logic.
Install Nginx:
sudo apt install -y nginxCreate a site config:
sudo tee /etc/nginx/sites-available/hello-api > /dev/null <<'EOF' server { listen 80; server_name api.yourdomain.com; return 301 https://$host$request_uri; }server { listen 443 ssl http2; server_name api.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;
# Security headers add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options nosniff; add_header X-Frame-Options DENY;
client_max_body_size 10m;
location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
# Support long-lived connections (SSE, WebSockets) proxy_set_header Connection "upgrade"; proxy_set_header Upgrade $http_upgrade; proxy_read_timeout 600s; proxy_send_timeout 600s; } } EOF
sudo ln -s /etc/nginx/sites-available/hello-api /etc/nginx/sites-enabled/ sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d api.yourdomain.com sudo nginx -t && sudo systemctl reload nginx
For the full Nginx walkthrough including tuning, see how to install and configure Nginx on Ubuntu 24.04.
Upgrading Go to a Newer Version
Go releases a new minor version roughly every six months (February and August) and patch releases as needed. The upgrade procedure with the tarball method is simply a re-install:
# 1. Download the new version
NEW_VERSION="1.23.0"
cd /tmp
wget https://go.dev/dl/go${NEW_VERSION}.linux-amd64.tar.gz2. Replace the old install
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go${NEW_VERSION}.linux-amd64.tar.gz3. Verify
go versionYour GOPATH, installed tools, and project module caches are untouched. Rebuild your services with the new toolchain:
cd ~/projects/hello-api
go build -o hello-api .
sudo systemctl restart hello-apiFor zero-downtime upgrades across multiple services, build new binaries first, then restart services one by one behind a load balancer or Nginx upstream block.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
go: command not found after install | PATH not loaded in this shell | Run source /etc/profile.d/go.sh or log out and back in. For non-login shells, set PATH explicitly. |
go install succeeds but binary not found | $GOPATH/bin not on PATH | Confirm echo $PATH contains $HOME/go/bin. Re-source /etc/profile.d/go.sh. |
go: cannot find main module | Running go build outside a module | Run go mod init <module-path> in your project root to create go.mod. |
cgo: C compiler "gcc" not found | Package needs cgo but build tools are missing | sudo apt install -y build-essential, or set CGO_ENABLED=0 if the dependency is optional. |
listen tcp :80: bind: permission denied | Unprivileged user cannot bind to ports below 1024 | Use Nginx reverse proxy (Step 13), or add AmbientCapabilities=CAP_NET_BIND_SERVICE to your systemd unit. |
go.sum: missing entry | go.sum out of sync with go.mod | Run go mod tidy to regenerate. Commit both files. |
Service fails with status=203/EXEC | systemd cannot execute the binary | Check ExecStart path is absolute, binary exists, has execute bit (chmod +x), and is owned by a readable path. |
MemoryDenyWriteExecute crashes the service | Rare Go runtime interaction with writable+executable pages | Remove MemoryDenyWriteExecute=true from the unit or set it to false if your specific Go version or a dependency JITs code. |
| Huge binary size (>30 MB) | Debug symbols and path strings embedded | Rebuild with -ldflags="-s -w" -trimpath (Step 8). |
proxyconnect tcp: dial tcp: lookup proxy.golang.org: no such host | Outbound DNS blocked | Check /etc/resolv.conf, ensure port 443 outbound is open, or set GOPROXY=direct. |
Useful Diagnostic Commands
# Full Go environment dump
go envActive service logs
sudo journalctl -u hello-api -n 100 --no-pagerLive logs
sudo journalctl -u hello-api -fVerify systemd security options took effect
sudo systemd-analyze security hello-apiWhat ports is the service bound to?
sudo ss -tlnp | grep hello-apiMemory/CPU usage
sudo systemctl status hello-apisystemd-analyze security gives your service a 0-10 exposure score and lists every hardening directive you could still add. A well-configured Go service should score below 3.0.
FAQ
Should I install Go from apt or the tarball?
The tarball. Ubuntu 24.04's golang-go package is typically several minor versions behind upstream, and it does not update until the next LTS point release. The official tarball from go.dev/dl is the same artifact the Go team uses internally, updates are a simple re-extract, and you get new language features and security patches immediately. The only case for apt is if you specifically need the version Ubuntu ships for policy reasons (such as reproducing a distribution package).
Do I still need to set GOPATH with Go modules?
Yes, but its role has shrunk. In module mode (default since Go 1.16), GOPATH is no longer where your source code has to live -- your projects can be anywhere on disk. However, GOPATH is still used as the module download cache ($GOPATH/pkg/mod) and as the install target for go install ($GOPATH/bin). Setting it explicitly keeps tooling predictable, especially inside Docker images and CI pipelines where the default ~/go may not exist.
What is the difference between go build and go install?
go build compiles the current package and writes the output binary to the current directory (or to the path given by -o). It is the right command for CI pipelines, Dockerfiles, and release artifacts. go install compiles the current package and places the binary in $GOBIN (or $GOPATH/bin if GOBIN is unset). It is the right command for installing developer tools from the internet (go install github.com/tool@latest) and for setting up reusable utilities on your PATH. For deploying a service to production, always use go build so you control exactly where the binary lands.
How do I make my Go binary as small as possible?
Three flags handle 90% of the shrinkage:
CGO_ENABLED=0 go build -ldflags="-s -w" -trimpath -o app .That takes a typical Go service from ~12 MB down to ~6 MB. For extreme cases (constrained embedded devices, tiny container images), run upx on the result:
upx --best --lzma appThis compresses the binary further, often to 2-3 MB, at the cost of slower startup (a few tens of ms for decompression) and incompatibility with some security scanners. For most VPS deployments, UPX is overkill -- a 6 MB binary is already tiny.
How do I handle secrets in a Go systemd service?
Do not hardcode secrets in the binary and do not put them in the unit file plaintext. Two common patterns:
/etc/hello-api/env owned by root with mode 0600, containing KEY=value lines. Reference it from the unit: EnvironmentFile=/etc/hello-api/env. systemd loads these variables into the service's environment without exposing them to other processes.LoadCredential=db-password:/etc/hello-api/db-password to mount a secret file into the service's private credential directory at $CREDENTIALS_DIRECTORY/db-password. The file is only readable by the service and is automatically bind-mounted.Never use -ldflags -X to inject secrets -- strings on the binary reveals them trivially.
How do I profile a Go service in production?
Import net/http/pprof in your main package:
import _ "net/http/pprof"Then expose pprof on a separate, internal-only port:
go func() {
log.Println(http.ListenAndServe("127.0.0.1:6060", nil))
}()From the server, capture a 30-second CPU profile:
go tool pprof -http=:8081 http://127.0.0.1:6060/debug/pprof/profile?seconds=30Or heap memory:
go tool pprof -http=:8081 http://127.0.0.1:6060/debug/pprof/heapThis opens an interactive flame graph in your browser. Never expose pprof to the public internet -- bind it to 127.0.0.1 only.
Next Steps
Your Ubuntu 24.04 VPS is now a proper Go development and hosting environment. Here is what to build on next:
- Learn the standard library deeply -- Spend time with pkg.go.dev browsing
net/http,context,database/sql,encoding/json, andsync. Most production Go services never need more than the standard library plus a database driver. - Set up GitHub Actions for CI -- Automate
go vet,golangci-lint run,go test ./..., and a cross-compile matrix on every push. Build artifacts straight to GitHub Releases. - Add structured logging -- Replace
log.Printfwithlog/slog(standard library, Go 1.21+) for JSON-formatted logs that ship cleanly to Loki, ELK, or CloudWatch. - Wire up Prometheus metrics -- The
prometheus/client_golanglibrary adds a/metricsendpoint in a few lines. Pair with our monitoring stack install guide to get Grafana dashboards running on the same VPS. - Containerize with a scratch image --
FROM scratchwith your static binary produces Docker images of 6-10 MB. Compare to a typical Node image at 500 MB+. - Explore Go generics (1.18+) and the new
iterpackage (1.23+) -- these shorten a lot of common patterns. - Read the official Go documentation -- go.dev has the best language tutorials, effective Go guide, and module reference on the web.
Deploy Go-Ready VPS in 60 Seconds>
Our CloudCore Starter is the perfect match for Go server workloads: 2 vCPU, 4 GB RAM, 50 GB NVMe, Ubuntu 24.04 LTS, root SSH in under a minute. Go binaries are so lean you can easily run three or four production services on this one box.>
- Full root access -- install any Go version you want
- NVMe SSD for fast go mod download and build caches
- Unmetered bandwidth for API traffic
- IPv4 + IPv6, reverse DNS, snapshots
- EUR 7.99/month starting price>
Get Your Go VPS Now -- Deployed and SSH-ready in 60 seconds.