Shardlyn Workload Specification
Overview
The Shardlyn Workload Specification defines how containerized workloads (including game servers) are configured. It's a declarative format that specifies everything needed to run a workload: container image, environment variables, ports, volumes, and resource requirements.
Many examples below use game servers because they exercise advanced patterns (ports, persistent volumes, install scripts), but the same schema applies to web apps, APIs, databases, and DevOps tooling.
Related
- Workloads Guide — Creating and managing workloads
- Instances Guide — Deploying workload instances
- API Reference — Workload REST API endpoints
- Architecture — How the reconciler applies workload specs
Specification Format
apiVersion: shardlyn/v1
kind: Workload
metadata:
name: string # Required: Unique identifier
labels: # Optional: Key-value pairs for filtering
key: value
annotations: # Optional: Non-identifying metadata
key: value
spec:
image: string # Required: Container image
command: [string] # Optional: Override entrypoint
args: [string] # Optional: Override cmd
workingDir: string # Optional: Working directory
user: string # Optional: User to run as
env: # Optional: Environment variables
KEY: value
ports: # Optional: Exposed ports
- name: string
containerPort: int
hostPort: int # Optional: Fixed host port
protocol: TCP|UDP
volumes: # Optional: Storage volumes
- name: string
mountPath: string
sizeGb: int
readOnly: bool
resources: # Optional: Resource limits
requests:
cpu: string
memory: string
limits:
cpu: string
memory: string
restartPolicy: string # Optional: always|on-failure|never
stopTimeout: int # Optional: seconds before force kill (default: 30)
healthcheck: # Optional: Health monitoring
command: [string]
intervalSeconds: int
timeoutSeconds: int
retries: int
startPeriodSeconds: intField Reference
metadata
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique name for the workload. Used in logs and UI. |
labels | map[string]string | No | Key-value pairs for categorization and filtering. |
annotations | map[string]string | No | Arbitrary metadata not used for filtering. |
Example:
metadata:
name: minecraft-survival
labels:
game: minecraft
version: "1.20.4"
type: survival
annotations:
description: "Main survival server"
owner: "[email protected]"spec.image
Container image to run. Supports any Docker-compatible image reference.
Examples:
spec:
image: itzg/minecraft-server:latest
image: itzg/minecraft-server:java17
image: ghcr.io/pterodactyl/yolks:java_17
image: registry.example.com/custom-image:v1.2.3spec.command and spec.args
Override the container's entrypoint and/or command.
commandreplaces the image's ENTRYPOINTargsreplaces the image's CMD
Example:
spec:
image: openjdk:17
command: ["/bin/sh", "-c"]
args: ["java -Xmx4G -jar server.jar nogui"]spec.env
Environment variables passed to the container.
Example:
spec:
env:
EULA: "TRUE"
TYPE: "VANILLA"
VERSION: "1.20.4"
MEMORY: "4G"
MAX_PLAYERS: "50"
MOTD: "Welcome to Shardlyn Server!"
ENABLE_RCON: "true"
RCON_PASSWORD: "supersecret"spec.ports
Network ports exposed by the container.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Identifier for the port (e.g., "game", "rcon") |
containerPort | int | Yes | Port inside the container |
hostPort | int | No | Specific host port. If omitted, auto-assigned. |
protocol | string | No | TCP or UDP. Default: TCP |
Example:
spec:
ports:
- name: game
containerPort: 25565
protocol: TCP
- name: rcon
containerPort: 25575
hostPort: 25575
protocol: TCP
- name: query
containerPort: 25565
protocol: UDPspec.volumes
Persistent storage volumes mounted into the container.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Volume identifier |
mountPath | string | Yes | Path inside container |
sizeGb | int | No | Requested size in GB |
readOnly | bool | No | Mount as read-only |
Example:
spec:
volumes:
- name: world-data
mountPath: /data
sizeGb: 50
- name: plugins
mountPath: /plugins
sizeGb: 5
- name: config
mountPath: /config
readOnly: falseNote: In the MVP, volumes are bind-mounted to /var/lib/shardlyn/volumes/{instance_id}/{volume_name} on the host.
spec.resources
CPU and memory resource requirements.
| Field | Type | Description |
|---|---|---|
requests.cpu | string | Minimum CPU needed |
requests.memory | string | Minimum memory needed |
limits.cpu | string | Maximum CPU allowed |
limits.memory | string | Maximum memory allowed |
CPU formats:
"1"= 1 CPU core"0.5"= Half a core"500m"= 500 millicores (0.5 cores)"2000m"= 2000 millicores (2 cores)
Memory formats:
"512Mi"= 512 mebibytes"1Gi"= 1 gibibyte"4Gi"= 4 gibibytes"8192Mi"= 8 gibibytes
Example:
spec:
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
cpu: "4"
memory: "8Gi"spec.restartPolicy
What to do when the container exits.
| Value | Description |
|---|---|
always | Always restart (default) |
on-failure | Restart only if exit code != 0 |
never | Never restart |
spec.stopTimeout
Seconds to wait before force-stopping the container (default: 30).
spec.healthcheck
Container health monitoring.
| Field | Type | Default | Description |
|---|---|---|---|
command | [string] | - | Command to run for health check |
intervalSeconds | int | 30 | Time between checks |
timeoutSeconds | int | 10 | Timeout for each check |
retries | int | 3 | Failures before unhealthy |
startPeriodSeconds | int | 0 | Grace period after start |
Example:
spec:
healthcheck:
command: ["mc-health"]
intervalSeconds: 60
timeoutSeconds: 10
retries: 3
startPeriodSeconds: 120spec.imagePullSecret
Name of a registry credential to use when pulling private container images. The credential must be created beforehand via the Credentials API with provider: "registry".
When specified, Shardlyn will look up the named credential, decrypt the registry password, and pass authentication to the Docker daemon during image pull. This applies to both the main spec.image and the optional spec.install.container image.
If omitted, images are pulled anonymously (suitable for public registries like Docker Hub, GHCR, Quay.io).
Example:
spec:
image: registry.example.com/my-org/custom-server:v2.1.0
imagePullSecret: my-registry-credSee the Private Registries section below for a complete setup guide.
spec.stopCommand
Command sent to the container console when stopping. Useful for workloads that need graceful shutdown commands (for example, many game servers).
Example:
spec:
stopCommand: "stop"spec.configFiles
Config file mutations applied before the container starts. Supports file, json, yaml, properties, and ini parsers.
Example:
spec:
configFiles:
server.properties:
parser: properties
find:
server-port: "{{SERVER_PORT}}"
motd: "{{SERVER_NAME}}"spec.install
Optional installation script executed before the main container starts. This is especially useful for game servers that require downloading files via SteamCMD or other installers, but also works for other pre-start bootstrap steps.
| Field | Type | Required | Description |
|---|---|---|---|
container | string | No | Docker image for installation. Defaults to spec.image if not specified. |
entrypoint | string | No | Override entrypoint for install container. Default: /bin/sh |
script | string | Yes | Bash script to execute |
How it works:
- Shardlyn creates a temporary container with the install image
- Mounts the instance volumes to
/mnt/server - Executes the installation script
- Removes the install container when complete
- Starts the main container with files in place
Template Variables: The script has access to environment variables from spec.env. You can use ${VAR_NAME} syntax.
Example - Basic:
spec:
install:
container: ghcr.io/example/installer:latest
entrypoint: /bin/bash
script: |
echo "Preparing server files..."
./install.shSteamCMD Installation
For Source/GoldSrc/Unreal games, use SteamCMD to download game files.
Note: Some games require an authenticated Steam account to download. Counter-Strike 2 (App ID 730) requires authenticated login - anonymous login will fail with error
0x202. Check the SteamDB page for each game to verify if anonymous download is supported.
Example - Counter-Strike 2 (Requires Steam Authentication):
CS2 requires a valid Steam account with CS2 in the library. We recommend using the joedwards32/cs2 image which handles authentication and updates automatically.
apiVersion: shardlyn/v1
kind: Workload
metadata:
name: cs2-server
labels:
game: cs2
spec:
image: joedwards32/cs2:latest
env:
# Required - Steam credentials (account must own CS2)
STEAMUSER: "" # Your Steam username
STEAMPASS: "" # Your Steam password
# Optional - Game Server Login Token (recommended for public servers)
SRCDS_TOKEN: "" # Get from https://steamcommunity.com/dev/managegameservers
# Server configuration
CS2_SERVERNAME: "My CS2 Server"
CS2_PORT: "27015"
CS2_MAXPLAYERS: "16"
CS2_GAMETYPE: "0"
CS2_GAMEMODE: "1"
CS2_STARTMAP: "de_dust2"
CS2_MAPGROUP: "mg_active"
CS2_RCONPW: "changeme"
ports:
- name: game
containerPort: 27015
protocol: UDP
- name: rcon
containerPort: 27015
protocol: TCP
- name: tv
containerPort: 27020
protocol: UDP
volumes:
- name: data
mountPath: /home/steam/cs2-dedicated
sizeGb: 50
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
cpu: "4"
memory: "8Gi"Security Warning: Never commit Steam credentials to version control. Use environment variables or secrets management when deploying.
Example - Rust:
apiVersion: shardlyn/v1
kind: Workload
metadata:
name: rust-server
labels:
game: rust
spec:
image: cm2network/steamcmd:root
install:
container: cm2network/steamcmd:root
entrypoint: /bin/bash
script: |
cd /mnt/server
/home/steam/steamcmd/steamcmd.sh \
+force_install_dir /mnt/server \
+login anonymous \
+app_update 258550 validate \
+quit
command: ["/bin/bash", "-c"]
args:
- |
cd /mnt/server
./RustDedicated -batchmode \
+server.port ${SERVER_PORT} \
+server.hostname "${SERVER_NAME}" \
+server.maxplayers ${MAX_PLAYERS} \
+server.worldsize ${WORLD_SIZE} \
+server.seed ${WORLD_SEED}
env:
SERVER_PORT: "28015"
SERVER_NAME: "Shardlyn Rust Server"
MAX_PLAYERS: "50"
WORLD_SIZE: "3000"
WORLD_SEED: "12345"
ports:
- name: game
containerPort: 28015
protocol: UDP
- name: rcon
containerPort: 28016
protocol: TCP
volumes:
- name: server
mountPath: /mnt/server
sizeGb: 50Example - ARK: Survival Evolved:
apiVersion: shardlyn/v1
kind: Workload
metadata:
name: ark-server
labels:
game: ark
spec:
image: cm2network/steamcmd:root
install:
container: cm2network/steamcmd:root
entrypoint: /bin/bash
script: |
cd /mnt/server
/home/steam/steamcmd/steamcmd.sh \
+force_install_dir /mnt/server \
+login anonymous \
+app_update 376030 validate \
+quit
command: ["/bin/bash", "-c"]
args:
- |
cd /mnt/server/ShooterGame/Binaries/Linux
./ShooterGameServer \
"${MAP_NAME}?listen?SessionName=${SERVER_NAME}?ServerPassword=${SERVER_PASSWORD}?ServerAdminPassword=${ADMIN_PASSWORD}?MaxPlayers=${MAX_PLAYERS}" \
-server -log
env:
MAP_NAME: "TheIsland"
SERVER_NAME: "Shardlyn ARK Server"
SERVER_PASSWORD: ""
ADMIN_PASSWORD: "changeme"
MAX_PLAYERS: "70"
ports:
- name: game
containerPort: 7777
protocol: UDP
- name: query
containerPort: 27015
protocol: UDP
volumes:
- name: server
mountPath: /mnt/server
sizeGb: 100
resources:
requests:
cpu: "2"
memory: "8Gi"
limits:
cpu: "4"
memory: "16Gi"Common SteamCMD App IDs:
| Game | App ID |
|---|---|
| Counter-Strike 2 | 730 |
| Counter-Strike: Global Offensive (Legacy) | 740 |
| Rust | 258550 |
| ARK: Survival Evolved | 376030 |
| Garry's Mod | 4020 |
| Team Fortress 2 | 232250 |
| Left 4 Dead 2 | 222860 |
| Valheim | 896660 |
| Project Zomboid | 380870 |
| 7 Days to Die | 294420 |
| Terraria | 105600 |
| Satisfactory | 1690800 |
Steam Account Login: For games requiring a Steam account (non-anonymous), use environment variables:
spec:
install:
script: |
/home/steam/steamcmd/steamcmd.sh \
+force_install_dir /mnt/server \
+login ${STEAM_USER} ${STEAM_PASS} \
+app_update ${STEAM_APP_ID} validate \
+quit
env:
STEAM_USER: "your_username"
STEAM_PASS: "" # Set at deploy time
STEAM_APP_ID: "12345"Note: For accounts with Steam Guard, you may need to use +set_steam_guard_code or pre-authenticate on the machine.
Complete Examples
Minecraft Vanilla
apiVersion: shardlyn/v1
kind: Workload
metadata:
name: minecraft-vanilla
labels:
game: minecraft
type: vanilla
spec:
image: itzg/minecraft-server:latest
env:
EULA: "TRUE"
TYPE: "VANILLA"
VERSION: "LATEST"
MEMORY: "4G"
MAX_PLAYERS: "20"
MOTD: "Shardlyn Minecraft Server"
ENABLE_RCON: "true"
RCON_PASSWORD: "changeme"
VIEW_DISTANCE: "10"
SPAWN_PROTECTION: "0"
ports:
- name: game
containerPort: 25565
protocol: TCP
- name: rcon
containerPort: 25575
protocol: TCP
volumes:
- name: data
mountPath: /data
sizeGb: 20
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
cpu: "2"
memory: "4Gi"
restartPolicy: alwaysMinecraft with Mods (Forge)
apiVersion: shardlyn/v1
kind: Workload
metadata:
name: minecraft-forge
labels:
game: minecraft
type: forge
modpack: custom
spec:
image: itzg/minecraft-server:java17
env:
EULA: "TRUE"
TYPE: "FORGE"
VERSION: "1.20.1"
FORGE_VERSION: "47.2.0"
MEMORY: "6G"
MAX_PLAYERS: "30"
JVM_OPTS: "-XX:+UseG1GC -XX:+ParallelRefProcEnabled"
ports:
- name: game
containerPort: 25565
protocol: TCP
- name: rcon
containerPort: 25575
protocol: TCP
volumes:
- name: data
mountPath: /data
sizeGb: 50
- name: mods
mountPath: /mods
sizeGb: 10
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
cpu: "4"
memory: "8Gi"
restartPolicy: alwaysCounter-Strike 2
apiVersion: shardlyn/v1
kind: Workload
metadata:
name: cs2-competitive
labels:
game: cs2
type: competitive
spec:
image: cm2network/cs2:latest
env:
SRCDS_TOKEN: ""
CS2_SERVERNAME: "Shardlyn CS2 Server"
CS2_PORT: "27015"
CS2_RCONPW: "changeme"
CS2_PW: ""
CS2_MAXPLAYERS: "16"
CS2_GAMETYPE: "0"
CS2_GAMEMODE: "1"
CS2_MAPGROUP: "mg_active"
CS2_STARTMAP: "de_dust2"
CS2_BOT_QUOTA: "0"
CS2_CHEATS: "0"
ports:
- name: game
containerPort: 27015
protocol: UDP
- name: rcon
containerPort: 27015
protocol: TCP
- name: tv
containerPort: 27020
protocol: UDP
volumes:
- name: data
mountPath: /home/steam/cs2-dedicated
sizeGb: 50
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
cpu: "4"
memory: "8Gi"
restartPolicy: alwaysValheim
apiVersion: shardlyn/v1
kind: Workload
metadata:
name: valheim-server
labels:
game: valheim
type: dedicated
spec:
image: lloesche/valheim-server:latest
env:
SERVER_NAME: "Shardlyn Valheim"
WORLD_NAME: "ShardlynWorld"
SERVER_PASS: "changeme"
SERVER_PUBLIC: "false"
UPDATE_CRON: ""
BACKUPS: "true"
BACKUPS_CRON: "0 */6 * * *"
BACKUPS_MAX_AGE: "7"
ports:
- name: game1
containerPort: 2456
protocol: UDP
- name: game2
containerPort: 2457
protocol: UDP
- name: game3
containerPort: 2458
protocol: UDP
volumes:
- name: config
mountPath: /config
sizeGb: 5
- name: data
mountPath: /opt/valheim
sizeGb: 10
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
cpu: "4"
memory: "8Gi"
restartPolicy: alwaysCustom Application
apiVersion: shardlyn/v1
kind: Workload
metadata:
name: custom-app
labels:
type: custom
spec:
image: my-registry.com/my-app:v1.0.0
command: ["/app/start.sh"]
args: ["--config", "/config/app.yaml"]
workingDir: /app
user: "1000:1000"
env:
APP_ENV: "production"
LOG_LEVEL: "info"
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: metrics
containerPort: 9090
protocol: TCP
volumes:
- name: config
mountPath: /config
readOnly: true
- name: data
mountPath: /data
sizeGb: 100
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
restartPolicy: on-failure
healthcheck:
command: ["curl", "-f", "http://localhost:8080/health"]
intervalSeconds: 30
timeoutSeconds: 5
retries: 3
startPeriodSeconds: 60Private Registries
Shardlyn supports pulling images from private container registries (Docker Hub private repos, GHCR, ECR, self-hosted registries, etc.) via the imagePullSecret field.
Step 1: Create a Registry Credential
curl -X POST https://your-shardlyn.example.com/v1/credentials \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "my-registry-cred",
"provider": "registry",
"registry_server": "registry.example.com",
"registry_username": "deploy-bot",
"registry_password": "your-secret-token"
}'Shardlyn validates the credentials by testing authentication against the registry's /v2/ endpoint.
Common registry servers:
| Registry | Server |
|---|---|
| Docker Hub | https://index.docker.io/v1/ |
| GitHub Container Registry | ghcr.io |
| AWS ECR | 123456789.dkr.ecr.us-east-1.amazonaws.com |
| Google Artifact Registry | us-docker.pkg.dev |
| Self-hosted | registry.example.com |
Step 2: Reference in Workload Spec
apiVersion: shardlyn/v1
kind: Workload
metadata:
name: my-private-server
spec:
image: registry.example.com/my-org/game-server:v1.0.0
imagePullSecret: my-registry-cred
ports:
- name: game
containerPort: 27015
protocol: UDP
volumes:
- name: data
mountPath: /data
sizeGb: 20Using with Install Scripts
When both imagePullSecret and spec.install.container are specified, the same credentials are used to pull the install container image:
spec:
image: registry.example.com/my-org/game-runtime:v1.0.0
imagePullSecret: my-registry-cred
install:
container: registry.example.com/my-org/game-installer:v1.0.0
script: |
cd /mnt/server
./install-game-files.shJSON Schema
The complete JSON Schema for validation is available at pkg/spec/schema.json.
The workload spec can be validated via the API when creating or updating workloads.
Migration from Pterodactyl Eggs
Shardlyn includes an egg importer that converts Pterodactyl egg JSON to Shardlyn workload specs. There is also an import button in the UI (Workloads).
API endpoint:
POST /v1/workloads/import-eggMapping:
| Egg Field | Shardlyn Field |
|---|---|
docker_images | spec.image |
startup | spec.command |
config.files | spec.configFiles |
config.stop | spec.stopCommand |
scripts.installation | spec.install |
Compatibility notes:
- Stop commands are supported via
spec.stopCommand. - Config file parsing supports
file,json,yaml,properties, andini. - Installation scripts run before the main container starts.
Testing Workloads
Shardlyn includes a CLI tool for testing workload specs locally before deploying. It validates the spec against the JSON Schema and optionally runs the full install + boot flow using Docker on your machine.
Quick Start
# Build the test CLI
make build-workload-test
# Validate a spec (no Docker required)
make test-workload FILE=examples/workloads/minecraft.yaml ARGS="-validate-only"
# Test a builtin template with Docker
make test-workload TEMPLATE=redis ARGS="-skip-boot"
# Full install + boot test
make test-workload FILE=examples/workloads/rust-steamcmd.yamlValidation Only
Validates the spec against the JSON Schema without requiring Docker. Useful in CI pipelines and for quick feedback during development.
# Validate a single file
make test-workload FILE=my-workload.yaml ARGS="-validate-only"
# Validate ALL example specs and builtin templates at once
make validate-workloadsFull Docker Test
Runs the complete lifecycle: pull images, run install script (if defined), boot the main container, and verify it starts successfully. Requires Docker.
# Test from a YAML file
make test-workload FILE=examples/workloads/rust-steamcmd.yaml
# Test a builtin template by ID
make test-workload TEMPLATE=minecraft
# Test ALL builtin templates
make test-workload-allAvailable Options
| Flag | Default | Description |
|---|---|---|
-file <path> | - | Workload YAML/JSON file to test |
-template <id> | - | Builtin template ID (e.g. minecraft, postgres) |
-validate-only | false | Only validate the spec, skip Docker |
-skip-boot | false | Run install but skip booting the main container |
-install-timeout | 600 | Install script timeout in seconds |
-boot-timeout | 60 | Boot check timeout in seconds |
-verbose | false | Show full container logs |
-keep | false | Keep volumes after test (for inspection) |
-no-color | false | Disable colored output |
-list-templates | false | List all builtin templates with details |
-list-ids | false | List template IDs only (for scripting) |
Builtin Templates
Shardlyn ships with 40+ builtin workload templates for popular games and applications. List them with:
./bin/workload-test -list-templatesThese templates are the same ones used by the planner when creating instances via the UI.
Example Output
-> Loading builtin template "redis"...
ok Validation passed (1ms)
-> Creating test volumes...
ok Created volume test-a1b2c3d4-data
-> Pulling image redis:7-alpine...
ok Image pulled (2s)
-> Booting main container...
ok Container running (1s)
-> Cleaning up...
ok Cleanup complete
ok Test passed!
Validation: 1ms
Boot: 1s
Total: 3sBest Practices
1. Use Specific Image Tags
# Good
image: itzg/minecraft-server:java17-alpine
# Avoid
image: itzg/minecraft-server:latest2. Set Resource Limits
Always set both requests and limits to prevent resource starvation.
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
cpu: "2"
memory: "4Gi"3. Use Labels for Organization
metadata:
labels:
game: minecraft
version: "1.20.4"
environment: production
team: gaming4. Secure Sensitive Data
- Don't hardcode passwords in specs
- Use environment variables that can be overridden
- Document which env vars contain secrets
5. Size Volumes Appropriately
Consider game world growth over time.
volumes:
- name: world
mountPath: /data
sizeGb: 50 # Room to grow