Goss: Fast, YAML-Based Server Validation for Infrastructure Testing
Goss is a Go-based server validation tool that runs tests in milliseconds. Where Serverspec and InSpec connect via SSH and execute Ruby, Goss runs as a single binary directly on the target system. Tests are defined in YAML, results are immediate.
The key differentiator: goss validate runs your entire spec in parallel. A 50-check spec that takes 2 minutes in Serverspec (sequential SSH calls) takes under a second in Goss.
Installing Goss
# Linux x86_64
curl -L https://github.com/goss-org/goss/releases/latest/download/goss-linux-amd64 \
-o /usr/local/bin/goss
chmod +x /usr/local/bin/goss
# macOS (for dgoss/Docker testing)
brew install goss
# Verify
goss --versionFor CI, pin a specific version:
GOSS_VERSION=v0.4.4
curl -L "https://github.com/goss-org/goss/releases/download/${GOSS_VERSION}/goss-linux-amd64" \
-o /usr/local/bin/gossWriting a goss.yaml
The spec file (goss.yaml) is straightforward YAML:
# goss.yaml
package:
nginx:
installed: true
versions:
- "1.24.*"
curl:
installed: true
openssl:
installed: true
service:
nginx:
enabled: true
running: true
sshd:
enabled: true
running: true
port:
tcp:80:
listening: true
ip:
- "0.0.0.0"
tcp:443:
listening: true
tcp6:22:
listening: false # SSH should not listen on IPv6
file:
/etc/nginx/nginx.conf:
exists: true
mode: "0644"
owner: root
group: root
contains:
- "worker_processes auto"
- "gzip on"
/var/log/nginx:
exists: true
filetype: directory
owner: nginx
user:
nginx:
exists: true
uid: 101
gid: 101
home: /var/cache/nginx
shell: /usr/sbin/nologin
groups:
- nginx
group:
nginx:
exists: true
gid: 101
process:
nginx:
running: true
command:
"nginx -t":
exit-status: 0
stderr:
- "syntax is ok"
- "test is successful"
"systemctl is-active nginx":
exit-status: 0
stdout:
- active
http:
http://localhost:
status: 200
timeout: 5000
body:
- "Welcome to nginx"
http://localhost/healthz:
status: 200
allow-insecure: falseRun it:
goss validate
# or
goss validate --format documentation
# or with JUnit output for CI
goss validate --format junit > test-results.xmlAuto-Adding Resources
Instead of writing YAML by hand, autogenerate specs from the current system state:
# Add a package check
goss add package nginx
# Add a service check
goss add service nginx
# Add a port check
goss add port 80
# Add a file check
goss add file /etc/nginx/nginx.conf
# Add a command check
goss add command "nginx -v"
# Add everything for a process
goss autoadd nginxgoss autoadd nginx detects the nginx process and automatically adds: the package, service, user, group, and process checks. This is the fastest way to capture current state as a spec.
After autopiloting, review goss.yaml and tighten assertions that are too permissive (like removing specific PIDs and keeping only process running checks).
Output Formats
# Human-readable (default)
goss validate
# Tap format (for tap-compliant tools)
goss validate --format tap
# JSON
goss validate --format json
# JUnit XML (for CI test reporting)
goss validate --format junit --output-file results.xml
# Documentation (verbose)
goss validate --format documentation
# Nagios-compatible exit codes
goss validate --format nagiosgossfile: Composable Specs
Break large specs into files by role:
# goss.yaml (main entry point)
gossfile:
./specs/base.yaml: {}
./specs/webserver.yaml: {}
./specs/tls.yaml: {}
# Override or extend
package:
custom-package:
installed: true# specs/base.yaml
package:
curl:
installed: true
vim:
installed: true
ca-certificates:
installed: true
service:
sshd:
enabled: true
running: true
kernel-param:
vm.swappiness:
value: "10"
net.ipv4.ip_forward:
value: "0"# specs/webserver.yaml
package:
nginx:
installed: true
service:
nginx:
enabled: true
running: true
port:
tcp:80:
listening: true
tcp:443:
listening: true
http:
http://localhost:
status: 200Now different server roles compose specs:
# goss-database.yaml
gossfile:
./specs/base.yaml: {}
./specs/postgresql.yaml: {}# goss-webserver.yaml
gossfile:
./specs/base.yaml: {}
./specs/webserver.yaml: {}
./specs/tls.yaml: {}goss -g goss-webserver.yaml validate
goss -g goss-database.yaml validateVariables and Templates
Goss supports Go templates with a variables file:
# vars.yaml
port: 8080
version: "1.24"
log_dir: /var/log/nginx# goss.yaml (with templates)
package:
nginx:
installed: true
versions:
- "{{ .Vars.version }}.*"
port:
tcp:{{ .Vars.port }}:
listening: true
file:
{{ .Vars.log_dir }}:
exists: true
filetype: directorygoss --vars vars.yaml validate
# or with inline vars
goss --vars-inline '{"port": 9090}' validateDifferent environments use different vars files:
# Production
goss --vars vars/prod.yaml validate
# Staging
goss --vars vars/staging.yaml validatedgoss: Testing Docker Images
dgoss is a wrapper that starts a Docker container, copies in goss.yaml, runs goss validate inside it, then removes the container:
# Install dgoss
curl -L https://github.com/goss-org/goss/releases/latest/download/dgoss \
-o /usr/local/bin/dgoss
chmod +x /usr/local/bin/dgossTest a Docker image:
# Run goss inside the container
dgoss run nginx:1.24 -p 80:80
# Edit mode — start container and generate goss.yaml interactively
dgoss edit nginx:1.24
# With environment variables
dgoss run -e NGINX_PORT=8080 myapp:latestWrite your spec:
# goss.yaml for Docker nginx
package:
nginx:
installed: true
service:
nginx:
enabled: true
running: true
port:
tcp:80:
listening: true
http:
http://localhost:
status: 200
timeout: 3000Run in CI to validate your Docker image before pushing:
# GitHub Actions
- name: Validate Docker image
run: |
docker build -t myapp:test .
dgoss run myapp:testcgoss: Cloud Init Testing
cgoss extends dgoss patterns for cloud-init testing. For VMs, the common pattern is:
- Packer builds an image and runs Goss as a provisioner
- Goss validates the image before it's published
{
"provisioners": [
{
"type": "shell",
"inline": [
"curl -L https://github.com/goss-org/goss/releases/latest/download/goss-linux-amd64 -o /usr/local/bin/goss",
"chmod +x /usr/local/bin/goss"
]
},
{
"type": "file",
"source": "goss.yaml",
"destination": "/tmp/goss.yaml"
},
{
"type": "shell",
"inline": [
"cd /tmp && goss validate --format documentation"
]
}
]
}goss serve: Expose as Health Check Endpoint
Goss can run as an HTTP server that exposes your spec as a health endpoint:
# Start health endpoint on port 8080
goss serve --listen-addr :8080 --format json
# Or as a daemon
goss serve --listen-addr :8080 &
# Check from anywhere
curl http://server:8080/healthzResponse:
{
"results": [...],
"summary": {
"failed-count": 0,
"test-count": 12
},
"summary-line": "Count: 12, Failed: 0, Duration: 0.042s"
}This makes Goss work as a Kubernetes readiness/liveness probe:
# Kubernetes deployment
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 30Start goss serve as a sidecar or in the app container's init process.
CI Integration
GitHub Actions
name: Goss Infrastructure Tests
on:
push:
branches: [main]
pull_request:
jobs:
goss:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Goss
run: |
curl -L https://github.com/goss-org/goss/releases/download/v0.4.4/goss-linux-amd64 \
-o /usr/local/bin/goss
chmod +x /usr/local/bin/goss
- name: Install dgoss
run: |
curl -L https://github.com/goss-org/goss/releases/download/v0.4.4/dgoss \
-o /usr/local/bin/dgoss
chmod +x /usr/local/bin/dgoss
- name: Build image
run: docker build -t app:test .
- name: Validate with Goss
run: dgoss run app:test
- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: goss-results
path: goss-results.jsonIntegration with Ansible
Run Goss as the final step in an Ansible playbook:
# roles/nginx/tasks/main.yml
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
- name: Start nginx
ansible.builtin.service:
name: nginx
state: started
enabled: yes
- name: Copy Goss spec
ansible.builtin.copy:
src: goss.yaml
dest: /tmp/goss.yaml
- name: Install Goss
ansible.builtin.get_url:
url: https://github.com/goss-org/goss/releases/latest/download/goss-linux-amd64
dest: /usr/local/bin/goss
mode: '0755'
- name: Run Goss validation
ansible.builtin.command:
cmd: goss -g /tmp/goss.yaml validate --format documentation
register: goss_result
changed_when: false
- name: Show results
ansible.builtin.debug:
msg: "{{ goss_result.stdout_lines }}"Retry Logic for Eventually-Consistent Checks
For services that take time to start, use retry:
goss validate --retry-timeout 30s --sleep 1sOr in goss.yaml for specific checks:
http:
http://localhost:
status: 200
timeout: 5000
# Retry happens at the CLI levelUseful after reboots, slow service starts, or when testing cloud-init provisioning where the service takes 10-20 seconds to fully initialize.
Comparing Goss vs Serverspec vs InSpec
| Feature | Goss | Serverspec | InSpec |
|---|---|---|---|
| Speed | Very fast (parallel) | Slow (sequential SSH) | Moderate |
| Language | YAML | Ruby/RSpec | Ruby/RSpec |
| Remote testing | Copy binary + SSH | Native SSH | Native SSH + plugins |
| Docker support | dgoss wrapper | Test Kitchen | Test Kitchen |
| Compliance profiles | No | No | Yes |
| Auto-generation | goss autoadd |
No | No |
| Binary deps | Single binary | Ruby + gems | Ruby + gems |
| Readability | High (YAML) | High (RSpec DSL) | High (RSpec DSL) |
Use Goss when: you want maximum speed, minimal dependencies, or need a health check endpoint. Use Serverspec when: you're already in a Ruby/Test Kitchen ecosystem and want RSpec patterns. Use InSpec when: you need compliance profiles, waivers, or Chef Automate integration.
Summary
Goss combines three things most other tools separate: fast local validation, autogeneration from live state, and a built-in health endpoint. The YAML format is readable enough that non-developers can review and understand specs. For Docker image validation, dgoss is the fastest way to add confidence that your images are correctly configured before they hit production.
Start with goss autoadd on a configured server to capture baseline state, prune the generated spec to meaningful assertions, and integrate goss validate into every Ansible playbook run or Docker build. The cost is a single Go binary and a YAML file — the payoff is a continuously validated infrastructure spec.