Table of Contents
- 🔧 Extending and Creating Eggs
- Table of Contents
- Understanding Egg Structure
- 1. Metadata - The Identity Card 🎫
- 2. Docker Images - The Foundation 🏗️
- 3. Variables - User Customization ⚙️
- 4. Scripts - The Worker Bees 🐝
- 5. Config - File Management 📝
- 6. Startup Detection - Knowing It's Ready 🎯
- Extending an Existing Egg
- Creating an Egg from Scratch
- Configuration Files Management
- Understanding Parsers
- Example 1: Managing a JSON Config File
- Example 2: Managing Multiple Files
- Example 3: Properties File Management
- Advanced Variable Usage
- Real Examples
- Example 1: Simple Game Server (Counter-Strike 2)
- Example 2: Discord Bot (Complex Config File)
- Example 3: Web Application (Node.js)
- Checklist: Creating a New Egg ✅
- Next Steps 🚀
- Learning Path 📚
- Helpful Links 🔗
- Common Mistakes to Avoid ❌
- Summary 📝
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
- Extending an Existing Egg
- Creating an Egg from Scratch
- Configuration Files Management
- Advanced Variable Usage
- Real Examples
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 systemnodejs_*— Node.js with different versionsjava_*— Java with different versionspython_*— Python with different versionsghcr.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 useghcr.io/ptero-eggs/installers:debianorghcr.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
startupcommand, use{{VARIABLE_NAME}}. Inconfig.filesparsers, 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.filesis stored: In the actual egg JSON, theconfig.filesvalue 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.filesparsers:"server-port": "{{server.build.default.port}}"
{{server.build.env.VARIABLE_NAME}}
- Access a user variable in
config.filesparsers - 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 thestartupfield. Use{{server.build.env.VARIABLE_NAME}}insideconfig.filesparsers. 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 (1–64)",
"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:debianor:alpine - ✅ Variables have clear descriptions and all required fields
- ✅ Every variable includes
user_viewableanduser_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:
-
- Complete list of all Wings-provided variables
{{server.build.default.port}},{{server.build.env.VAR}}, and more- Real examples from actual eggs
-
Configuration Variables Reference ⚙️
- All about user-customizable variables
- Variable validation rules
- Best practices for variable naming
-
- 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:
- Egg Basics — What are eggs and basic structure
- Configuration Variables — How to make your egg customizable
- Extending Eggs — This page! Creating and extending eggs
- Wings Variables — Special variables from the panel
- File Parsers — Managing configuration files
Helpful Links 🔗
- Back to README — Overview of all documentation
- Egg Basics — Start here if you're new
- Configuration Variables — Create customizable settings
- Wings Variables — Panel-provided variables
- File Parsers — Automatic file management
Common Mistakes to Avoid ❌
-
Hardcoding values ❌
- Wrong:
"startup": "java -jar server.jar --port 25565" - Right:
"startup": "java -jar server.jar --port {{SERVER_PORT}}"
- Wrong:
-
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!
- Startup command →
-
Forgetting variable descriptions ❌
- Users won't know what to put in!
-
Missing
user_viewableoruser_editable❌- Always include both in every variable definition
-
Wrong install path ❌
- Installation script must write to
/mnt/server, not/home/container
- Installation script must write to
-
Using wrong parser ❌
- Each file format needs the right parser — YAML files need
"parser": "yaml", not"parser": "json"
- Each file format needs the right parser — YAML files need
-
Not testing ❌
- Always test your egg on a fresh installation before sharing
-
Case sensitivity ❌
- Variable names are case-sensitive!
{{server_port}}≠{{SERVER_PORT}}
Summary 📝
You've learned:
- Eggs are single
.jsonfiles — 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! 🥚🚀