Skip to content

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

Specification Format

yaml
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: int

Field Reference

metadata

FieldTypeRequiredDescription
namestringYesUnique name for the workload. Used in logs and UI.
labelsmap[string]stringNoKey-value pairs for categorization and filtering.
annotationsmap[string]stringNoArbitrary metadata not used for filtering.

Example:

yaml
metadata:
  name: minecraft-survival
  labels:
    game: minecraft
    version: "1.20.4"
    type: survival
  annotations:
    description: "Main survival server"
    owner: "admin@example.com"

spec.image

Container image to run. Supports any Docker-compatible image reference.

Examples:

yaml
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.3

spec.command and spec.args

Override the container's entrypoint and/or command.

  • command replaces the image's ENTRYPOINT
  • args replaces the image's CMD

Example:

yaml
spec:
  image: openjdk:17
  command: ["/bin/sh", "-c"]
  args: ["java -Xmx4G -jar server.jar nogui"]

spec.env

Environment variables passed to the container.

Example:

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

FieldTypeRequiredDescription
namestringYesIdentifier for the port (e.g., "game", "rcon")
containerPortintYesPort inside the container
hostPortintNoSpecific host port. If omitted, auto-assigned.
protocolstringNoTCP or UDP. Default: TCP

Example:

yaml
spec:
  ports:
    - name: game
      containerPort: 25565
      protocol: TCP
    - name: rcon
      containerPort: 25575
      hostPort: 25575
      protocol: TCP
    - name: query
      containerPort: 25565
      protocol: UDP

spec.volumes

Persistent storage volumes mounted into the container.

FieldTypeRequiredDescription
namestringYesVolume identifier
mountPathstringYesPath inside container
sizeGbintNoRequested size in GB
readOnlyboolNoMount as read-only

Example:

yaml
spec:
  volumes:
    - name: world-data
      mountPath: /data
      sizeGb: 50
    - name: plugins
      mountPath: /plugins
      sizeGb: 5
    - name: config
      mountPath: /config
      readOnly: false

Note: 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.

FieldTypeDescription
requests.cpustringMinimum CPU needed
requests.memorystringMinimum memory needed
limits.cpustringMaximum CPU allowed
limits.memorystringMaximum 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:

yaml
spec:
  resources:
    requests:
      cpu: "1"
      memory: "2Gi"
    limits:
      cpu: "4"
      memory: "8Gi"

spec.restartPolicy

What to do when the container exits.

ValueDescription
alwaysAlways restart (default)
on-failureRestart only if exit code != 0
neverNever restart

spec.stopTimeout

Seconds to wait before force-stopping the container (default: 30).

spec.healthcheck

Container health monitoring.

FieldTypeDefaultDescription
command[string]-Command to run for health check
intervalSecondsint30Time between checks
timeoutSecondsint10Timeout for each check
retriesint3Failures before unhealthy
startPeriodSecondsint0Grace period after start

Example:

yaml
spec:
  healthcheck:
    command: ["mc-health"]
    intervalSeconds: 60
    timeoutSeconds: 10
    retries: 3
    startPeriodSeconds: 120

spec.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:

yaml
spec:
  image: registry.example.com/my-org/custom-server:v2.1.0
  imagePullSecret: my-registry-cred

See 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:

yaml
spec:
  stopCommand: "stop"

spec.configFiles

Config file mutations applied before the container starts. Supports file, json, yaml, properties, and ini parsers.

Example:

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

FieldTypeRequiredDescription
containerstringNoDocker image for installation. Defaults to spec.image if not specified.
entrypointstringNoOverride entrypoint for install container. Default: /bin/sh
scriptstringYesBash script to execute

How it works:

  1. Shardlyn creates a temporary container with the install image
  2. Mounts the instance volumes to /mnt/server
  3. Executes the installation script
  4. Removes the install container when complete
  5. 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:

yaml
spec:
  install:
    container: ghcr.io/example/installer:latest
    entrypoint: /bin/bash
    script: |
      echo "Preparing server files..."
      ./install.sh

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

yaml
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:

yaml
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: 50

Example - ARK: Survival Evolved:

yaml
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:

GameApp ID
Counter-Strike 2730
Counter-Strike: Global Offensive (Legacy)740
Rust258550
ARK: Survival Evolved376030
Garry's Mod4020
Team Fortress 2232250
Left 4 Dead 2222860
Valheim896660
Project Zomboid380870
7 Days to Die294420
Terraria105600
Satisfactory1690800

Steam Account Login: For games requiring a Steam account (non-anonymous), use environment variables:

yaml
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

yaml
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: always

Minecraft with Mods (Forge)

yaml
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: always

Counter-Strike 2

yaml
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: always

Valheim

yaml
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: always

Custom Application

yaml
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: 60

Private 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

bash
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:

RegistryServer
Docker Hubhttps://index.docker.io/v1/
GitHub Container Registryghcr.io
AWS ECR123456789.dkr.ecr.us-east-1.amazonaws.com
Google Artifact Registryus-docker.pkg.dev
Self-hostedregistry.example.com

Step 2: Reference in Workload Spec

yaml
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: 20

Using with Install Scripts

When both imagePullSecret and spec.install.container are specified, the same credentials are used to pull the install container image:

yaml
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.sh

JSON 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-egg

Mapping:

Egg FieldShardlyn Field
docker_imagesspec.image
startupspec.command
config.filesspec.configFiles
config.stopspec.stopCommand
scripts.installationspec.install

Compatibility notes:

  • Stop commands are supported via spec.stopCommand.
  • Config file parsing supports file, json, yaml, properties, and ini.
  • 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

bash
# 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.yaml

Validation Only

Validates the spec against the JSON Schema without requiring Docker. Useful in CI pipelines and for quick feedback during development.

bash
# 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-workloads

Full Docker Test

Runs the complete lifecycle: pull images, run install script (if defined), boot the main container, and verify it starts successfully. Requires Docker.

bash
# 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-all

Available Options

FlagDefaultDescription
-file <path>-Workload YAML/JSON file to test
-template <id>-Builtin template ID (e.g. minecraft, postgres)
-validate-onlyfalseOnly validate the spec, skip Docker
-skip-bootfalseRun install but skip booting the main container
-install-timeout600Install script timeout in seconds
-boot-timeout60Boot check timeout in seconds
-verbosefalseShow full container logs
-keepfalseKeep volumes after test (for inspection)
-no-colorfalseDisable colored output
-list-templatesfalseList all builtin templates with details
-list-idsfalseList template IDs only (for scripting)

Builtin Templates

Shardlyn ships with 40+ builtin workload templates for popular games and applications. List them with:

bash
./bin/workload-test -list-templates

These 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:      3s

Best Practices

1. Use Specific Image Tags

yaml
# Good
image: itzg/minecraft-server:java17-alpine

# Avoid
image: itzg/minecraft-server:latest

2. Set Resource Limits

Always set both requests and limits to prevent resource starvation.

yaml
resources:
  requests:
    cpu: "1"
    memory: "2Gi"
  limits:
    cpu: "2"
    memory: "4Gi"

3. Use Labels for Organization

yaml
metadata:
  labels:
    game: minecraft
    version: "1.20.4"
    environment: production
    team: gaming

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

yaml
volumes:
  - name: world
    mountPath: /data
    sizeGb: 50  # Room to grow

Built for teams that want control of their own infrastructure.