Breaking down Pterodactyl's Functionality
I've been doing a bunch of Ptero Panel modding recently, and that's gotten me quite hyped up for projects like Moonlight Panel, however that thing is made in C# and I’m NOT touching that language with a 12 foot pole. This article going to be a deep dive into what pterodactyl panel does with its docker magic, and how the frontend and backend communicate. Hopefully inspiring developers and showing you it's really not that hard to make (it's hard to make secure lol)
Deciphering Wings
Like all good things, we should probably start from the back. Wings is essentially a glorified docker wrapper with API endpoints, websockets and cron stuff.
Let’s start with the most important part (the one that runs all of it), the docker communication elements. To figure this out lets first grab a file tree of the project (also available on github):
wings/
├── cmd/
├── config/
├── environment/
│ └── docker/
│ ├── api.go
│ ├── container.go
│ ├── environment.go
│ ├── power.go
│ └── stats.go
├── events/
├── internal/
│ ├── cron/
│ ├── database/
│ ├── models/
│ ├── progress/
│ └── ufs/
├── loggers/
│ └── cli/
├── parser/
├── remote/
├── router/
│ ├── downloader/
│ ├── middleware/
│ ├── tokens/
│ └── websocket/
├── server/
│ ├── backup/
│ ├── filesystem/
│ │ ├── archiverext/
│ │ └── testdata/
│ ├── installer/
│ └── transfer/
├── sftp/
├── system/
├── Dockerfile
├── go.mod
├── go.sum
└── wings.go
Now that we have a lay of the land, let's focus on that environment/docker/
directory. This is where the magic happens for running the actual game servers. Wings uses the official Docker Go SDK (github.com/docker/docker/client
) to directly talk to the Docker daemon running on the host machine. No messing about with shell commands here, it's all programmatic API calls.
Here's the rundown on the key players in that directory:
environment.go
: This is the main struct (Environment
) that represents a server's runtime. Think of it as the control center for a specific server's container. It holds the config, the Docker container ID, the current state (Offline
,Running
, etc.), and the Docker client itself. It provides the standard interface the rest of Wings uses to talk about a server's environment, hiding the Docker-specific bits.container.go
: This guy handles the nitty-gritty of the container's lifecycle.Create()
builds the container: pulls the right image (ensureImageExists()
), sets up ports, mounts volumes for server data, applies resource limits, injects environment variables – the works.Attach()
hooks into the container's input/output streams (client.ContainerAttach
). This is how you see console output and how Wings sends commands in. Crucially, this also kicks off the resource monitoring (pollResources()
).SendCommand()
just shoves text into the container's standard input. Simple.InSituUpdate()
lets you tweak CPU/memory limits on a live container (client.ContainerUpdate
). Handy tool to have to limit containers that wish to screw with your usage.Destroy()
cleans up, stopping and removing the container (client.ContainerRemove
).
power.go
: Handles all power actions.OnBeforeStart()
makes sure you start fresh. It nukes the old container (client.ContainerRemove
) and callsCreate()
to build a new one with the latest settings from the Panel.Start()
is the launch sequence: callsOnBeforeStart()
, thenAttach()
, then fires the engines withclient.ContainerStart
. It keeps track of the state (Starting
,Running
) and handles bumps along the way.Stop()
tries to be nice. It figures out how the server is configured to stop (a specific command likestop
or a signal likeSIGINT
) and sends that viaSendCommand()
orSignalContainer()
. If it doesn't know how, it falls back to the default Docker stop (client.ContainerStop
).WaitForStop()
callsStop()
and then waits patiently (client.ContainerWait
) for the container to actually die. If it takes too long, it can bring out the hammer (Terminate()
).Terminate()
/SignalContainer()
are the low-level functions for sending signals (client.ContainerKill
), likeSIGKILL
when patience runs out.
stats.go
: The stat counter.pollResources()
runs in the background once attached. It listens to the Docker stats stream (client.ContainerStats
), crunches the numbers for CPU, memory (using calculations that match thedocker stats
command), and network traffic, then broadcasts these stats (environment.Stats
) for the Panel to display.Uptime()
just figures out how long the container has been running.
api.go
: Containing custom, faster way to get container details throughContainerInspect
by hitting the Docker API endpoint directly via HTTP, bypassing some overhead in the standard SDK call. Also handles parsing API errors.
So, that's the core of the Docker orchestration. Wings uses these components to turn Panel requests into direct Docker API actions, giving you isolated, controlled server environments.
At this point we’re already like 80% of the way to the important functionality that wings has, the only remaining part is to expose all the API calls and SFTP.
Speaking of SFTP… let’s talk about
Filesystem Access and How It’s Granted
Alright, so how does the fancy web UI let you mess with files? It's not magic, it's just more API calls plumbed through Wings. Remember that router/
directory? That's where the HTTP endpoints live. Specifically, file operations are handled by routes defined in wings/router/router_server_files.go
.
When you click around in the Panel's file manager (list directory, view a file, upload something), the frontend sends an API request to the Panel, which in turn relays a request to the appropriate Wings endpoint (like /api/servers/{uuid}/files/list
).
Here's the flow:
- Request Hits Wings: The request arrives at a specific route in
router_server_files.go
. - Authentication Middleware: Before the route handler even sees the request, middleware steps in. This middleware (
router/middleware/
) grabs the authorization token (a JWT) sent with the request. It verifies this token is valid and was issued by the Panel. Crucially, it extracts who is making the request and what server ({uuid}
) they're trying to access. It likely also pulls the user's specific permissions for that server's files from the token's payload. - Authorization & Filesystem Context: The middleware injects the server context (let's call it
s
) and potentially the validated user permissions into the request context. The route handler can then easily get the correctFilesystem
object for that specific server by calling something likes.Filesystem()
. ThisFilesystem
object (fromwings/server/filesystem/filesystem.go
) is sandboxed – it knows the root path for the server's data and won't allow access outside it (thanks to path validation logic inpath.go
). - Permission Check: Before performing the action, the
Filesystem
method itself (or the route handler calling it) checks if the authenticated user actually has permission for the requested operation (e.g.,read
,write
,delete
) on the specific file or directory. This uses the permissions extracted from the token. If the check fails, it bails out with an error (likely a 403 Forbidden). - Action Execution: If authentication and permissions pass, the
Filesystem
method is called. For example, as we found withgrep
, the handler for listing files callss.Filesystem().ListDirectory(dir)
. Similar handlers exist for reading (ReadFile()
), writing (Writefile()
), compressing (CompressFiles()
), deleting (Delete()
), etc. These methods infilesystem.go
,archive.go
, andcompress.go
perform the actual disk operations within the server's data directory. - Response: The result (file list, file content, success message, or error) is sent back up the chain to the Panel and finally to your browser.
Here’s a diagram showing exactly what’s up (I recommend opening in a new tab)
So, filesystem access isn't granted directly. It's proxied through Wings' API, gated by strict authentication (verifying the request comes from the Panel for a specific user) and fine-grained authorization (checking the user's permissions for that server and action) at multiple steps. The
Filesystem
object ensures operations stay within the server's designated boundaries.
Now how do I build and run docker images with this new info?
Let's start with what Pterodactyl uses, it's own first-party format called Eggs. You can think of an Egg as Pterodactyl's fancy blueprint for a specific server type. It's typically just a JSON file that spells out everything Wings needs to know to get a server up and running, from installation to configuration to the command that actually starts the containers.
Here's all the info that an egg would contain:
- Metadata: Boring stuff - name, author, description. Also flags special features like if it needs you to accept a EULA.
- Docker Image: Tells Wings which base Docker image to pull (e.g.,
ghcr.io/pterodactyl/yolks:java_17
). This is the foundation the server runs on. It also specifies the user ID to run as inside the container. - Startup Command: The exact command line Wings executes inside the container to start the server process (e.g.,
java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar server.jar
). Notice the{{SERVER_MEMORY}}
? That's a variable, we'll get to those. - Configuration: Hints for the Panel and Wings:
files
: Defines config files the Panel should let users edit (likeserver.properties
), specifying how to parse them (YAML, Java properties, INI, etc.) and rules for finding/replacing values.startup
: Contains hints about when the server is considered "done" starting up (e.g., look for "Done" in the console output).logs
: Specifies where log files are if they're not just dumped to the console.stop
: Defines how to gracefully stop the server – usually a command to send (likestop
) or a signal (^C
for SIGINT).
- Installation Script: This is a big one. It's a shell script (
scripts.installation.script
) that Wings runs in a temporary container before the main server container is even created. This script does the heavy lifting: downloading game files (steamcmd
,wget
), running installers, setting up initial configs, downloading mods, etc. It even uses its own specified Docker image (scripts.installation.container
), often one with tools likecurl
orsteamcmd
pre-installed. - Variables: The secret sauce for customization. This section defines a list of variables (e.g.,
SERVER_NAME
,SERVER_PORT
,MAX_PLAYERS
). Each variable has:- A user-friendly name and description (shown in the Panel UI).
- The actual environment variable key (e.g.,
SERVER_PORT
). - A default value.
- Flags for whether the user can see/edit it.
- Validation rules (e.g., must be a number between 1024 and 65535). So, how does Wings use this?
- The Panel sends the chosen Egg's JSON data to Wings.
- Wings parses this JSON.
- Installation: If needed, Wings spins up the temporary
script.container
image, injects the user-configuredvariables
as environment variables, and runs thescript.install
shell script inside it. This downloads/sets up the server files in a volume. - Container Creation: Wings uses the main
docker_image
from the Egg. It configures the container's environment, injecting the final set of resolvedvariables
(defaults overridden by user values). It sets up port mappings based on allocations. It mounts the volume where the installation script put the server files. - Startup: Wings constructs the final
startup
command, replacing placeholders like{{SERVER_MEMORY}}
with the actual resolved variable values. - Execution: Wings starts the container using the constructed startup command. The server process boots up inside the container, reading its configuration from the mounted volume and environment variables.
- Management: Wings uses the
config
hints (stop command, log location, parsable files) to manage the running server via API calls from the Panel. That's the Egg lifecycle in a nutshell. It's a structured way to define everything needed to go from zero to a running, configurable server instance within a Docker container.
Why not just a docker compose?
That's a great question! I mean there's technically nothing stopping you, if you're a power user and want to host your own panel you could technically even modify pterodactyl to use docker composes. But the Pterodactyl devs didn't choose Eggs just for giggles. There are solid reasons why this custom format makes more sense for this specific application (a multi-user hosting panel):
- Keep It Simple, Stupid (KISS for Users): Let's be real, your average Minecraft server admin doesn't want to learn Docker Compose syntax, manage
.env
files, or debug volume mount permissions. They want to click "Paper", type in a server name, maybe pick a port, and have it work. Eggs abstract away all the Docker complexity. The Panel UI reads the Egg's variable definitions and presents simple forms. Job done. - Dynamic Config That Doesn't Suck: Eggs are built around user-configurable variables. The Panel UI is generated directly from the
variables
section of the Egg. Trying to achieve this level of dynamic, UI-driven configuration with standard Docker Compose would be a nightmare. You'd need weird conventions or sidecar containers just to expose configurable parameters cleanly. Eggs define this metadata (name, description, rules) right alongside the variable itself. - Installation as a First-Class Citizen: Game servers often need complex setup before they can run (downloading configurations and mods via SteamCMD, running installers, patching files). Eggs have a dedicated
script.install
section and a specific container image just for this purpose. Docker Compose focuses on defining the runtime stack; pre-run setup is usually baked into the Dockerfile, which is less flexible for user-specific variations needed at install time (like downloading different modpacks based on a variable). - Panel Needs Metadata: The Egg JSON includes extra bits just for the Panel – like how to parse
server.properties
(config.files
) or where logs are (config.logs
). This allows the Panel to offer integrated features like the file manager's text editor or log viewer. A standarddocker-compose.yml
doesn't have standardized fields for this Pterodactyl-specific info. - Admin Control Freak Mode (Security & Sanity): In a multi-tenant environment, you cannot let users just upload arbitrary Docker Compose files. That's asking for trouble (mounting host root, running crypto miners, network chaos). Eggs provide a curated template system. Admins control which Eggs are available, defining the approved base images, resource limits (via service options linked to the Egg), available variables, and install steps. It provides necessary guardrails.
- Shareable, Standardized Blueprints: The Egg format creates a common language within the Pterodactyl community for defining server types. It's easy to export, share, and import Eggs, fostering collaboration. So yeah, Docker Compose is great for developers defining apps. Eggs are a purpose-built abstraction layer on top of Docker, tailored for the specific needs of a user-friendly, secure, multi-tenant game server hosting platform. They prioritize the user and administrator experience over exposing the raw underlying container tech.
TL;DR: How Pterodactyl Works
- Wings: A Go daemon running on each game server node. It's basically a smart wrapper around Docker, controlled via an API.
- Docker Control: Wings uses the Docker API (via Go SDK) to start, stop, monitor resource usage, and manage the lifecycle of server containers based on commands from the Panel. No direct user interaction with Docker needed.
- File Management: Accessed through the Panel UI, but requests are proxied to the Wings API. Wings authenticates requests using JWTs from the Panel, checks permissions, and uses a sandboxed filesystem object to interact with server files securely.
- Eggs (Not Docker Compose): Pterodactyl uses JSON files called "Eggs" as server blueprints, not Docker Compose files.
- What: Eggs define the Docker image, installation script (for downloading game files/mods), startup command, configuration files (for Panel editing), stop commands, and user-configurable variables.
- Why: Eggs simplify things for users (no Docker knowledge needed), integrate tightly with the Panel UI (especially for variables), handle complex game server installations better, provide metadata the Panel needs, and give admins secure control over what users can deploy. Compose is too generic and lacks the specific structure/metadata needed for a user-friendly hosting panel.
- Panel: The central web interface (PHP/Laravel) where users and admins manage everything. It stores configuration in a database and tells the appropriate Wings instance what actions to perform (start server, list files, etc.) via authenticated API calls. It orchestrates, Wings executes.
P.S. If you want to talk more about ptero modding join my discord server. If you want me to make you your own pterodactyl panel clone or wrapper, join my development discord server