grounds.yaml lives at the root of your project and tells forge what you’re pushing. It’s read by both the Gradle plugin (to upload it) and forge (to build + deploy it).
Minimal example
grounds.yaml
Full example
grounds.yaml
plugins below.
Fields
name (required)
The logical app name. Used as:
- The Kubernetes
Deploymentname (so it has to match[a-z0-9-]{1,40}). - The hostname prefix for staging previews (
<name>-pr<id>.mc.grnds.iofor Minecraft workloads,…dev.grnds.ioforservice). - The display name in the portal.
#grounds-platform if you need to migrate state.
type (required)
What kind of workload this is. One of:
type drives quotas, resource defaults, port mappings, and which container our renderer wires up.
baseImage (required)
The catalog source key for the runtime image your JAR is layered onto. Must be one of the base image sources allowed for your type:
Forge resolves the source key to the current stable version in the base image catalog when it builds your push. Do not put an OCI tag or digest in
grounds.yaml; promote a new catalog version instead.
The valid source keys come from the platform catalog. The table above lists the standard sources, but self-hosted or internal environments may expose additional sources.
jar (optional, default build/libs/*.jar)
A glob pointing at the JAR to upload, relative to the project root. The Gradle plugin auto-detects shadowJar or jar and sets this for you, so you usually don’t need to write it.
When to set it explicitly:
- You produce multiple JARs and want to ensure the right one is picked.
- You build with something other than
shadowJar/jar.
plugins (optional)
A list of 2–10 plugin sources bundled into a single Paper / Velocity / gamemode server. Useful when you want to test plugin interaction (economy + chat + teams) on one running instance instead of pushing each plugin as its own server.
For the internal workflow that uses these fields with local overrides, see
Test multi-plugin stacks.
A working end-to-end example lives at groundsgg/multi-plugin-sample — two trivial Paper plugins bundled via :gradle-project refs. Clone, run ./gradlew groundsPush, see both plugins boot on one server.
For team workflows, prefer structured entries with id, optional variant,
and source:
source is the portable default used by plain grounds push. id and
variant let the CLI match the entry to a local
workspace override when you pass --local or
--with-local.
Each source is one of three source kinds, distinguished by prefix:
The Gradle plugin packs all resolved JARs into a
tar.gz and forge unpacks them into /app/plugins/ in manifest order (a numeric prefix preserves load order, since Paper’s pluginsFolder scan is filesystem-ordered).
GitHub source rules
- Owner allowlist — only
groundsgg/*is accepted. Other owners are rejected at parse time on the client and re-validated server-side at push admission. Hard security boundary, not configurable. - Tag pinning — the tag must be a SemVer-shaped tag (
v1.4.2, optional pre-release / build metadata) or a 40-char commit SHA.latest, branch names, and partial SHAs are intentionally rejected so bundles stay reproducible. - Asset filter — the asset must end in
.jar. No source tarballs, no zips. If you don’t name an asset, the first.jaron the release is used. - SHA256 verification — when a release ships a
<asset>.sha256sidecar,groundsPushfetches it and verifies before caching. Releases without one are accepted unchanged. - Auth — public repos work without auth; private repos read
GITHUB_TOKENfrom the environment. A 404 with no token in scope surfaces a hint about it.
<owner>-<repo>-<tag>-<asset>.jar. Because tags are immutable pins, a cache hit means the same bytes as before — no re-download per push.
Constraints
- Mutually exclusive with
jar:— pick one shape per manifest. - Forbidden for
type: service(services have a single-jarENTRYPOINT; multi-plugin has no meaningful semantics there). - 2 minimum (single-plugin uses
jar:), 10 maximum, 100 MB total upload after resolution. - For Gradle project refs the subproject must apply a Jar-producing plugin (
java,com.gradleup.shadow, etc.).groundsPushdiscoversshadowJarfirst, then falls back tojar. - Absolute local paths still work but log a “non-portable manifest” warning, because CI won’t see your machine’s filesystem.
target (optional)
A default target if neither the CLI flag nor the Gradle extension overrides it. Order of precedence:
- CLI flag:
grounds push --target=staging - Gradle extension:
groundsPush { target.set("staging") } grounds.yaml: target: ...- Fallback:
dev
resources (optional)
CPU/memory request hints. They’re suggestions — forge clamps them to the per-type minimums (e.g., plugin-paper is at least 250m CPU and 1Gi memory) and to per-project quota maxima. If your value falls inside the legal range, it’s used as-is.
- We always set
requests == limits. Pods are deterministic and don’t burst. - Below the minimum: silently raised to the minimum.
- Above the project quota: push fails at admission with a quota error.
agones (optional, gamemode only)
Agones-specific knobs. Only meaningful for type: gamemode, where the renderer wraps your workload in an Agones Fleet. Other types ignore the block.
The Agones SDK sidecar is injected by the cluster’s mutating webhook on every Fleet-managed pod, so
plugin-agones-paper (and the Minestom/Velocity equivalents) can talk to localhost:9358 and report Allocated/Ready state from player events.
Other fields
grounds.yaml is strict. Unknown top-level fields are rejected at parse time so typos don’t silently get ignored. If you need something not covered here, open a #grounds-platform ticket — the parser only knows what’s listed above.
Validation
The Gradle plugin parsesgrounds.yaml before upload. Common errors:
Editor support
There’s no JSON Schema forgrounds.yaml yet. On the roadmap; for now use this page as the reference. The Gradle plugin’s parse error messages name the offending field and line number.