1
0
Fork 0
2 Extending eggs
Damien FLETY edited this page 2026-04-12 20:46:58 +00:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

🔧 Extending and Creating Eggs

Now that you understand the basics, let's learn how to extend existing eggs and create new ones from scratch! This guide is perfect for 14-year-olds and shows you exactly how to customize eggs for your needs.

Table of Contents

Understanding Egg Structure

Every egg is a single .json file (for example, egg-myserver.json). There are no folders, no separate script files — everything lives inside that one file. Inside it, there are several main sections:

egg-name.json
├── Metadata (name, author, description)
├── Docker Images (what OS to use)
├── Variables (settings users can customize)
├── Scripts (installation script embedded as a string)
├── Config (file management)
└── Startup Detection (how to know it's running)

Let's break down what each part does:

1. Metadata - The Identity Card 🎫

This tells Wings about your egg:

{
  "name": "My Awesome Game Server",
  "author": "your_name@example.com",
  "description": "A super cool server for my game",
  "meta": {
    "version": "PTDL_v2"
  }
}

2. Docker Images - The Foundation 🏗️

Docker images are like pre-built computers with software already installed. Here are common ones:

{
  "docker_images": {
    "Ubuntu": "ghcr.io/ptero-eggs/yolks:ubuntu",
    "Node.js 20": "ghcr.io/ptero-eggs/yolks:nodejs_20",
    "Java 21": "ghcr.io/ptero-eggs/yolks:java_21",
    "Java 17": "ghcr.io/ptero-eggs/yolks:java_17",
    "Python 3.12": "ghcr.io/ptero-eggs/yolks:python_3.12"
  }
}

Common image families:

  • ubuntu / debian — A basic Linux system
  • nodejs_* — Node.js with different versions
  • java_* — Java with different versions
  • python_* — Python with different versions
  • ghcr.io/ptero-eggs/steamcmd:sniper — For Steam games (CS2, Rust, etc.)

Note: All runtime images live at ghcr.io/ptero-eggs/yolks:*. Installation containers are separate — they use ghcr.io/ptero-eggs/installers:debian or ghcr.io/ptero-eggs/installers:alpine.

3. Variables - User Customization ⚙️

Variables let users customize your egg without editing files. See the Configuration Variables Reference for more details.

4. Scripts - The Worker Bees 🐝

Scripts do the actual work. The installation script is embedded directly inside the JSON as a string at scripts.installation.script — there is no separate script file:

{
  "scripts": {
    "installation": {
      "container": "ghcr.io/ptero-eggs/installers:debian",
      "entrypoint": "bash",
      "script": "#!/bin/bash\necho 'Setting up your server...'\nmkdir -p /mnt/server\ncd /mnt/server\n# More installation commands here"
    }
  }
}

Important paths: During installation, server files go into /mnt/server. At runtime, the container sees those same files at /home/container. These are two different views of the same storage!

5. Config - File Management 📝

This tells Wings how to manage configuration files. The config.files, config.startup, and config.logs fields are stored as JSON-encoded strings inside the egg — not as raw objects:

{
  "config": {
    "files": "{}",
    "logs": "{}",
    "startup": "{\"done\": \"Server started\"}",
    "stop": "stop"
  }
}

We'll dive deeper into this later!

6. Startup Detection - Knowing It's Ready 🎯

The config.startup string, when decoded, tells Wings what text to look for in console output:

{
  "done": "Server is ready to accept connections"
}

When Wings sees that text printed to the console, it marks the server as "running" in the panel.

Extending an Existing Egg

The easiest way to create a new egg is to copy an existing one and modify it!

Step-by-Step Example: Creating a Minecraft Spigot Egg

Let's say you want to create a Spigot server egg (Spigot is a popular Minecraft server).

Step 1: Find a similar egg

Look for an existing Minecraft or Java-based egg in the eggs repository.

Step 2: Copy the egg file

cp egg-papermc.json egg-spigot.json

Step 3: Update the metadata

Change the name, description, and author:

{
  "name": "Spigot Server",
  "author": "your_name@example.com",
  "description": "A Spigot Minecraft Server - Fast and customizable!"
}

Step 4: Update the installation script

In scripts.installation.script, change the download URL. The script runs inside the installation container and places files in /mnt/server:

#!/bin/bash
# Spigot Installation Script
# Server Files: /mnt/server
mkdir -p /mnt/server
cd /mnt/server
wget https://hub.spigotmc.org/jenkins/latest/spigot-latest.jar -O server.jar
echo 'Download complete!'

Step 5: Test your egg

Try installing and running it to make sure everything works!

Example: Extending a Discord Bot

Let's extend a bot egg to add a new variable.

Incomplete variable (missing required fields — don't do this!):

{
  "name": "Token",
  "env_variable": "DISCORD_TOKEN",
  "default_value": ""
}

Complete, correct variable (always include ALL fields):

{
  "name": "Bot Token",
  "description": "Your Discord bot token from the Developer Portal",
  "env_variable": "DISCORD_TOKEN",
  "default_value": "",
  "user_viewable": true,
  "user_editable": true,
  "rules": "required|string|max:100",
  "field_type": "text"
}

Add a new variable for the command prefix:

{
  "name": "Bot Prefix",
  "description": "The character that triggers bot commands (e.g., !)",
  "env_variable": "BOT_PREFIX",
  "default_value": "!",
  "user_viewable": true,
  "user_editable": true,
  "rules": "required|string|max:4",
  "field_type": "text"
}

Then use it in your startup command. In the startup field, reference variables with {{VARIABLE_NAME}} directly:

./bot --token {{DISCORD_TOKEN}} --prefix {{BOT_PREFIX}}

Important: In the startup command, use {{VARIABLE_NAME}}. In config.files parsers, use {{server.build.env.VARIABLE_NAME}}. These are different syntaxes for different places!

Creating an Egg from Scratch

Let's create a complete egg for a simple Node.js application!

Full Example: Node.js Web Server Egg

Here's a complete egg file. Remember: this is the entire egg — just one .json file, no folders!

{
  "_comment": "My First Web Server Egg",
  "meta": {
    "version": "PTDL_v2"
  },
  "exported_at": "2024-01-01T00:00:00+00:00",
  "name": "Node.js Web Server",
  "author": "student@school.com",
  "description": "A simple Node.js web server egg for beginners",
  "features": null,
  "docker_images": {
    "Node.js 20": "ghcr.io/ptero-eggs/yolks:nodejs_20",
    "Node.js 18": "ghcr.io/ptero-eggs/yolks:nodejs_18"
  },
  "file_denylist": [],
  "startup": "node /home/container/index.js",
  "config": {
    "files": "{}",
    "logs": "{}",
    "startup": "{\"done\": \"Server listening on port\"}",
    "stop": "^C"
  },
  "scripts": {
    "installation": {
      "container": "ghcr.io/ptero-eggs/installers:debian",
      "entrypoint": "bash",
      "script": "#!/bin/bash\necho 'Installing Node.js app...'\nmkdir -p /mnt/server\ncd /mnt/server\nnpm install\necho 'Installation complete!'"
    }
  },
  "variables": [
    {
      "name": "Port",
      "description": "The port your web server will listen on",
      "env_variable": "PORT",
      "default_value": "3000",
      "user_viewable": true,
      "user_editable": true,
      "rules": "required|numeric|between:1024,65535",
      "field_type": "text"
    },
    {
      "name": "Server Name",
      "description": "The display name of your web server",
      "env_variable": "SERVER_NAME",
      "default_value": "My Awesome Server",
      "user_viewable": true,
      "user_editable": true,
      "rules": "required|string|max:64",
      "field_type": "text"
    }
  ]
}

Save this as egg-nodejs-webserver.json — that single file is your entire egg!

Now create a simple Node.js server file to go with it:

// index.js  (lives at /home/container/index.js at runtime)
const http = require('http');

const PORT = process.env.PORT || 3000;
const SERVER_NAME = process.env.SERVER_NAME || 'My Server';

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end(`Welcome to ${SERVER_NAME}!\n`);
});

