G1
Developer Tools Azure

Go 1.26 Development Stack on Ubuntu 24.04 on Azure User Guide

| Product: Go 1.26.5 Development Stack on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and use of the Go 1.26 Development Stack on Ubuntu 24.04 on Azure from the cloudimg Azure Marketplace images.

This image is a development workstation, not a server. You connect to it over SSH and use it to write, build, test, debug and cross compile Go programs. It runs no network service of any kind: after first boot the only listening socket reachable from outside the machine is SSH on port 22. There is no web console, no dashboard and no login page, and consequently no credential of any kind ships in the image — there is nothing to log in to and nothing to rotate.

The toolchain is Go 1.26.5, the current stable release, installed from the official go.dev archive and verified against the SHA-256 that the Go project publishes for that file. Ubuntu 24.04's own golang-go package is Go 1.22, which is outside upstream's two release support window, so it is deliberately not installed and the build fails closed if it is present. The toolchain is also pinned: GOTOOLCHAIN is set to local, so a go.mod that names a newer release will not silently download and run a different compiler behind your back.

Where your work lives. A dedicated 32 GiB data volume is attached at /srv/godata and carries GOPATH, the module cache, the build cache and a workspace directory. These are configured as system wide defaults in Go's own go.env file rather than only in a login profile, so they apply everywhere — an interactive shell, ssh host 'go build', a systemd unit or a cron job all resolve the same paths. You can grow or snapshot that volume independently of the operating system disk.

What is included

  • Go 1.26.5 from the official go.dev archive, SHA-256 verified, at GOROOT=/usr/local/go with go and gofmt on the default PATH

  • gopls v0.23.0, the official Go language server, so VS Code Remote-SSH, Zed, Neovim or any other LSP editor gives you completion, navigation and refactoring the moment you connect

  • Delve v1.27.0 (dlv), the Go debugger

  • staticcheck v0.7.0 for static analysis beyond the toolchain's built in go vet

  • gotestsum v1.13.0 for readable test output and JUnit XML a build server can consume

  • A dedicated 32 GiB data volume at /srv/godata holding GOPATH, GOMODCACHE, GOCACHE and a workspace directory, mounted by UUID so it survives reboots and resizes

  • cloudimg-go-selftest, a single command that compiles and runs a real concurrent program, checks the answer it computes, runs a test suite, vets and statically analyses it, fetches a module from the proxy, and cross compiles to Arm and Windows

  • The complete third party licence bundle for the toolchain and every bundled tool, including the Go patent grant, under /usr/share/doc/cloudimg-golang/licenses/

  • Ubuntu 24.04 LTS, fully patched at build time with unattended-upgrades left enabled so the machine keeps receiving security updates

Prerequisites

  • Active Azure subscription, SSH public key, and a virtual network and subnet in your target region

  • A subscription to the Go Development Stack listing on Azure Marketplace

  • A Network Security Group rule allowing inbound TCP 22 from your administrative network. No other port needs to be open — this image publishes no service.

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is comfortable for editing, building and testing typical Go projects. Go's compiler parallelises across cores, so choose a larger Standard_D size if you compile a large codebase repeatedly or run heavy test suites.

Step 1: Deploy from the Azure Portal

  1. Open the Go Development Stack offer on the Azure Marketplace and choose Get It Now, then Create.
  2. Select your subscription, resource group and region, and choose the Standard_B2s size.
  3. Provide your SSH public key for the administrator account you name here. That account becomes the developer account on the machine.
  4. Allow inbound TCP 22 from your administrative network in the Network Security Group, then create the virtual machine.

Step 2: Deploy from the Azure CLI

az vm create \
  --resource-group my-rg \
  --name go-dev-01 \
  --image <golang-marketplace-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Step 3: First boot

On the first boot of every virtual machine, golang-firstboot.service runs once. The one thing that genuinely cannot be decided when the image is built is who the developer is — you choose the administrator account name when you create the machine — so first boot resolves that account, grants it access to the data volume, records the resolved environment at /var/lib/cloudimg/golang-workspace.env, and writes its completion sentinel. It finishes in a second or two.

That environment file contains no secrets. It is a plain manifest of paths and versions, readable by any user on the machine, because this image has nothing to authenticate against.

systemctl is-active golang-firstboot.service
cat /var/lib/cloudimg/golang-workspace.env

Expected output:

active
# cloudimg Go Development Stack — resolved environment for this VM.
# Written once by golang-firstboot.service. This file contains NO secrets: this image runs
# no network service, so there is nothing to authenticate against and nothing to rotate.
GOVERSION=go1.26.5
GOROOT=/usr/local/go
GOPATH=/srv/godata/gopath
GOMODCACHE=/srv/godata/pkg/mod
GOCACHE=/srv/godata/cache
GOWORKSPACE=/srv/godata/workspace
GODEV_USER=azureuser
GODEV_GROUP=godev
GODATA_MOUNTED=yes
FIRSTBOOT_COMPLETED=2026-07-26T07:36:22Z

Step 4: Connect and confirm the toolchain

Connect over SSH using the administrator account you chose:

ssh azureuser@<vm-ip>

Then confirm the toolchain and where it keeps its state:

go version
go env GOROOT GOPATH GOMODCACHE GOCACHE GOTOOLCHAIN

Expected output:

go version go1.26.5 linux/amd64
/usr/local/go
/srv/godata/gopath
/srv/godata/pkg/mod
/srv/godata/cache
local

Terminal showing go version reporting go1.26.5, the Go environment pointing GOPATH and both caches at the dedicated /srv/godata volume with GOTOOLCHAIN pinned to local, the first boot environment manifest, and the installed versions of gopls, Delve, staticcheck and gotestsum

The four developer tools are on the default PATH as well:

gopls version
dlv version
staticcheck --version
gotestsum --version

Expected output:

golang.org/x/tools/gopls v0.23.0
Delve Debugger
Version: 1.27.0
Build: $Id: 0782d3511ee64ac561a207d35b3403f49d3744a6 $
staticcheck 2026.1 (v0.7.0)
gotestsum version v1.13.0

Step 5: Verify the whole workstation in one command

Before you trust a rented build machine with your own code, prove it works. cloudimg-go-selftest compiles a real concurrent program, runs it and checks the value it computes, runs its test suite, vets and statically analyses it, confirms every tool is present, cross compiles to Arm and Windows and verifies the resulting binaries really are foreign objects, and fetches a module from the proxy to prove the module proxy, checksum database and certificate trust all work.

cloudimg-go-selftest

Expected output ends with:

== cross-compilation
      linux/arm64: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked
ok    linux/arm64 binary is a genuine linux arm64 object
      windows/amd64: PE32+ executable (console) x86-64, for MS Windows, 16 sections
ok    windows/amd64 binary is a genuine windows amd64 object

== module fetch (GOPROXY + checksum database + CA trust)
ok    fetched and verified golang.org/x/text from the module proxy

== shipped licences
ok    143 licence/notice files shipped under /usr/share/doc/cloudimg-golang/licenses

cloudimg-go-selftest: ALL CHECKS PASSED

Terminal showing cloudimg-go-selftest completing every stage: the pinned go1.26.5 toolchain, a successful build, the compiled program computing PRIMES=17984 SUM=1709600813, a passing test suite, clean go vet and staticcheck runs, all developer tools present, genuine arm64 and Windows cross compiled binaries, a verified module fetch, and 143 shipped licence files, ending in ALL CHECKS PASSED

Add --full to also cross compile to darwin/arm64 and linux/386, or --offline to skip the module proxy check on a machine with no outbound internet access.

Step 6: Where your code and caches live

findmnt -no SOURCE,TARGET,FSTYPE,SIZE /srv/godata
df -h /srv/godata
ls -l /srv/godata

Expected output:

/dev/sdc /srv/godata ext4   31.2G
Filesystem      Size  Used Avail Use% Mounted on
/dev/sdc         32G  1.6G   29G   6% /srv/godata
total 44
drwxrwsr-x+ 258 root      godev  4096 Jul 26 07:32 cache
drwxrwsr-x+   4 root      godev  4096 Jul 26 07:31 gopath
drwxrwx---+   2 root      root  16384 Jul 26 07:30 lost+found
drwxrwxr-x+   3 root      root   4096 Jul 26 07:31 pkg
drwxrwsr-x+   2 azureuser godev  4096 Jul 26 07:31 workspace

Put your projects in /srv/godata/workspace. The godev group owns the shared caches and your administrator account is a member of it, so you never need sudo to build. If you add more developer accounts to the machine, add them to godev as well:

$ sudo usermod -aG godev alice

Step 7: Build and run your first project

Everything below runs as your ordinary account — no elevated privileges are needed anywhere in this guide.

mkdir -p /srv/godata/workspace/hello
cd /srv/godata/workspace/hello
[ -f go.mod ] || go mod init example.com/hello
cat > main.go <<'EOF'
package main

import (
    "fmt"
    "runtime"
    "sync"
)

func main() {
    const n = 1000000
    workers := runtime.NumCPU()
    parts := make([]int64, workers)

    var wg sync.WaitGroup
    for w := 0; w < workers; w++ {
        wg.Add(1)
        go func(w int) {
            defer wg.Done()
            var sum int64
            for i := w + 1; i <= n; i += workers {
                sum += int64(i)
            }
            parts[w] = sum
        }(w)
    }
    wg.Wait()

    var total int64
    for _, p := range parts {
        total += p
    }
    fmt.Printf("workers=%d  sum(1..%d)=%d\n", workers, n, total)
}
EOF
go build -o hello .
./hello

Expected output on a two core machine:

workers=2  sum(1..1000000)=500000500000

Step 8: Add a dependency

Module downloads are cached on the data volume, so a dependency you fetch once is available to every later build without going back to the network. Here the same calculation is rewritten using errgroup, which propagates the first error out of a group of goroutines:

cd /srv/godata/workspace/hello
go get golang.org/x/sync@v0.22.0
cat > main.go <<'EOF'
package main

import (
    "context"
    "fmt"
    "runtime"

    "golang.org/x/sync/errgroup"
)

func main() {
    const n = 1000000
    workers := runtime.NumCPU()
    parts := make([]int64, workers)

    g, _ := errgroup.WithContext(context.Background())
    for w := 0; w < workers; w++ {
        w := w
        g.Go(func() error {
            var sum int64
            for i := w + 1; i <= n; i += workers {
                sum += int64(i)
            }
            parts[w] = sum
            return nil
        })
    }
    if err := g.Wait(); err != nil {
        panic(err)
    }

    var total int64
    for _, p := range parts {
        total += p
    }
    fmt.Printf("workers=%d  sum(1..%d)=%d\n", workers, n, total)
}
EOF
go build -o hello .
./hello
go list -m all

Expected output:

go: added golang.org/x/sync v0.22.0
workers=2  sum(1..1000000)=500000500000
example.com/hello
golang.org/x/sync v0.22.0

Step 9: Run the tests

go test is built into the toolchain. gotestsum wraps it with clearer output and can emit JUnit XML for a build server.

cd /srv/godata/workspace/hello
cat > main_test.go <<'EOF'
package main

import "testing"

func TestGaussFormula(t *testing.T) {
    const n = 1000000
    want := int64(n) * int64(n+1) / 2
    if want != 500000500000 {
        t.Fatalf("closed form gave %d, want 500000500000", want)
    }
}
EOF
go test ./...
gotestsum --format testname

Expected output:

ok      example.com/hello   0.003s
PASS TestGaussFormula (0.00s)
PASS .

DONE 1 tests in 0.003s

To produce a JUnit report for a build server:

cd /srv/godata/workspace/hello
gotestsum --junitfile /srv/godata/workspace/hello/report.xml --format testname
head -2 /srv/godata/workspace/hello/report.xml

Step 10: Cross compile

Go compiles for other operating systems and processor architectures from this one machine, with no additional toolchain to install. The standard library has already been compiled for the common targets when the image was built, so the first cross compile is fast rather than a surprise stall.

cd /srv/godata/workspace/hello
GOOS=linux   GOARCH=arm64 go build -o hello-linux-arm64 .
GOOS=windows GOARCH=amd64 go build -o hello-windows-amd64.exe .
GOOS=darwin  GOARCH=arm64 go build -o hello-darwin-arm64 .
file hello-linux-arm64 hello-windows-amd64.exe hello-darwin-arm64

Expected output:

hello-linux-arm64:       ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked
hello-windows-amd64.exe: PE32+ executable (console) x86-64, for MS Windows, 16 sections
hello-darwin-arm64:      Mach-O 64-bit arm64 executable, flags:<|DYLDLINK|PIE>

go tool dist list prints every target the toolchain supports.

Terminal showing a project being built and run, gotestsum reporting a passing test, three cross compiled binaries for linux arm64, windows amd64 and darwin arm64 with file confirming each is a genuine foreign object, and clean staticcheck and go vet runs

Step 11: Static analysis

go vet ships with the toolchain and catches a curated set of real mistakes. staticcheck goes considerably further.

cd /srv/godata/workspace/hello
go vet ./...
staticcheck ./...

