Add install.sh: install or upgrade Megalodon as a systemd service
CI / test (push) Successful in 2m15s

Downloads the release for this machine and verifies its checksum (or builds
from source), creates the service user and folders, writes and opens the
environment file, installs the hardened unit and prints the first-run admin
password. Re-running upgrades; --uninstall [--purge] removes it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-10-31 00:21:53 -06:00
1 parent 0d2cb31abf
commit 81e3f73521
2 files changed
+239 -1

No files matched your search

+16 -1
View File
@@ -59,7 +59,22 @@ password you set there is the RCON password.
admin password. Keep it on your LAN or a VPN; only Megalodon needs to reach it. If Megalodon and
the server are on the same machine or Docker network, nothing needs to be opened at all.
## Running it
## Installing on Linux (systemd)
```sh
curl -fsSL https://git.veillax.com/Veillax/megalodon/raw/branch/main/deploy/install.sh | sudo sh
```
This downloads the latest release (checking its SHA-256), installs `/usr/local/bin/megalodon`,
creates a `megalodon` user, `/etc/megalodon/` (settings) and `/var/lib/megalodon/` (data), opens
`/etc/megalodon/megalodon.env` in your editor, then starts the service and prints the first-run
admin password. Run it again to upgrade; your settings are kept.
Options: `--version v0.1.0`, `--listen 0.0.0.0:8480`, `--source` (build from the repository with Go
instead), `--no-edit`, `--no-start`, and `--uninstall` (add `--purge` to delete settings and data).
Download the script and read it first if you prefer: it is [`deploy/install.sh`](deploy/install.sh).
## Running it by hand
```sh
# download a release, or: go build ./cmd/megalodon
+223
View File
@@ -0,0 +1,223 @@
#!/bin/sh
# Installs (or upgrades) Megalodon as a systemd service.
#
# curl -fsSL https://git.veillax.com/Veillax/megalodon/raw/branch/main/deploy/install.sh | sudo sh
# sudo sh install.sh [--version v0.1.0] [--listen 127.0.0.1:8480] [--source] [--no-edit] [--no-start]
# sudo sh install.sh --uninstall [--purge]
#
# What it does:
# 1. downloads the release archive for this machine and checks its SHA-256 (or, with --source,
# clones the repository and builds it with Go),
# 2. installs the binary to /usr/local/bin/megalodon,
# 3. creates a "megalodon" user, /etc/megalodon (settings) and /var/lib/megalodon (data),
# 4. writes /etc/megalodon/megalodon.env and opens it in your editor,
# 5. installs and starts the systemd service, and prints the first-run admin password.
#
# Running it again upgrades the binary and keeps your settings.
set -eu
REPO_URL=${MEGALODON_REPO_URL:-https://git.veillax.com/Veillax/megalodon}
API_URL=${MEGALODON_API_URL:-https://git.veillax.com/api/v1/repos/Veillax/megalodon}
BIN=/usr/local/bin/megalodon
CONF_DIR=/etc/megalodon
DATA_DIR=/var/lib/megalodon
ENV_FILE=$CONF_DIR/megalodon.env
CONF_FILE=$CONF_DIR/megalodon.json
UNIT=/etc/systemd/system/megalodon.service
USER_NAME=megalodon
VERSION=""
LISTEN="127.0.0.1:8480"
SOURCE=0
EDIT=1
START=1
UNINSTALL=0
PURGE=0
say() { printf '%s\n' "$*"; }
die() { printf 'install.sh: %s\n' "$*" >&2; exit 1; }
while [ $# -gt 0 ]; do
case $1 in
--version) VERSION=${2:?--version needs a value}; shift ;;
--listen) LISTEN=${2:?--listen needs a value}; shift ;;
--source) SOURCE=1 ;;
--no-edit) EDIT=0 ;;
--no-start) START=0 ;;
--uninstall) UNINSTALL=1 ;;
--purge) PURGE=1 ;;
-h|--help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) die "unknown option $1" ;;
esac
shift
done
[ "$(id -u)" -eq 0 ] || die "run as root (sudo sh install.sh)"
[ "$(uname -s)" = Linux ] || die "this installer is for Linux; on Windows, download the .zip from the releases page"
command -v systemctl >/dev/null 2>&1 || HAVE_SYSTEMD=0
HAVE_SYSTEMD=${HAVE_SYSTEMD:-1}
if [ "$UNINSTALL" -eq 1 ]; then
if [ "$HAVE_SYSTEMD" -eq 1 ]; then
systemctl disable --now megalodon 2>/dev/null || true
rm -f "$UNIT"
systemctl daemon-reload
fi
rm -f "$BIN"
if [ "$PURGE" -eq 1 ]; then
rm -rf "$CONF_DIR" "$DATA_DIR"
userdel "$USER_NAME" 2>/dev/null || true
say "Megalodon removed, including its settings and data."
else
say "Megalodon removed. Settings ($CONF_DIR) and data ($DATA_DIR) were kept; add --purge to delete them."
fi
exit 0
fi
fetch() { # fetch URL DEST
if command -v curl >/dev/null 2>&1; then curl -fsSL -o "$2" "$1"
elif command -v wget >/dev/null 2>&1; then wget -qO "$2" "$1"
else die "need curl or wget"; fi
}
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
# ---- 1. get the binary ----
if [ "$SOURCE" -eq 1 ]; then
command -v git >/dev/null 2>&1 || die "--source needs git"
command -v go >/dev/null 2>&1 || die "--source needs Go (https://go.dev/dl/)"
say "Cloning $REPO_URL ..."
git clone --quiet ${VERSION:+--branch "$VERSION"} --depth 1 "$REPO_URL" "$TMP/src"
(cd "$TMP/src" && CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=${VERSION:-source}" -o "$TMP/megalodon" ./cmd/megalodon)
else
case $(uname -m) in
x86_64|amd64) ARCH=amd64 ;;
aarch64|arm64) ARCH=arm64 ;;
*) die "no release build for $(uname -m); try --source" ;;
esac
if [ -z "$VERSION" ]; then
fetch "$API_URL/releases/latest" "$TMP/latest.json"
VERSION=$(sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$TMP/latest.json" | head -n 1)
[ -n "$VERSION" ] || die "could not find the latest release; use --version or --source"
fi
ARCHIVE="megalodon_${VERSION}_linux_${ARCH}.tar.gz"
say "Downloading Megalodon $VERSION ($ARCH) ..."
fetch "$REPO_URL/releases/download/$VERSION/$ARCHIVE" "$TMP/$ARCHIVE"
fetch "$REPO_URL/releases/download/$VERSION/SHA256SUMS" "$TMP/SHA256SUMS"
want=$(grep " $ARCHIVE\$" "$TMP/SHA256SUMS" | cut -d' ' -f1)
[ -n "$want" ] || die "$ARCHIVE is not listed in SHA256SUMS"
got=$(sha256sum "$TMP/$ARCHIVE" | cut -d' ' -f1)
[ "$want" = "$got" ] || die "checksum mismatch for $ARCHIVE; refusing to install"
tar -xzf "$TMP/$ARCHIVE" -C "$TMP"
cp "$TMP/megalodon_${VERSION}_linux_${ARCH}/megalodon" "$TMP/megalodon"
fi
# ---- 2. install it ----
UPGRADE=0
[ -x "$BIN" ] && UPGRADE=1
if [ "$UPGRADE" -eq 1 ] && [ "$HAVE_SYSTEMD" -eq 1 ]; then systemctl stop megalodon 2>/dev/null || true; fi
install -m 0755 "$TMP/megalodon" "$BIN"
say "Installed $("$BIN" version) to $BIN"
# ---- 3. user and folders ----
id "$USER_NAME" >/dev/null 2>&1 || useradd --system --home-dir "$DATA_DIR" --shell /usr/sbin/nologin "$USER_NAME" 2>/dev/null \
|| adduser --system --home "$DATA_DIR" --shell /usr/sbin/nologin "$USER_NAME" 2>/dev/null \
|| die "could not create the $USER_NAME user"
# The dashboard rewrites its settings file, so the service must own the folder that holds it.
mkdir -p "$CONF_DIR" "$DATA_DIR"
chown "$USER_NAME" "$CONF_DIR" "$DATA_DIR"
chmod 0750 "$CONF_DIR" "$DATA_DIR"
# ---- 4. settings and environment file ----
if [ ! -f "$CONF_FILE" ]; then
printf '{\n "listen": "%s",\n "data_dir": "%s"\n}\n' "$LISTEN" "$DATA_DIR" > "$CONF_FILE"
chown "$USER_NAME" "$CONF_FILE"
chmod 0600 "$CONF_FILE"
fi
if [ ! -f "$ENV_FILE" ]; then
cat > "$ENV_FILE" <<EOF
# Environment for the megalodon service. Restart it after editing: systemctl restart megalodon
# Where the settings file is. The dashboard edits this file, so leave it where the service can write.
MEGALODON_CONFIG=$CONF_FILE
# The time zone used by scheduled jobs ("04:30" means 04:30 in this zone). Examples: Europe/London,
# America/New_York, UTC. Leave it commented to use the server's own zone.
#TZ=Europe/London
EOF
chmod 0640 "$ENV_FILE"
chgrp "$USER_NAME" "$ENV_FILE" 2>/dev/null || true
fi
if [ "$EDIT" -eq 1 ] && [ -r /dev/tty ]; then
ED=${VISUAL:-${EDITOR:-}}
[ -n "$ED" ] || for c in nano vim vi; do command -v "$c" >/dev/null 2>&1 && { ED=$c; break; }; done
if [ -n "$ED" ]; then
say "Opening $ENV_FILE in $ED ..."
$ED "$ENV_FILE" </dev/tty >/dev/tty || say "(the editor exited with an error; continuing)"
fi
fi
# ---- 5. service ----
if [ "$HAVE_SYSTEMD" -eq 0 ]; then
say "systemd was not found, so no service was installed. Run it yourself with:"
say " sudo -u $USER_NAME env \$(grep -v '^#' $ENV_FILE | xargs) $BIN serve -config $CONF_FILE"
exit 0
fi
cat > "$UNIT" <<EOF
[Unit]
Description=Megalodon, ARK server management over RCON
After=network-online.target
Wants=network-online.target
[Service]
User=$USER_NAME
EnvironmentFile=$ENV_FILE
WorkingDirectory=$DATA_DIR
ExecStart=$BIN serve
Restart=on-failure
RestartSec=5
# Megalodon only needs the network and its own folders.
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=$CONF_DIR $DATA_DIR
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable megalodon >/dev/null 2>&1
if [ "$START" -eq 1 ]; then
systemctl restart megalodon
sleep 3
if systemctl is-active --quiet megalodon; then
say "Megalodon is running."
else
say "Megalodon did not start. Look at: journalctl -u megalodon -n 50"
exit 1
fi
pw=$(journalctl -u megalodon --no-pager 2>/dev/null | sed -n 's/.*first run: created user "admin" with password \([^ ]*\).*/\1/p' | tail -n 1)
if [ "$UPGRADE" -eq 0 ] && [ -n "$pw" ]; then
say ""
say " Sign in as: admin"
say " Password: $pw (shown once; change it under Settings > Users)"
fi
say ""
say "Dashboard: http://$LISTEN"
say "Logs: journalctl -u megalodon -f"
say "Settings: $CONF_FILE and $ENV_FILE"
case $LISTEN in
127.*|localhost*|\[::1\]*) say "It only listens on this machine. To reach it elsewhere, put an HTTPS reverse proxy (or Hammerhead) in front of it." ;;
esac
fi