Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Golang Ubuntu
GUIDEInstall Guides

How to Install Go (Golang) on Ubuntu 24.04 VPS: Latest Version + systemd Setup

24 min read

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
  • Prerequisites
  • Step 1: Update System Packages
  • Step 2: Download the Latest Go Tarball
  • Step 3: Extract Go to /usr/local/go
  • Step 4: Configure PATH with /etc/profile.d/go.sh
  • Step 5: Verify the Installation
  • Step 6: Set Up GOPATH and go install
  • Step 7: Create Your First Module (go.mod / go.sum)
  • Step 8: Production Build Flags and Static Binaries
  • Step 9: Cross-Compile for Other Platforms
  • Step 10: Run Your Binary as a systemd Service
  • Step 11: Hot Reload for Development (air / reflex)
  • Step 12: Popular Web Frameworks (Gin, Echo, Fiber)
  • Step 13: Nginx Reverse Proxy for Your Go Service
  • Upgrading Go to a Newer Version
  • Troubleshooting
  • FAQ
  • Next Steps
  • 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 build produces one self-contained executable. No runtime to install, no virtualenv, no node_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 scratch Docker 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, and context cover most of what a backend service needs without external packages.
    • Excellent tooling -- go fmt, go vet, go test, go mod, and go install are built in. No separate build tools, test runners, or package managers.
    The real-world payoff is that self-hosted infrastructure written in Go -- Caddy, Prometheus, Grafana Loki, Vault, Consul, MinIO, SyncThing, Gitea -- all run comfortably on a 4 GB VPS, often several at once.

    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:

    bash
    ssh root@your-server-ip

    Step 1: Update System Packages

    Refresh the package index and apply any pending upgrades before installing new software:

    bash
    sudo apt update && sudo apt upgrade -y

    Install a few utilities we will use for downloading and verifying the tarball:

    bash
    sudo apt install -y wget curl git ca-certificates tar build-essential

    build-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:

    bash
    sudo reboot

    Step 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.

    bash
    cd /tmp
    GO_VERSION="1.22.5"
    wget https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz

    If your VPS is ARM-based (some cloud providers offer Ampere/Graviton nodes), download the ARM64 build instead:

    bash
    wget https://go.dev/dl/go${GO_VERSION}.linux-arm64.tar.gz

    Verify the Download (Recommended)

    Every release on go.dev/dl lists a SHA-256 checksum. Verify the tarball before extracting:

    bash
    sha256sum go${GO_VERSION}.linux-amd64.tar.gz

    Compare 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:

    bash
    sudo rm -rf /usr/local/go
    sudo tar -C /usr/local -xzf /tmp/go${GO_VERSION}.linux-amd64.tar.gz

    After extraction, the directory layout looks like this:

    text
    /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:

    bash
    rm /tmp/go${GO_VERSION}.linux-amd64.tar.gz

    Step 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.

    bash
    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.sh

    What 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 with go install. Defaults to ~/go if unset, but being explicit helps when scripting.
    • PATH=...:$GOROOT/bin:$GOPATH/bin -- Puts the go and gofmt commands on your shell path, plus any binaries you install with go install (they land in $GOPATH/bin).
    Load the new environment without logging out:

    bash
    source /etc/profile.d/go.sh

    For 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:

    bash
    go version

    Expected output:

    text
    go version go1.22.5 linux/amd64

    Inspect the full Go environment:

    bash
    go env

    Expected output (abbreviated):

    text
    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:

    bash
    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.go

    Expected output:

    text
    Go is working

    Go 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:

  • Module cache -- Downloaded dependencies are stored under $GOPATH/pkg/mod.
  • Global binary install -- Tools installed with go install github.com/user/tool@latest land in $GOPATH/bin.
  • Install a useful developer tool to verify the workflow:

    bash
    go install golang.org/x/tools/cmd/goimports@latest

    After completion, the binary is at $GOPATH/bin/goimports. Because we added $GOPATH/bin to PATH in the previous step, you can run it directly:

    bash
    goimports -h

    Other common tools worth installing:

    bash
    # Linter (catches bugs and style issues)
    go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

    Live reload for development (see Step 11)

    go install github.com/air-verse/air@latest

    Dependency graph visualizer

    go install github.com/loov/goda@latest

    Migrations tool

    go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

    These 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 constraints
    • go.sum -- cryptographic checksums of every downloaded dependency, committed to git for reproducible builds
    Create a simple HTTP server to see modules in action:

    bash
    mkdir -p ~/projects/hello-api && cd ~/projects/hello-api
    go mod init github.com/yourusername/hello-api

    This creates a go.mod file:

    text
    module github.com/yourusername/hello-api

    go 1.22

    Create main.go:

    bash
    cat > main.go <<'EOF'
    package main

    import ( "encoding/json" "log" "net/http" "os" "time" )

    type Response struct { Message string json:&quot;message&quot; Time time.Time json:&quot;time&quot; Host string json:&quot;host&quot; }

    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:

    bash
    go run main.go

    Expected output:

    text
    2026/04/16 12:00:00 Listening on :8080

    In another SSH session, hit the endpoint:

    bash
    curl http://localhost:8080/

    Expected output:

    json
    {"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:

    bash
    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.
    Check the result:

    bash
    ls -lh hello-api
    file hello-api

    Expected output:

    text
    -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:

    bash
    ./hello-api

    Embedding Version Information

    You can inject build metadata at compile time using -ldflags -X:

    bash
    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:

    go
    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:

    bash
    # 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:

    bash
    go tool dist list

    This 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:

    bash
    sudo useradd --system --no-create-home --shell /usr/sbin/nologin helloapi

    Place the Binary

    Copy the compiled binary to a stable path and set ownership:

    bash
    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-api

    Write the systemd Unit

    bash
    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-api

    Restart policy

    Restart=on-failure RestartSec=5s

    Security 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-api

    Resource 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 /tmp and /var/tmp.
    • ProtectSystem=strict -- Mounts /usr, /boot, /efi, and /etc read-only. Combined with ReadWritePaths=, 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-service set, 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 add AmbientCapabilities=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

    bash
    sudo systemctl daemon-reload
    sudo systemctl enable hello-api
    sudo systemctl start hello-api
    sudo systemctl status hello-api

    Expected status output:

    text
    ● 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-api

    Notice the memory footprint: 6 MB for a full HTTP service. That is the Go advantage.

    Tail logs:

    bash
    sudo journalctl -u hello-api -f

    For 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:

    bash
    go install github.com/air-verse/air@latest

    In your project directory, generate a default config:

    bash
    air init

    This creates .air.toml. Run air:

    bash
    air

    Every 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)

    bash
    go install github.com/cespare/reflex@latest

    Run any command on file changes:

    bash
    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.

    bash
    go get -u github.com/gin-gonic/gin
    go
    package main

    import "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.

    bash
    go get -u github.com/labstack/echo/v4
    go
    package main

    import ( "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.

    bash
    go get -u github.com/gofiber/fiber/v2
    go
    package main

    import "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:

    bash
    sudo apt install -y nginx

    Create a site config:

    bash
    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:

    bash
    # 1. Download the new version
    NEW_VERSION="1.23.0"
    cd /tmp
    wget https://go.dev/dl/go${NEW_VERSION}.linux-amd64.tar.gz

    2. Replace the old install

    sudo rm -rf /usr/local/go sudo tar -C /usr/local -xzf go${NEW_VERSION}.linux-amd64.tar.gz

    3. Verify

    go version

    Your GOPATH, installed tools, and project module caches are untouched. Rebuild your services with the new toolchain:

    bash
    cd ~/projects/hello-api
    go build -o hello-api .
    sudo systemctl restart hello-api

    For 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

    ProblemCauseSolution
    go: command not found after installPATH not loaded in this shellRun 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 PATHConfirm echo $PATH contains $HOME/go/bin. Re-source /etc/profile.d/go.sh.
    go: cannot find main moduleRunning go build outside a moduleRun go mod init <module-path> in your project root to create go.mod.
    cgo: C compiler "gcc" not foundPackage needs cgo but build tools are missingsudo apt install -y build-essential, or set CGO_ENABLED=0 if the dependency is optional.
    listen tcp :80: bind: permission deniedUnprivileged user cannot bind to ports below 1024Use Nginx reverse proxy (Step 13), or add AmbientCapabilities=CAP_NET_BIND_SERVICE to your systemd unit.
    go.sum: missing entrygo.sum out of sync with go.modRun go mod tidy to regenerate. Commit both files.
    Service fails with status=203/EXECsystemd cannot execute the binaryCheck ExecStart path is absolute, binary exists, has execute bit (chmod +x), and is owned by a readable path.
    MemoryDenyWriteExecute crashes the serviceRare Go runtime interaction with writable+executable pagesRemove 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 embeddedRebuild with -ldflags="-s -w" -trimpath (Step 8).
    proxyconnect tcp: dial tcp: lookup proxy.golang.org: no such hostOutbound DNS blockedCheck /etc/resolv.conf, ensure port 443 outbound is open, or set GOPROXY=direct.

    Useful Diagnostic Commands

    bash
    # Full Go environment dump
    go env

    Active service logs

    sudo journalctl -u hello-api -n 100 --no-pager

    Live logs

    sudo journalctl -u hello-api -f

    Verify systemd security options took effect

    sudo systemd-analyze security hello-api

    What ports is the service bound to?

    sudo ss -tlnp | grep hello-api

    Memory/CPU usage

    sudo systemctl status hello-api

    systemd-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:

    bash
    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:

    bash
    upx --best --lzma app

    This 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:

  • Environment file -- Create /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.
  • systemd credentials -- On systemd 250+, use 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:

    go
    import _ "net/http/pprof"

    Then expose pprof on a separate, internal-only port:

    go
    go func() {
        log.Println(http.ListenAndServe("127.0.0.1:6060", nil))
    }()

    From the server, capture a 30-second CPU profile:

    bash
    go tool pprof -http=:8081 http://127.0.0.1:6060/debug/pprof/profile?seconds=30

    Or heap memory:

    bash
    go tool pprof -http=:8081 http://127.0.0.1:6060/debug/pprof/heap

    This 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, and sync. 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.Printf with log/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_golang library adds a /metrics endpoint 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 scratch with 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 iter package (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.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket