Deployment guide

Golang

Overview

The Go 1.26 Development Stack is a ready-to-use Go workstation on Ubuntu 24.04 on Azure. You connect over SSH and use it to write, build, test, debug and cross-compile Go programs. This guide covers its deployment and use from the SecureAnyCloud Azure Marketplace image.

This image is a development workstation, not a server. 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, so no credential of any kind ships in the image. There is nothing to log in to and nothing to rotate.

The toolchain. The image installs Go 1.26.5, the current stable release, from the official go.dev archive and verifies it 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. The image deliberately does not install it, 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, not only in a login profile. 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 the volume independently of the operating system disk.

What you get

  • 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 that 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
  • sac-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/sac-golang/licenses/
  • Ubuntu 24.04 LTS, fully patched at build time, with unattended-upgrades left enabled so the machine keeps receiving security updates

Prerequisites

You need an active Azure subscription, an SSH public key, a virtual network with a subnet in your target region, and a subscription to the Go Development Stack listing on Azure Marketplace.

Add a Network Security Group rule that allows inbound TCP 22 from your administrative network. No other port needs to be open, because this image publishes no service.

Recommended VM 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.

1

Deploy from the Azure Portal

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

Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:golang:default:latest
az vm create \
  --resource-group my-rg \
  --name go-dev-01 \
  --image sac1647283583153:golang:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard
3

Check the first-boot result

On the first boot of every VM, golang-firstboot.service runs once. The one thing the build cannot decide in advance is who the developer is, because you choose the administrator account name when you create the machine.

First boot resolves that account and grants it access to the data volume. It records the resolved environment at /var/lib/sac/data/golang-workspace.env and writes its completion sentinel. It finishes in a second or two.

The 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/sac/data/golang-workspace.env

Expected output:

active
# sac 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
4

Connect and confirm the toolchain

Connect over SSH with 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 too:

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
5

Verify the whole workstation in one command

Before you trust a rented build machine with your own code, prove that it works. Run sac-go-selftest. It performs these checks:

  • It compiles a real concurrent program, runs it and checks the value it computes.
  • It runs the program's test suite, vets it and statically analyses it.
  • It confirms that every tool is present.
  • It cross-compiles to Arm and Windows and verifies that the resulting binaries really are foreign objects.
  • It fetches a module from the proxy, which proves that the module proxy, the checksum database and certificate trust all work.
sac-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/sac-golang/licenses sac-go-selftest: ALL CHECKS PASSED

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

6

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

In this example, the same calculation is rewritten with 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
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
10

Cross-compile

Go compiles for other operating systems and processor architectures from this one machine, with no additional toolchain to install. The image build already compiled the standard library for the common targets, 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.

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 report something, 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, so you can gate a pipeline on it.

A note on golangci-lint. It is not preinstalled, 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. To use golangci-lint on your own machine, install it yourself with go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest.

12

Debug with Delve

Delve is the Go debugger. It is interactive, so run it from your own terminal, not from 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, run 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.

13

Remote development from your editor

gopls, the official Go language server, is already installed. That makes this machine a remote development box, not 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 other configuration is needed.
  • JetBrains GoLand: use its remote development mode over SSH.

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

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 the compiler that built your binary is then not necessarily the one go version reported, and a build that would otherwise be offline gains a network dependency. Pinning to local keeps the image honest about the version it ships.

If a project you work on 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.

15

Review licences and attribution

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

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

Expected output:

sac 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. The image ships both, as it does for every golang.org/x/... module.

Trademarks. Go and the Go gopher are trademarks of Google LLC. SecureAnyCloud 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 a very small attack surface because it publishes no service. List the listening sockets to confirm:

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 the systemd-resolved 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 VM and expand the disk in the Azure Portal or with az disk update --size-gb. Then start the machine and 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, clear the caches. This is safe, and they simply refill:

$ go clean -cache $ go clean -modcache

Updating

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

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

The Go toolchain in this image is pinned at 1.26.5. SecureAnyCloud publishes refreshed images as new Go releases land. Moving to a new image is the supported upgrade path, because 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. It 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/sac-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, but it 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 to opt in to automatic toolchain downloads, or use a newer SecureAnyCloud 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/sac/data/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 sac-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

SecureAnyCloud provides 24/7 support for this image. Contact sales@secureanycloud.com with your Azure subscription ID and the output of sac-go-selftest.

Deploy on Azure

Launch Go 1.26.5 Development Stack on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure