Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite Creating Custom egg and Docker image #556

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
59 changes: 58 additions & 1 deletion community/config/eggs/creating_a_custom_egg.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,29 @@ can be tweaked per-server if needed).
_Docker images must be specifically designed to work with Pterodactyl Panel._ You should read more about that in
our [Creating a Docker Image](/community/config/eggs/creating_a_custom_image.md) guide.

## The Pterodactyl Install Procces

::: warning
Please be aware of how the pterodactyl install proces works!
QuintenQVD0 marked this conversation as resolved.
Show resolved Hide resolved
:::

```
1. Spin up install container
Creates a new container using an install image that's run as root.
QuintenQVD0 marked this conversation as resolved.
Show resolved Hide resolved
Uses a volume mount on `/mnt/server` for the server files, which is the working directory during installation.
The volume will be later mounted as `/home/container` for the server container. Any files outside of `/mnt/server` will be gone after installation.
Install script can pull files or set up all that is needed to run the server, such as writing files, directories or compiling apps.
QuintenQVD0 marked this conversation as resolved.
Show resolved Hide resolved
It is regularly used to just download the files required. Such as server files and configs.
QuintenQVD0 marked this conversation as resolved.
Show resolved Hide resolved

2. Stop and destroy install container

3. Start a new container with the server files in /home/container
This is where the server is actually run. No root privileges.
QuintenQVD0 marked this conversation as resolved.
Show resolved Hide resolved
Any dependencies installed during the install process are gone.
The container that is started should have everything you need.
No packages can be installed. Any required dependencies must exist in the used Docker image.
```

## Configure Process Management
This is perhaps the most important step in this service option configuration, as this tells the Daemon how to run everything.

Expand Down Expand Up @@ -80,6 +103,10 @@ Avoid using this parser if possible.
* `json` (supports `*` notation)
* `xml`

::: tip
If you want to use egg non stock variables in the configuration parser you must reference them as `{{server.build.env.ENVNAME}}` or just `{{env.ENVNAME}}`. Do not forget to to replace `ENVNAME` with the actual enviroment name you have setup.
QuintenQVD0 marked this conversation as resolved.
Show resolved Hide resolved
:::

Once you have defined a parser, we then define a `find` block which tells the Daemon what specific elements to find
and replace. In this example, we have provided four separate items within the `server.properties` file that we want to
find and replace to the assigned values. You can use either an exact value, or define a specific server setting from
Expand Down Expand Up @@ -116,6 +143,26 @@ single matching line. In this case, we are looking for either `127.0.0.1` or `lo
docker interface defined in the configuration file using `{{config.docker.interface}}`.
:::

#### File Parser
The file parser the whole line that you are trying to edit. For example:
QuintenQVD0 marked this conversation as resolved.
Show resolved Hide resolved

```json
{
"main/server.cfg": {
"parser": "file",
"find": {
"seta sv_hostname": "seta sv_hostname \"{{env.SERVER_NAME}}\"",
"seta sv_maxClients": "seta sv_maxClients \"{{env.SERVER_MAXCLIENTS}}\"",
"seta rconPassword": "seta rconPassword \"{{env.RCON_PASSWORD}}\"",
"seta g_password": "seta g_password \"{{env.SERVER_PASSWORD}}\"",
"Map": "Map {{env.SERVER_MAP}}"
}
}
}
```

The `"` on the right side are escaped with a `\` because else they would brake the json syntax for the parser.
QuintenQVD0 marked this conversation as resolved.
Show resolved Hide resolved

### Start Configuration
The last block to configure is the `Start Configuration` for servers running using this service option.

Expand All @@ -127,7 +174,17 @@ The last block to configure is the `Start Configuration` for servers running usi

In the example block above, we define `done` as the entire line, or part of a line that indicates a server is done
starting, and is ready for players to join. When the Daemon sees this output, it will mark the server as `ON` rather
than `STARTING`.
than `STARTING`.

If your aplication has multiple messages that mean that it is fully startup then you can also do it like this:
QuintenQVD0 marked this conversation as resolved.
Show resolved Hide resolved
```json
{
"done":[
"change this text 1",
"change this text 2"
]
}
```

That concludes basic service option configuration.

Expand Down
122 changes: 78 additions & 44 deletions community/config/eggs/creating_a_custom_image.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[[toc]]