server.listen(PORT, '0.0.0.0', () => {
  console.log(`Server listening on port ${PORT}`);
});

Configuration Files Management

One of the most powerful features of Wings is managing configuration files. Let's learn how!

Understanding Parsers

Wings can edit files in different formats:

Parser Format Example Files
json JSON format config.json, settings.json
yaml YAML format config.yml, settings.yaml
ini INI format .my.cnf, config.ini
file Plain text server.properties, config.txt
properties Java properties server.properties, bot.properties

How config.files is stored: In the actual egg JSON, the config.files value is a JSON-encoded string (the whole thing is wrapped in quotes with escaped characters inside). The decoded examples below show what the value looks like once parsed — Wings handles the decoding automatically.

Example 1: Managing a JSON Config File

Let's say you have a Discord bot with a JSON config:

config.json:

{
  "token": "YOUR_TOKEN_HERE",
  "prefix": "!",
  "owner_id": "12345"
}

The decoded config.files value in your egg:

{
  "config.json": {
    "parser": "json",
    "find": {
      "token": "{{server.build.env.BOT_TOKEN}}",
      "prefix": "{{server.build.env.BOT_PREFIX}}",
      "owner_id": "{{server.build.env.OWNER_ID}}"
    }
  }
}

In your variables array:

{
  "variables": [
    {
      "name": "Bot Token",
      "description": "Your Discord bot token from the Developer Portal",
      "env_variable": "BOT_TOKEN",
      "default_value": "",
      "user_viewable": true,
      "user_editable": true,
      "rules": "required|string|max:100",
      "field_type": "text"
    },
    {
      "name": "Command Prefix",
      "description": "Character used to trigger bot commands",
      "env_variable": "BOT_PREFIX",
      "default_value": "!",
      "user_viewable": true,
      "user_editable": true,
      "rules": "required|string|max:4",
      "field_type": "text"
    },
    {
      "name": "Owner ID",
      "description": "Discord user ID of the bot owner",
      "env_variable": "OWNER_ID",
      "default_value": "",
      "user_viewable": true,
      "user_editable": true,
      "rules": "nullable|string|max:20",
      "field_type": "text"
    }
  ]
}

Wings will automatically update config.json when a user changes these variables!

Example 2: Managing Multiple Files

You can manage multiple configuration files at once. Decoded config.files value:

{
  "config/main.json": {
    "parser": "json",
    "find": {
      "server_port": "{{server.build.default.port}}",
      "server_name": "{{server.build.env.SERVER_NAME}}"
    }
  },
  "config/plugins.yaml": {
    "parser": "yaml",
    "find": {
      "enabled_plugins": "{{server.build.env.PLUGINS}}"
    }
  },
  "settings.ini": {
    "parser": "ini",
    "find": {
      "database.host": "{{server.build.env.DB_HOST}}",
      "database.port": "{{server.build.env.DB_PORT}}"
    }
  }
}

Example 3: Properties File Management

For server.properties files (common in Minecraft). Decoded config.files value:

{
  "server.properties": {
    "parser": "properties",
    "find": {
      "server-ip": "0.0.0.0",
      "server-port": "{{server.build.default.port}}",
      "max-players": "{{server.build.env.MAX_PLAYERS}}"
    }
  }
}

Advanced Variable Usage

Using Wings-Provided Variables

Wings provides special variables you can use:

{{server.build.default.port}}

  • The default port allocated to this server
  • Used in config.files parsers: "server-port": "{{server.build.default.port}}"

{{server.build.env.VARIABLE_NAME}}

  • Access a user variable in config.files parsers
  • Example: "token": "{{server.build.env.BOT_TOKEN}}"

{{VARIABLE_NAME}}

  • Reference a variable directly in the startup command
  • Example: java -jar server.jar --port {{SERVER_PORT}}

Rule of thumb: Use {{VARIABLE_NAME}} in the startup field. Use {{server.build.env.VARIABLE_NAME}} inside config.files parsers. Mixing these up is one of the most common mistakes!

Example: Complete Configuration System

Here's a real example from a Discord bot egg:

Variables array:

{
  "variables": [
    {
      "name": "Discord Token",
      "description": "Your bot token from the Discord Developer Portal",
      "env_variable": "DISCORD_TOKEN",
      "default_value": "",
      "user_viewable": true,
      "user_editable": true,
      "rules": "required|string|max:100",
      "field_type": "text"
    },
    {
      "name": "Bot Prefix",
      "description": "Character used to trigger commands",
      "env_variable": "BOT_PREFIX",
      "default_value": "!",
      "user_viewable": true,
      "user_editable": true,
      "rules": "required|string|max:4",
      "field_type": "text"
    },
    {
      "name": "Database URL",
      "description": "Connection string for the bot's database",
      "env_variable": "DATABASE_URL",
      "default_value": "sqlite://./bot.db",
      "user_viewable": true,
      "user_editable": true,
      "rules": "required|string|max:255",
      "field_type": "text"
    }
  ]
}

Decoded config.files value (.env file managed via properties parser):

{
  ".env": {
    "parser": "properties",
    "find": {
      "DISCORD_TOKEN": "{{server.build.env.DISCORD_TOKEN}}",
      "BOT_PREFIX": "{{server.build.env.BOT_PREFIX}}",
      "DATABASE_URL": "{{server.build.env.DATABASE_URL}}"
    }
  }
}

Generated .env file at runtime:

DISCORD_TOKEN=abc123token
BOT_PREFIX=!
DATABASE_URL=sqlite://./bot.db

Real Examples

Example 1: Simple Game Server (Counter-Strike 2)

Here's how CS2 handles multiple settings. Every variable has all required fields:

{
  "startup": "LD_LIBRARY_PATH=\"$HOME/game/bin/linuxsteamrt64:$LD_LIBRARY_PATH\" ./game/bin/linuxsteamrt64/cs2 -dedicated -ip 0.0.0.0 -port {{SERVER_PORT}} -maxplayers {{MAX_PLAYERS}} +hostname \"{{SERVER_NAME}}\"",
  "variables": [
    {
      "name": "Server Name",
      "description": "Display name shown in the server browser",
      "env_variable": "SERVER_NAME",
      "default_value": "My CS2 Server",
      "user_viewable": true,
      "user_editable": true,
      "rules": "required|string|max:64",
      "field_type": "text"
    },
    {
      "name": "Max Players",
      "description": "Maximum number of players allowed at once (164)",
      "env_variable": "MAX_PLAYERS",
      "default_value": "10",
      "user_viewable": true,
      "user_editable": true,
      "rules": "required|numeric|between:1,64",
      "field_type": "text"
    },
    {
      "name": "Game Mode",
      "description": "Game mode: 0=casual, 1=competitive, 2=deathmatch",
      "env_variable": "GAME_MODE",
      "default_value": "1",
      "user_viewable": true,
      "user_editable": true,
      "rules": "required|numeric|between:0,5",
      "field_type": "text"
    }
  ]
}

Example 2: Discord Bot (Complex Config File)

The Ree6 bot shows how to handle many configuration options through a YAML file. Decoded config.files value:

{
  "config.yml": {
    "parser": "yaml",
    "find": {
      "bot.tokens.release": "{{server.build.env.BOT_TOKEN}}",
      "bot.misc.status": "{{server.build.env.MISC_STATUS}}",
      "database.user": "{{server.build.env.DATABASE_USER}}",
      "database.password": "{{server.build.env.DATABASE_PW}}",
      "database.host": "{{server.build.env.DATABASE_HOST}}"
    }
  }
}

Example 3: Web Application (Node.js)

Startup command and decoded config.files for a Node.js app using a .env file:

{
  "startup": "npm start"
}

Decoded config.files:

{
  ".env": {
    "parser": "properties",
    "find": {
      "PORT": "{{server.build.default.port}}",
      "NODE_ENV": "production",
      "APP_SECRET": "{{server.build.env.APP_SECRET}}"
    }
  }
}

Checklist: Creating a New Egg

Before you share your egg, make sure to check:

  • Metadata is correct (name, author, description)
  • Docker image exists and uses the ghcr.io/ptero-eggs/yolks:* registry
  • Installer container uses ghcr.io/ptero-eggs/installers:debian or :alpine
  • Variables have clear descriptions and all required fields
  • Every variable includes user_viewable and user_editable
  • Every variable has "field_type": "text"
  • Installation script targets /mnt/server, not /home/container
  • Startup command references runtime path /home/container
  • Startup detection pattern is correct
  • Configuration files are properly defined
  • All variable names match between sections
  • Tested on a fresh installation
  • No hardcoded values (use variables instead)

Next Steps 🚀

Now that you can create and extend eggs, you might want to learn about:

  1. Wings Variables Reference 🌐

    • Complete list of all Wings-provided variables
    • {{server.build.default.port}}, {{server.build.env.VAR}}, and more
    • Real examples from actual eggs
  2. Configuration Variables Reference ⚙️

    • All about user-customizable variables
    • Variable validation rules
    • Best practices for variable naming
  3. File Parsers Guide 📝

    • Deep dive into managing different file formats
    • JSON, YAML, INI, properties, and file parsers
    • Tips and real-world examples

Learning Path 📚

You should read the documentation in this order:

  1. Egg Basics — What are eggs and basic structure
  2. Configuration Variables — How to make your egg customizable
  3. Extending Eggs — This page! Creating and extending eggs
  4. Wings Variables — Special variables from the panel
  5. File Parsers — Managing configuration files

Common Mistakes to Avoid

  1. Hardcoding values

    • Wrong: "startup": "java -jar server.jar --port 25565"
    • Right: "startup": "java -jar server.jar --port {{SERVER_PORT}}"
  2. Wrong variable syntax in the wrong place

    • Startup command → {{VARIABLE_NAME}}
    • Config files parser → {{server.build.env.VARIABLE_NAME}}
    • Mixing these up and your variables just won't work!
  3. Forgetting variable descriptions

    • Users won't know what to put in!
  4. Missing user_viewable or user_editable

    • Always include both in every variable definition
  5. Wrong install path

    • Installation script must write to /mnt/server, not /home/container
  6. Using wrong parser

    • Each file format needs the right parser — YAML files need "parser": "yaml", not "parser": "json"
  7. Not testing

    • Always test your egg on a fresh installation before sharing
  8. Case sensitivity

    • Variable names are case-sensitive!
    • {{server_port}}{{SERVER_PORT}}

Summary 📝

You've learned:

  • Eggs are single .json files — copy one to start a new egg!
  • How to extend existing eggs by modifying variables and scripts
  • How to create new eggs from scratch with all required fields
  • How to manage configuration files with parsers
  • How and when to use {{VARIABLE_NAME}} vs {{server.build.env.VARIABLE_NAME}}
  • Real examples from actual game server and bot eggs

The key is: Start simple, test often, and gradually add complexity!

Happy egg creating! 🥚🚀