Both are silent when they find nothing. When staticcheck does have something to say it names the file, the line and the check identifier, so you can look the rule up or silence it deliberately:

$ staticcheck ./...
lint_demo.go:8:2: should use 'return strings.Contains(strings.ToLower(s), "go") == true' instead of 'if strings.Contains(strings.ToLower(s), "go") == true { return true }; return false' (S1008)
lint_demo.go:8:5: should omit comparison to bool constant, can be simplified to strings.Contains(strings.ToLower(s), "go") (S1002)

staticcheck exits non-zero when it reports a finding, which makes it straightforward to gate a pipeline on.

A note on golangci-lint. It is not preinstalled here, and that is a deliberate licensing decision rather than an oversight: golangci-lint is distributed under the GNU General Public Licence v3.0, which would attach a corresponding source obligation to this image. staticcheck and go vet cover the same ground without it. If you want golangci-lint on your own machine, install it yourself with go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest.

Step 12: Debug with Delve

Delve is the Go debugger. It is interactive, so run it from your own terminal rather than a script:

$ cd /srv/godata/workspace/hello
$ dlv debug .
Type 'help' for list of commands.
(dlv) break main.main
Breakpoint 1 set at 0x4a1b40 for main.main() ./main.go:12
(dlv) continue
(dlv) next
(dlv) print workers
(dlv) goroutines
(dlv) quit

dlv test debugs a test binary instead, and dlv attach <pid> attaches to a process that is already running. To debug from an editor on your laptop, start a headless session on this machine and forward the port over your existing SSH connection:

$ dlv debug --headless --listen=127.0.0.1:2345 --api-version=2 .

Then from your laptop, ssh -L 2345:127.0.0.1:2345 azureuser@<vm-ip> and point your editor's Go debugger at localhost:2345. Binding Delve to loopback and reaching it through the SSH tunnel keeps the debug port off the public internet — never expose port 2345 in a Network Security Group.

Step 13: Remote development from your editor

gopls, the official Go language server, is already installed, which is what makes this machine usable as a remote development box rather than just a build box.

  • VS Code: install the Remote-SSH and Go extensions, connect to azureuser@<vm-ip>, and open /srv/godata/workspace. The Go extension finds gopls on the PATH and gives you completion, go to definition, rename and inline diagnostics against the code on this machine.
  • Zed, Neovim, Helix or any LSP client: point it at gopls; no configuration beyond that is needed.
  • JetBrains GoLand: use its remote development mode over SSH.

Because the language server, the compiler and the caches all live on this machine, your laptop only carries the editor. Nothing needs to be installed locally and nothing needs to be synchronised.

Step 14: The pinned toolchain

GOTOOLCHAIN is set to local in Go's own defaults file, /usr/local/go/go.env. Upstream's default is auto, which silently downloads and runs a different Go release whenever a go.mod declares a newer one. That is convenient, but it means the compiler that actually built your binary is not necessarily the one go version reported, and it adds a network dependency to a build that would otherwise be offline. Pinning to local keeps this image honest about the version it ships.

If a project you work on genuinely requires a newer release and you want upstream's behaviour, opt in per user:

$ go env -w GOTOOLCHAIN=auto

That writes to your own ~/.config/go/env and leaves the system default alone. Set it back with go env -u GOTOOLCHAIN.

Step 15: Licences and attribution

Every upstream licence, notice, patent grant and authors file shipped by the toolchain and by every bundled tool, including their transitive dependencies, is copied verbatim into the image and indexed by a single notice file.

head -18 /usr/share/doc/cloudimg-golang/licenses/NOTICE
find /usr/share/doc/cloudimg-golang/licenses -type f | wc -l
head -3 /usr/share/doc/cloudimg-golang/licenses/go-1.26.5/PATENTS

Expected output:

cloudimg Go Development Stack — third-party notices
===================================================
...
  Component                          Version      Licence         Notices in
  Go programming language toolchain  1.26.5       BSD-3-Clause    go-1.26.5/LICENSE
                                                  + patent grant  go-1.26.5/PATENTS
  gopls (golang.org/x/tools)         v0.23.0      BSD-3-Clause    modules/golang.org/x/tools*/
  Delve debugger (dlv)               v1.27.0      MIT             modules/github.com/go-delve/*/
  staticcheck (honnef.co/go/tools)   v0.7.0       MIT             modules/honnef.co/go/tools*/
  gotestsum                          v1.13.0      Apache-2.0      modules/gotest.tools/*/
143
Additional IP Rights Grant (Patents)

"This implementation" means the copyrightable works distributed by

The Go project distributes an additional patent grant alongside its BSD-3-Clause licence; both are shipped, as they are for every golang.org/x/... module.

Trademarks. Go and the Go gopher are trademarks of Google LLC. cloudimg is not affiliated with, sponsored by, or endorsed by Google LLC. The Go name is used here only to identify the software this image contains.

Terminal showing the shipped third party notice listing every bundled component with its licence, a count of 143 licence files in the image, the opening lines of the Go patent grant, and ss confirming that the only listening sockets are SSH on port 22 and the loopback DNS stub resolver

Security posture

This image has an unusually small attack surface for a marketplace product, because it publishes nothing.

ss -Hltn

Expected output:

LISTEN 0      4096         0.0.0.0:22 0.0.0.0:*
LISTEN 0      4096      127.0.0.54:53 0.0.0.0:*
LISTEN 0      4096   127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0      4096            [::]:22    [::]:*

Port 22 is SSH. The two entries on port 53 are systemd-resolved's stub resolver bound to loopback addresses on every stock Ubuntu image; they are not reachable from the network.

Practical guidance:

  • Keep the Network Security Group closed to everything but TCP 22 from your administrative network. Nothing else on this machine needs an inbound rule.
  • Use SSH keys, not passwords. The image is created with key authentication.
  • Tunnel, do not expose. If you run a development server, Delve or gopls over a port, bind it to 127.0.0.1 and reach it with ssh -L. Never open a development port in the Network Security Group.
  • The image ships no credentials. There is no default password, API key or token anywhere in it, because there is no service to authenticate against.
  • Security updates continue. unattended-upgrades is enabled, so the machine keeps applying Ubuntu security updates after deployment.

Resizing and backing up the data volume

/srv/godata is an ordinary Azure managed disk mounted by UUID. To grow it, stop the virtual machine, expand the disk in the Azure Portal or with az disk update --size-gb, start the machine and then grow the filesystem in place:

$ sudo resize2fs /dev/disk/azure/scsi1/lun0

Snapshot the same disk to back up your workspace and caches. The operating system disk holds only the toolchain and tools, which the image can always reproduce, so the data volume is the disk worth protecting.

To reclaim space, the caches are safe to clear and will simply refill:

$ go clean -cache
$ go clean -modcache

Updating

Ubuntu security updates apply automatically. To update the packages by hand:

$ sudo apt-get update && sudo apt-get -y dist-upgrade

The Go toolchain in this image is pinned at 1.26.5. cloudimg publishes refreshed images as new Go releases land, and moving to a new image is the supported upgrade path — it keeps the checksum verification, the licence bundle and the selftest in step with the toolchain. If you need a second toolchain alongside the pinned one in the meantime, use the Go project's own mechanism, which installs into your own account and leaves the system toolchain untouched:

$ go install golang.org/dl/go1.27@latest
$ ~/go/bin/go1.27 download

Troubleshooting

go is not found. The toolchain is at /usr/local/go/bin/go with a symlink at /usr/local/bin/go. Confirm with command -v go. If PATH has been altered, /etc/profile.d/cloudimg-go.sh restores it on a fresh login.

Permission denied writing to the cache. Your account must be in the godev group. Check with id, add it with sudo usermod -aG godev $USER, then start a new SSH session so the new group membership applies.

/srv/godata is missing or empty. Confirm the data disk is attached and mounted with findmnt /srv/godata. The fstab entry uses nofail, so a machine whose data disk is detached still boots — it just has no workspace. Re-attach the disk and run sudo mount /srv/godata.

A build wants a newer Go than 1.26.5. That is GOTOOLCHAIN=local doing its job. See Step 14 for how to opt in to automatic toolchain downloads, or use a newer cloudimg image.

First boot did not complete. Inspect it with systemctl status golang-firstboot.service and journalctl -u golang-firstboot.service. The service is a one shot that writes /var/lib/cloudimg/golang-firstboot.done when it finishes; it is safe to re-run with sudo systemctl restart golang-firstboot.service.

Something is wrong and you want a single verdict. Run cloudimg-go-selftest. It exercises the compiler, the runtime, the test framework, the analysers, the module proxy and cross compilation, and tells you exactly which stage failed.

Support

cloudimg provides 24/7 support for this image. Contact support@cloudimg.co.uk with your Azure subscription ID and the output of cloudimg-go-selftest.