├── .gitignore ├── LICENSE ├── README.md ├── app.json ├── bin ├── compile ├── detect └── release ├── etc └── files.json ├── opt ├── index.rhtml ├── minecraft └── sync ├── requirements.txt ├── server.properties └── system.properties /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License: 2 | 3 | Copyright (C) 2015 Joe Kutner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heroku Minecraft Buildpack 2 | 3 | This is a [Heroku Buildpack](https://devcenter.heroku.com/articles/buildpacks) 4 | for running a Minecraft server in a [dyno](https://devcenter.heroku.com/articles/dynos). 5 | 6 | [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 7 | 8 | ## Usage 9 | 10 | Create a [free ngrok account](https://ngrok.com/) and copy your Auth token. Then create a new Git project with a `eula.txt` file: 11 | 12 | ```sh-session 13 | $ echo 'eula=true' > eula.txt 14 | $ git init 15 | $ git add eula.txt 16 | $ git commit -m "first commit" 17 | ``` 18 | 19 | Then, install the [Heroku CLI](https://cli.heroku.com/). 20 | Create a Heroku app, set your ngrok token, and push: 21 | 22 | ```sh-session 23 | $ heroku create 24 | $ heroku buildpacks:add heroku/python 25 | $ heroku buildpacks:add heroku/jvm 26 | $ heroku buildpacks:add jkutner/minecraft 27 | $ heroku config:set NGROK_API_TOKEN="xxxxx" 28 | $ git push heroku master 29 | ``` 30 | 31 | Finally, open the app: 32 | 33 | ```sh-session 34 | $ heroku open 35 | ``` 36 | 37 | This will display the ngrok logs, which will contain the name of the server 38 | (really it's a proxy, but whatever): 39 | 40 | ``` 41 | Server available at: 0.tcp.ngrok.io:17003 42 | ``` 43 | 44 | Copy the `0.tcp.ngrok.io:17003` part, and paste it into your local Minecraft app 45 | as the server name. 46 | 47 | ## Syncing to S3 48 | 49 | The Heroku filesystem is [ephemeral](https://devcenter.heroku.com/articles/dynos#ephemeral-filesystem), 50 | which means files written to the file system will be destroyed when the server is restarted. 51 | 52 | Minecraft keeps all of the data for the server in flat files on the file system. 53 | Thus, if you want to keep you world, you'll need to sync it to S3. 54 | 55 | First, create an [AWS account](https://aws.amazon.com/) and an S3 bucket. Then configure the bucket 56 | and your AWS keys like this: 57 | 58 | ``` 59 | $ heroku config:set AWS_BUCKET=your-bucket-name 60 | $ heroku config:set AWS_ACCESS_KEY=xxx 61 | $ heroku config:set AWS_SECRET_KEY=xxx 62 | ``` 63 | 64 | The buildpack will sync your world to the bucket every 60 seconds, but this is configurable by setting the `AWS_SYNC_INTERVAL` config var. 65 | 66 | ## Connecting to the server console 67 | 68 | The Minecraft server runs inside a `screen` session. You can use [Heroku Exec](https://devcenter.heroku.com/articles/heroku-exec) to connect to your server console. 69 | 70 | Once you have Heroku Exec installed, you can connect to the console using 71 | 72 | ``` 73 | $ heroku ps:exec 74 | Establishing credentials... done 75 | Connecting to web.1 on ⬢ lovely-minecraft-2351... 76 | $ screen -r minecraft 77 | ``` 78 | 79 | **WARNING** You are now connected to the Minecraft server. Use `Ctrl-A Ctrl-D` to exit the screen session. 80 | (If you hit `Ctrl-C` while in the session, you'll terminate the Minecraft server.) 81 | 82 | ## Customizing 83 | 84 | ### ngrok 85 | 86 | You can customize ngrok by setting the `NGROK_OPTS` config variable. For example: 87 | 88 | ``` 89 | $ heroku config:set NGROK_OPTS="--remote-addr 1.tcp.ngrok.io:25565" 90 | ``` 91 | 92 | ### Minecraft 93 | 94 | You can choose the Minecraft version by setting the MINECRAFT_VERSION like so: 95 | 96 | ``` 97 | $ heroku config:set MINECRAFT_VERSION="1.18.1" 98 | ``` 99 | 100 | You can also configure the server properties by creating a `server.properties` 101 | file in your project and adding it to Git. This is how you would set things like 102 | Creative mode and Hardcore difficulty. The various options available are 103 | described on the [Minecraft Wiki](http://minecraft.gamepedia.com/Server.properties). 104 | 105 | You can add files such as `banned-players.json`, `banned-ips.json`, `ops.json`, 106 | `whitelist.json` to your Git repository and the Minecraft server will pick them up. 107 | 108 | ### Adding New Minecraft Versions 109 | 110 | Please submit Pull Requests to [`etc/files.json`](https://github.com/jkutner/heroku-buildpack-minecraft/blob/master/etc/files.json) 111 | 112 | ### Using the Buildpack from source 113 | 114 | If you want the bleeding edge version of this buildpack run: 115 | 116 | ``` 117 | $ heroku buildpacks:remove jkutner/minecraft 118 | $ heroku buildpacks:add https://github.com/jkutner/heroku-buildpack-minecraft 119 | ``` 120 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Minecraft", 3 | "description": "A Minecraft Server on Heroku", 4 | "keywords": [ 5 | "games", 6 | "minecraft" 7 | ], 8 | "repository": "https://github.com/jkutner/heroku-buildpack-minecraft", 9 | "env": { 10 | "MINECRAFT_EULA": { 11 | "description": "Do you accept the Minecraft EULA?", 12 | "value": "true" 13 | }, 14 | "NGROK_API_TOKEN": { 15 | "description": "Your personal ngrok API token", 16 | "required": true 17 | }, 18 | "AWS_BUCKET": { 19 | "description": "S3 Bucket to store your server state (you will lose your games when the server restarts if you don't do this)", 20 | "required": false 21 | }, 22 | "AWS_ACCESS_KEY": { 23 | "description": "The AWS access key for the S3 Bucket", 24 | "required": false 25 | }, 26 | "AWS_SECRET_KEY": { 27 | "description": "The AWS secret key for the S3 Bucket", 28 | "required": false 29 | } 30 | }, 31 | "buildpacks": [ 32 | { 33 | "url": "heroku/python" 34 | }, 35 | { 36 | "url": "heroku/jvm" 37 | }, 38 | { 39 | "url": "heroku-community/inline" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /bin/compile: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | indent() { 6 | sed -u 's/^/ /' 7 | } 8 | 9 | export_env_dir() { 10 | env_dir=$1 11 | whitelist_regex=${2:-''} 12 | blacklist_regex=${3:-'^(PATH|GIT_DIR|CPATH|CPPATH|LD_PRELOAD|LIBRARY_PATH|JAVA_OPTS)$'} 13 | if [ -d "$env_dir" ]; then 14 | for e in $(ls $env_dir); do 15 | echo "$e" | grep -E "$whitelist_regex" | grep -qvE "$blacklist_regex" && 16 | export "$e=$(cat $env_dir/$e)" 17 | : 18 | done 19 | fi 20 | } 21 | 22 | BP_BIN_DIR="$(cd "$(dirname "$0")" && pwd)" 23 | BUILD_DIR=$1 24 | CACHE_DIR=$2 25 | OPT_DIR=$BP_BIN_DIR/../opt/ 26 | ETC_DIR=$BP_BIN_DIR/../etc/ 27 | 28 | export_env_dir $3 29 | 30 | APT_CACHE_DIR="$CACHE_DIR/apt/cache" 31 | APT_STATE_DIR="$CACHE_DIR/apt/state" 32 | APT_OPTIONS="-o debug::nolocking=true -o dir::cache=$APT_CACHE_DIR -o dir::state=$APT_STATE_DIR" 33 | 34 | mkdir -p "$APT_CACHE_DIR/archives/partial" 35 | mkdir -p "$APT_STATE_DIR/lists/partial" 36 | 37 | echo "-----> Installing screen... " 38 | apt-get $APT_OPTIONS update -y | indent 39 | apt-get $APT_OPTIONS -y --allow-downgrades --allow-remove-essential --allow-change-held-packages -d install --reinstall screen | indent 40 | mkdir -p $BUILD_DIR/.apt/var/run/screen 41 | 42 | mkdir -p $BUILD_DIR/.profile.d 43 | cat <$BUILD_DIR/.profile.d/000_apt.sh 44 | export PATH="\$HOME/.apt/usr/bin:\$PATH" 45 | export LD_LIBRARY_PATH="\$HOME/.apt/usr/lib/x86_64-linux-gnu:\$HOME/.apt/usr/lib/i386-linux-gnu:\$HOME/.apt/usr/lib:\$LD_LIBRARY_PATH" 46 | export LIBRARY_PATH="\$HOME/.apt/usr/lib/x86_64-linux-gnu:\$HOME/.apt/usr/lib/i386-linux-gnu:\$HOME/.apt/usr/lib:\$LIBRARY_PATH" 47 | export INCLUDE_PATH="\$HOME/.apt/usr/include:\$INCLUDE_PATH" 48 | export CPATH="\$INCLUDE_PATH" 49 | export CPPPATH="\$INCLUDE_PATH" 50 | export PKG_CONFIG_PATH="\$HOME/.apt/usr/lib/x86_64-linux-gnu/pkgconfig:\$HOME/.apt/usr/lib/i386-linux-gnu/pkgconfig:\$HOME/.apt/usr/lib/pkgconfig:\$PKG_CONFIG_PATH" 51 | export PYTHONPATH="\$HOME/.apt/usr/lib/python2.7/dist-packages" 52 | export SCREENDIR="\$HOME/.apt/var/run/screen" 53 | EOF 54 | 55 | for DEB in $(ls -1 $APT_CACHE_DIR/archives/*.deb); do 56 | dpkg -x $DEB $BUILD_DIR/.apt/ 57 | done 58 | 59 | echo -n "-----> Installing ngrok... " 60 | curl --silent -o ngrok.zip -L "https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip" | indent 61 | unzip ngrok.zip -d $BUILD_DIR/bin > /dev/null 2>&1 62 | echo "done" 63 | 64 | minecraft_version=${MINECRAFT_VERSION:-"1.18.1"} 65 | minecraft_url="$(cat $ETC_DIR/files.json | python -c "import json,sys;obj=json.load(sys.stdin);print(obj[\"server\"][\"${minecraft_version}\"][\"url\"])")" 66 | if [ -z "$minecraft_url" ]; then 67 | echo "Could not find URL for Minecraft version $minecraft_version. Please check files.json." 68 | exit 1 69 | fi 70 | 71 | echo -n "-----> Installing Minecraft ${minecraft_version}... " 72 | curl -o minecraft.jar -s -L $minecraft_url 73 | mv minecraft.jar $BUILD_DIR/minecraft.jar 74 | echo "done" 75 | 76 | if [ -n "${MINECRAFT_EULA:-""}" ]; then 77 | echo -n "-----> Accepting Minecraft EULA... " 78 | echo "eula=true" >> $BUILD_DIR/eula.txt 79 | echo "done" 80 | fi 81 | 82 | mkdir -p $BUILD_DIR/bin 83 | [ ! -f $BUILD_DIR/minecraft ] && cp $OPT_DIR/minecraft $BUILD_DIR/bin 84 | [ ! -f $BUILD_DIR/sync ] && cp $OPT_DIR/sync $BUILD_DIR/bin 85 | [ ! -f $BUILD_DIR/index.rhtml ] && cp $OPT_DIR/index.rhtml $BUILD_DIR 86 | 87 | chmod +x $BUILD_DIR/bin/minecraft 88 | chmod +x $BUILD_DIR/bin/sync 89 | -------------------------------------------------------------------------------- /bin/detect: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -f eula.txt ]; then 4 | echo "Minecraft" 5 | exit 0 6 | else 7 | exit 1 8 | fi 9 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cat < 2 | <% f.lines.select { |line| line.include?("URL:") }.each do |line| %> 3 | Server available at: 4 | <% if line.match(/tcp:\/\/(.+:[0-9]+) P/) %> 5 | <%= line.match(/tcp:\/\/(.+:[0-9]+) P/)[1] %> 6 | <% else %> 7 | <%= line.match(/tcp:\/\/(.+:[0-9]+)/)[1] %> 8 | <% end %> 9 |
10 | <% end %> 11 | <% end %> 12 | -------------------------------------------------------------------------------- /opt/minecraft: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | mc_port=25566 4 | port=${1:-${PORT:-8080}} 5 | 6 | if [ -z "$NGROK_API_TOKEN" ]; then 7 | echo "You must set the NGROK_API_TOKEN config var to create a TCP tunnel!" 8 | exit 1 9 | fi 10 | 11 | # Start the TCP tunnel 12 | ngrok_cmd="bin/ngrok tcp -authtoken $NGROK_API_TOKEN -log stdout --log-level debug ${NGROK_OPTS} ${mc_port}" 13 | echo "Starting ngrok..." 14 | eval "$ngrok_cmd | tee ngrok.log &" 15 | ngrok_pid=$! 16 | 17 | # Do an inline sync first, then start the background job 18 | echo "Starting sync..." 19 | bin/sync 20 | if [ "$READ_ONLY" != "true" ]; then 21 | eval "while true; do sleep ${AWS_SYNC_INTERVAL:-60}; bin/sync; done &" 22 | sync_pid=$! 23 | fi 24 | 25 | # create server config 26 | echo "server-port=${mc_port}" >> /app/server.properties 27 | for f in whitelist banned-players banned-ips ops; do 28 | test ! -f $f.json && echo -n "[]" > $f.json 29 | done 30 | 31 | limit=$(ulimit -u) 32 | case $limit in 33 | 512) # 2X Dyno 34 | heap="768m" 35 | ;; 36 | 32768) # PX Dyno 37 | heap="4g" 38 | ;; 39 | *) # 1X Dyno 40 | heap="384m" 41 | ;; 42 | esac 43 | 44 | echo "Starting: minecraft ${mc_port}" 45 | eval "screen -L -h 2048 -dmS minecraft java -Xmx${heap} -Xms${heap} -jar minecraft.jar nogui" 46 | main_pid=$! 47 | 48 | # Flush the logfile every second, and ensure that the logfile exists 49 | screen -X "logfile 1" && sleep 1 50 | 51 | echo "Tailing log" 52 | eval "tail -f screenlog.0 &" 53 | tail_pid=$! 54 | 55 | trap "kill $ngrok_pid $main_pid $sync_pid $tail_pid" SIGTERM 56 | trap "kill -9 $ngrok_pid $main_pid $sync_pid $tail_pid; exit" SIGKILL 57 | 58 | eval "ruby -rwebrick -e'WEBrick::HTTPServer.new(:BindAddress => \"0.0.0.0\", :Port => ${port}, :MimeTypes => {\"rhtml\" => \"text/html\"}, :DocumentRoot => Dir.pwd).start'" 59 | -------------------------------------------------------------------------------- /opt/sync: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | world_name="${WORLD_NAME:-default}" 4 | 5 | if [ -n "$AWS_BUCKET" ]; then 6 | cat << EOF > .s3cfg 7 | [default] 8 | access_key = ${AWS_ACCESS_KEY} 9 | secret_key = ${AWS_SECRET_KEY} 10 | EOF 11 | # note: this won't work if level-name is set in server.properties 12 | # todo: dynamically determine world/ dir 13 | if [ -d world ]; then 14 | s3cmd sync world/ s3://${AWS_BUCKET}/${WORLD_NAME}/world/ 15 | else 16 | mkdir -p world 17 | cd world 18 | s3cmd get --recursive s3://${AWS_BUCKET}/${WORLD_NAME}/world/ 19 | cd .. 20 | fi 21 | rm .s3cfg 22 | fi 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | s3cmd 2 | -------------------------------------------------------------------------------- /server.properties: -------------------------------------------------------------------------------- 1 | gamemode=1 2 | force-gamemode=true 3 | enforce-whitelist=false 4 | white-list=false 5 | -------------------------------------------------------------------------------- /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=17 2 | --------------------------------------------------------------------------------