::: warning
This tutorial uses examples from our [`core:java`](https://github.com/pterodactyl/images/tree/java) docker image,
This tutorial uses examples from our [`yolks:java_17`](https://github.com/pterodactyl/yolks/tree/master/java/17) docker image,
which can be found on GitHub. This tutorial also assumes some knowledge of [Docker](https://docker.io/), we suggest
reading up if this all looks foreign to you.
:::
Expand All @@ -13,52 +13,62 @@ reading up if this all looks foreign to you.
The most important part of this process is to create the [`Dockerfile`](https://docs.docker.com/engine/reference/builder/)
that will be used by the Daemon. Due to heavy restrictions on server containers, you must setup this file in a specific manner.

We try to make use of [Alpine Linux](https://alpinelinux.org) as much as possible for our images in order to keep their size down.
In most images we try to use a [Debian based OS](https://www.debian.org) as much as possible for our images.
QuintenQVD0 marked this conversation as resolved.
Show resolved Hide resolved

```bash
# ----------------------------------
# Pterodactyl Core Dockerfile
# Environment: Java
# Minimum Panel Version: 0.6.0
# ----------------------------------
FROM openjdk:8-jdk-alpine
FROM --platform=$TARGETOS/$TARGETARCH eclipse-temurin:17-jdk-jammy

MAINTAINER Pterodactyl Software, <support@pterodactyl.io>
LABEL author="Matthew Penner" maintainer="matthew@pterodactyl.io"

RUN apk add --no-cache --update curl ca-certificates openssl git tar bash sqlite fontconfig \
&& adduser --disabled-password --home /home/container container
LABEL org.opencontainers.image.source="https://github.com/pterodactyl/yolks"
LABEL org.opencontainers.image.licenses=MIT

USER container
ENV USER=container HOME=/home/container
RUN apt-get update -y \
&& apt-get install -y lsof curl ca-certificates openssl git tar sqlite3 fontconfig libfreetype6 tzdata iproute2 libstdc++6 \
&& useradd -d /home/container -m container

WORKDIR /home/container
USER container
ENV USER=container HOME=/home/container
WORKDIR /home/container

COPY ./entrypoint.sh /entrypoint.sh

CMD ["/bin/bash", "/entrypoint.sh"]
COPY ./../entrypoint.sh /entrypoint.sh
CMD [ "/bin/bash", "/entrypoint.sh" ]
```

Lets walk through the `Dockerfile` above. The first thing you'll notice is the [`FROM`](https://docs.docker.com/engine/reference/builder/#from) declaration.

```bash
FROM openjdk:8-jdk-alpine
FROM --platform=$TARGETOS/$TARGETARCH eclipse-temurin:17-jdk-jammy
```

In this case, we are using [`openjdk:8-jdk-alpine`](https://github.com/docker-library/openjdk) which provides us with Java 8.
The `--platform=$TARGETOS/$TARGETARCH` allows us to specify in the github workflow that we want to build for linux/amd64 and linux/arm64. See [Docker docs](https://docs.docker.com/engine/reference/builder/#from)

In this case, we are using [`eclipse-temurin:17-jdk-jammy`](https://github.com/adoptium/containers/tree/main) which provides us with Java 17.

## Installing Dependencies

The next thing we do is install the dependencies we will need using Alpine's package manager: `apk`. You'll notice some
specific flags that keep the container small, including `--no-cache`, as well as everything being contained in a
single [`RUN`](https://docs.docker.com/engine/reference/builder/#run) block.
The next thing we do is install the dependencies we will need using Debian/Ubuntu's package manager: `apt`. You'll notice some
specific flags `-y` as the docker build is non interactive, as well as everything being contained in a
single [`RUN`](https://docs.docker.com/engine/reference/builder/#run) block.

::: warning
The dependencie `iproute2` is required in every docker container to make the ip command work
QuintenQVD0 marked this conversation as resolved.
Show resolved Hide resolved
:::

## Files In The Docker Image
::: warning
Because the way that Pterodactyl works no files can be placed in the docker container in `/home/container`.
QuintenQVD0 marked this conversation as resolved.
Show resolved Hide resolved
:::

All files must be downloaded with the egg install script, this means for example that you can not put your bot files or minecraft server jar can not be put in the image as you can with regular docker images
QuintenQVD0 marked this conversation as resolved.
Show resolved Hide resolved

## Creating a Container User

Within this `RUN` block, you'll notice the `useradd` command.

```bash
adduser -D -h /home/container container
```
useradd -d /home/container -m container
```

::: warning
All Pterodactyl containers must have a user named `container`, and the user home **must** be `/home/container`.
Expand All @@ -79,8 +89,8 @@ we define the command to be used when the container is started using [`CMD`](htt
The `CMD` line should always point to the `entrypoint.sh` file.

```bash
COPY ./entrypoint.sh /entrypoint.sh
CMD ["/bin/bash", "/entrypoint.sh"]
COPY ./../entrypoint.sh /entrypoint.sh
CMD [ "/bin/bash", "/entrypoint.sh" ]
```

## Entrypoint Script
Expand All @@ -92,21 +102,46 @@ These entrypoint files are actually fairly abstracted, and the Daemon will pass
variable before processing it and then executing the command.

```bash
#!/bin/bash
cd /home/container

# Output Current Java Version
java -version ## only really needed to show what version is being used. Should be changed for different applications
# Default the TZ environment variable to UTC.
TZ=${TZ:-UTC}
export TZ

# Set environment variable that holds the Internal Docker IP
INTERNAL_IP=$(ip route get 1 | awk '{print $(NF-2);exit}')
export INTERNAL_IP

# Switch to the container's working directory
cd /home/container || exit 1

# Print Java version
printf "\033[1m\033[33mcontainer@pterodactyl~ \033[0mjava -version\n"
java -version

# Convert all of the "{{VARIABLE}}" parts of the command into the expected shell
# variable format of "${VARIABLE}" before evaluating the string and automatically
# replacing the values.
PARSED=$(echo "${STARTUP}" | sed -e 's/{{/${/g' -e 's/}}/}/g' | eval echo "$(cat -)")

# Display the command we're running in the output, and then execute it with the env
# from the container itself.
printf "\033[1m\033[33mcontainer@pterodactyl~ \033[0m%s\n" "$PARSED"
# shellcheck disable=SC2086
exec env ${PARSED}
```

# Replace Startup Variables
MODIFIED_STARTUP=`eval echo $(echo ${STARTUP} | sed -e 's/{{/${/g' -e 's/}}/}/g')`
echo ":/home/container$ ${MODIFIED_STARTUP}"
First we set the timezone.
```bash
TZ=${TZ:-UTC}
export TZ
```

# Run the Server
${MODIFIED_STARTUP}
Then we make the internal ip avaible in the docker container.
```bash
INTERNAL_IP=$(ip route get 1 | awk '{print $(NF-2);exit}')
export INTERNAL_IP
```

The second command, `cd /home/container`, simply ensures we are in the correct directory when running the rest of the
The third command, `cd /home/container`, simply ensures we are in the correct directory when running the rest of the
commands. We then follow that up with `java -version` to output this information to end-users, but that is not necessary.

## Modifying the Startup Command
Expand All @@ -116,24 +151,23 @@ is parsing the environment `STARTUP` that is passed into the container by the Da
looks something like the example below:

```bash
STARTUP="java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}"
STARTUP="java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}"
```

::: v-pre
You'll notice some placeholders there, specifically `{{SERVER_MEMORY}}` and `{{SERVER_JARFILE}}`. These both refer to
You'll notice some placeholders there, specifically `{{SERVER_JARFILE}}`. These refer to
other environment variables being passed in, and they look something like the example below.
:::

```bash
SERVER_MEMORY=1024
SERVER_JARFILE=server.jar
```

There are a host of different environment variables, and they change depending on the specific service option
configuration. However, that is not necessarily anything to worry about here.

```bash
MODIFIED_STARTUP=`eval echo $(echo ${STARTUP} | sed -e 's/{{/${/g' -e 's/}}/}/g')`
PARSED=$(echo "${STARTUP}" | sed -e 's/{{/${/g' -e 's/}}/}/g' | eval echo "$(cat -)")
```

::: v-pre
Expand All @@ -142,18 +176,18 @@ curly braces `{{EXAMPLE}}` with a matching environment variable (such as `EXAMPL
:::

```bash
java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}
java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}
```

Becomes:

```bash
java -Xms128M -Xmx1024M -jar server.jar
java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}} server.jar
QuintenQVD0 marked this conversation as resolved.
Show resolved Hide resolved
```

## Run the Command

The last step is to run this modified startup command, which is done with the line `${MODIFIED_STARTUP}`.
The last step is to run this modified startup command, which is done with the line `exec env ${PARSED}`.

### Note

Expand Down
Loading