Go 1.26 Development Stack on Ubuntu 24.04 on Azure User Guide
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/gowithgoandgofmton the defaultPATH -
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/godataholdingGOPATH,GOMODCACHE,GOCACHEand 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-upgradesleft 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
- Open the Go Development Stack offer on the Azure Marketplace and choose Get It Now, then Create.
- Select your subscription, resource group and region, and choose the
Standard_B2ssize. - Provide your SSH public key for the administrator account you name here. That account becomes the developer account on the machine.
- 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

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

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.

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.
staticcheckandgo vetcover the same ground without it. If you want golangci-lint on your own machine, install it yourself withgo 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 findsgoplson thePATHand 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.

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
goplsover a port, bind it to127.0.0.1and reach it withssh -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-upgradesis 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.