Prerequisites
Parallax’s headline claim is that target hosts need no Python. That is true, and it is worth being precise about what it costs: the work Ansible does with a Python interpreter on the target, Parallax does with a Go toolchain on the controller. Read this page before installing.
Controller requirements
A Go toolchain is required at runtime — not just to build
This is the single most surprising requirement, so it comes first.
Parallax modules are Go programs. They are not shipped inside the parallax binary
as precompiled payloads; they are compiled on the controller, on demand, for the
architecture of each target host, and the resulting static binary is copied over
and executed. pkg/module/compiler/compiler.go shells out to go build to do this.
The compile is roughly:
CGO_ENABLED=0 GOOS=<target-os> GOARCH=<target-arch> \
go build -tags netgo,osusergo -ldflags "-s -w -extldflags '-static'" -o <module> .
netgo and osusergo plus CGO_ENABLED=0 are what make the module a fully static
binary with no libc, DNS-resolver or NSS dependency on the target — that is the
mechanism behind “no Python, no runtime dependencies on the target”.
So on the controller you need:
| Requirement | Notes |
|---|---|
| Go 1.25 or newer | Must be on PATH as go. Match the version in go.mod. |
| Network access to the Go module proxy | go mod tidy runs when generating a module. See Offline operation. |
Writable GOCACHE | See Warm your build cache. |
| A writable temp directory | Each compile gets its own os.MkdirTemp scratch directory. |
If go is not on PATH, the first task that needs a module fails with:
parallax requires a Go toolchain on the controller to compile modules,
but 'go' was not found on PATH: exec: "go": executable file not found in $PATH
That check is a preflight in the compiler, so the failure is reported up front
rather than surfacing as an opaque exec error somewhere inside module execution.
Warm your build cache
Compilation happens per module, per target GOOS/GOARCH. A cold GOCACHE means
the standard library gets compiled for that target before your module does, which
turns a sub-second step into tens of seconds. A warm cache turns it back into
milliseconds. This is the difference between Parallax feeling instant and feeling
broken, so it is worth arranging deliberately:
Do not wipe
GOCACHEbetween runs. In CI, cache theGOCACHEdirectory (go env GOCACHE, usually~/.cache/go-build) along with the module cache.In a container image, pre-warm the cache at build time for the architectures you actually target:
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -tags netgo,osusergo std && \ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -tags netgo,osusergo stdParallax also caches the compiled module binaries themselves (
--cache, on by default, in~/.parallax/module-cache). That is a separate layer above the Go build cache, and both matter.
The module SDK source must be findable
Generated modules import the module SDK. Rather than fetching it from the network,
Parallax writes a replace directive into the generated go.mod pointing at a
local copy of the module SDK source. If it cannot find that source, no replace
directive is written and the build falls back to resolving the SDK over the network.
resolveSDKPath in module-sdk/functions.go looks in this order:
PARALLAX_MODULE_SDK_PATH— if set and it names an existing directory.runtime.Caller(0)— the directory of the SDK source file itself, used only if the recorded path is absolute and still exists on disk.- Known system paths, in order:
/usr/local/share/parallax/module-sdk/opt/parallax/module-sdk
If none of these resolve, resolveSDKPath returns an empty string and the replace
directive is silently omitted.
The -trimpath trap
Step 2 above does not work in a released binary.
runtime.Caller(0) returns the compile-time source path. Release binaries are built
with -trimpath (see .gitlab-ci.yml, the release:binaries job), which is exactly
what -trimpath removes: the recorded path becomes a module-relative path such as
go.digitalxero.dev/parallax-module-sdk/functions.go, not an absolute filesystem
path. The filepath.IsAbs guard then rejects it and resolution falls through.
The practical consequence:
For any binary you downloaded rather than built yourself,
PARALLAX_MODULE_SDK_PATHor one of the two known system paths is effectively mandatory.
It fails softly rather than loudly — you get a network fetch of the SDK, or a build
error about a missing module, rather than a clear “SDK not found” message. If module
compilation fails on a released binary and works from a go run checkout, this is
almost certainly why.
Set it explicitly:
export PARALLAX_MODULE_SDK_PATH=/usr/local/share/parallax/module-sdk
or install the SDK source to /usr/local/share/parallax/module-sdk and leave the
variable unset.
Offline operation
GenerateCode runs go mod tidy in the generated module directory unless
PARALLAX_OFFLINE is set. Setting it also swaps go fmt for a direct gofmt
invocation, avoiding the toolchain’s module resolution entirely:
export PARALLAX_OFFLINE=1
Use this only when the SDK path resolves locally and the module’s imports are all
standard library or already in the local module cache — PARALLAX_OFFLINE skips
dependency resolution, it does not vendor anything for you.
Reference setup
acceptance/docker/ubuntu.dockerfile is the known-good configuration and is worth
copying:
# Go toolchain from the official distribution, on PATH
ENV GOLANG_VERSION 1.25.12
ENV PATH /usr/local/go/bin:${PATH}
RUN wget -q https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz -O go.tgz \
&& tar -C /usr/local -xzf go.tgz \
&& rm go.tgz
# Module SDK source, at the path the resolver already knows about,
# and named explicitly so it does not depend on the resolver's fallbacks
COPY module-sdk /usr/local/share/parallax/module-sdk
ENV PARALLAX_MODULE_SDK_PATH=/usr/local/share/parallax/module-sdk
Note that it does both: it installs to a known system path and sets the environment variable. That belt-and-braces approach is the recommendation.
Target host requirements
Targets need no Python, no agent and no Parallax installation. They do need:
| Requirement | Why |
|---|---|
| An SSH server, reachable from the controller | Default transport. local, docker and winrm connections are also available. |
| A shell and a writable temp directory | Compiled module binaries are copied in, executed and removed. |
| An architecture the controller can cross-compile for | Any Go-supported GOOS/GOARCH pair. |
sudo / su / doas | Only if you use become. |
Module-specific target requirements
Most modules are self-contained static binaries and shell out to nothing. The exceptions are worth knowing about:
| Module | Needs on the target | Notes |
|---|---|---|
unarchive | tar for .tar, .tar.gz, .tar.bz2, .tar.xz; gunzip for a bare .gz | Zip archives are handled natively via archive/zip and need nothing. |
apt, yum, dnf, pip, … | The corresponding package manager | Expected. |
service, systemd | The corresponding init system | Expected. |
Note:
unarchive’s use of externaltarandgunzipreflects current behaviour. Native extraction is in progress; when it lands, this row drops to “nothing” and this note should be removed.
Quick check
On the controller:
go version # must print 1.25 or newer
go env GOCACHE # must be writable, and ideally already populated
echo "${PARALLAX_MODULE_SDK_PATH:-<unset>}"
ls /usr/local/share/parallax/module-sdk 2>/dev/null || echo "not installed system-wide"
If PARALLAX_MODULE_SDK_PATH is unset and no system path exists and you are
running a downloaded release binary, fix that before going further — see
The -trimpath trap.
On a target, from the controller:
parallax play -i inventory.ini --check ping.yml
The first run is the slow one; it populates both the Go build cache and the module binary cache.
Next steps
- Installation — getting the
parallaxbinary - Quick Start — running your first playbook