오라클 클라우드로 팰월드 24시간 서버를 여는 방법에 대해 서술한 글이다.
오라클 클라우드의 무료 티어 인스턴스를 사용하였다.
무료 티어 기준 오라클 클라우드는 구글 클라우드 서버(GCP)보다 아주 높은 사양을 제공해준다. 특히 GCP로 팰월드를 돌리기 위해서는 3개월 간 무료로 주는 300달러를 사용해서 사양을 맞춰야 한다. 따라서 3개월 이후에는 새로운 계정으로 다시 서버를 열어야 과금이 되지 않는다. 하지만 오라클 클라우드는 24GB의 RAM(GCP는 무료 티어일 경우 1GB RAM이다)을 주면서도 평생 무료이다. 따라서 게임 24시간 서버를 열려고 할 때 아주 유용하다. 오늘은 해당 오라클 클라우드를 통해서 24시간 팰월드 서버를 열어보도록 하겠다. 또한, 디스코드와도 연동하여 서버 정보를 쉽게 볼 수 있도록 해보겠다.
무료 티어 인스턴스 생성 방법은 아래 글에 적어놓았다. cd /mc_server 부분까지 따라하면 된다. 방화벽 설정은 따라하지 않아도 된다. 팰월드에 맞는 방화벽 설정을 해야 하기 때문이다.
https://tramp-long.tistory.com/56
[마인크래프트] 24시간 마인크래프트(Fabric) 서버 열기
24시간 마인크래프트 서버를 여는 방법에 대해 서술한 글이다오라클 클라우드의 무료 티어 인스턴스를 사용하였다. 24시간 마인크래프트 서버를 열어보기 위해 오라클 클라우드에 가입을 진행
tramp-long.tistory.com
방화벽 설정을 해주자.

생성한 인스턴스 이름을 클릭하여 상세 정보로 들어가자.

서브넷 이름을 눌러주자.

Default Security List를 누르자.

보안 규칙 탭에 있는 수신 규칙을 추가해준다.

이와 같이 추가한다.
이제 해당 인스턴스에 접속하여 명령어를 쳐주자. 이 또한 위에 첨부한 글에 설명되어 있다. 글에서 cd /mc_server 부분까지 따라왔다는 가정하에 이뤄진다. 또한, 우리는 도커라는 서비스를 사용해야 하기 때문에 인스턴스에 도커를 설치하자. 잘 정리된 글이 있어 첨부한다.
https://codingdrive.tistory.com/34
우분투 24.04 LTS 서버 Docker 설치
우분투 24.04 LTS(Noble Numbat) 서버에서 Docker 엔진을 설치하는 가장 권장되는 방법은 Docker 공식 리포지토리(Repository)를 사용하는 것입니다. 이 방법은 향후 업데이트를 apt 명령어로 편리하게 관리할
codingdrive.tistory.com
mkdir Palworld && cd Palworld
우리가 본격적으로 작업할 디렉터리를 생성한 뒤 해당 디렉터리로 이동한다.
mkdir palworld && chmod 777 palworld
palworld 디렉터리를 생성한 후 모든 유저에게 읽기, 쓰기, 실행 권한을 부여한다.
vi compose.yaml
compose.yaml 파일을 생성한다. i를 눌러 쓰기 모드로 변경한 후 아래 코드를 복사 붙여넣기 한다.
services:
palworld:
image: thijsvanloef/palworld-server-docker:latest
restart: unless-stopped
container_name: palworld-server
stop_grace_period: 30s
ports:
- 8211:8211/udp
- 8212:8212/tcp
environment:
- PUID=1000
- PGID=1000
- PORT=8211
- PLAYERS=8
- ENABLE_PERF_THREADING_ARGS=true
- WORKER_THREADS_SERVER=4
- REST_API_ENABLED=true
- REST_API_PORT=8212
- TZ=Asia/Seoul
- SERVER_NAME=서버 이름
- SERVER_PASSWORD=원하는비번
- ADMIN_PASSWORD=관리자비번
- COMMUNITY=false
- AUTO_UPDATE_ENABLED=false
- USE_DEPOT_DOWNLOADER=true
volumes:
- ./palworld:/palworld/
이후 Esc-:wq를 입력하여 저장 후 닫기를 한다. 참고로 WORKER_THREADS_SERVER의 경우에는 서버의 cpu 코어 수에 맞게 설정해주면 된다.
docker compose는 이 서버를 어떻게 띄울지를 yaml 파일에 적어두고 명령 한 줄로 실행하는 도구다. 그렇다면 docker는 무엇일까? docker는 앱이랑 그 앱이 돌아가는 데 필요한 모든 것(런타임, 라이브러리, 설정)을 하나의 상자에 통째로 담아서 어디서든 똑같이 실행되게 하는 기술이다. 서버 설정을 한 번에 하는 도구라고 쉽게 생각하자.
추가적인 환경 설정 변수의 경우 아래 링크에서 볼 수 있다.
https://github.com/thijsvanloef/palworld-server-docker/blob/main/.env.example
palworld-server-docker/.env.example at main · thijsvanloef/palworld-server-docker
A Docker Container to easily run a Palworld dedicated server. - thijsvanloef/palworld-server-docker
github.com
docker compose up -d
도커 컴포즈 파일을 올린다. docker compose ps를 쳤을 때 상태가 UP이면 정상적으로 올라간 것이다. docker compose logs -f로 로그를 볼 수 있다. Ctrl+C로 로그를 그만 볼 수 있다.
docker compose up -d # 서버 실행 (백그라운드)
docker compose down # 서버 정지 + 컨테이너 제거 (세이브는 볼륨에 남음)
docker compose ps # 지금 떠 있는 컨테이너 상태
docker compose logs -f # 실시간 로그 보기 (Ctrl+C로 나감)
docker compose restart # 재시작
docker compose pull # 이미지 최신 버전 다시 받기
알아두면 좋은 도커 명령어들이다. 본인 상황에 맞게 알아서 쓰자.
이제 팰월드를 킨 후 <오라클_공인IP>:8211로 접속하면 성공이다.
밑에 글은 편의성을 위한 글이다.
서버 자동 재시작
팰월드 서버는 시간이 지날수록 램 사용량이 올라가는 문제가 있다고 한다. 따라서 램이 가득차 크래시로 갑자기 서버가 죽는 문제를 방지하기 위해 새벽에 자동 재시작을 하도록 설정해보자.
원래는 크론잡을 이용하였으나 현재 받고 있는 도커 이미지에서 설정이 가능하므로 compose.yaml에서 설정하는 방식으로 하자. 크론잡 파트는 참고상 삭제하지 않았다. compose.yaml 방식만 따라 해주면 된다.
compose.yaml 방식
vi compose.yaml
compose.yaml 파일을 연다. i를 눌러 편집 모드로 들어간다.
environment:
- AUTO_REBOOT_ENABLED=true
- AUTO_REBOOT_CRON_EXPRESSION=0 5 * * *
- AUTO_REBOOT_EVEN_IF_PLAYERS_ONLINE=false
- AUTO_REBOOT_WARN_MINUTES=5
environment 부분에 해당 값을 추가해준다. 차례대로 자동 재시작 설정, 자동 재시작 시간(현재 새벽 5시), 사람 있으면 재시작 스킵, 5분 전 게임 내 경고 설정이다. Esc-:wq를 입력해서 저장 후 닫아주자.
docker compose down
도커 컨테이너를 중지하자.
docker compose up -d
도커 컨테이너를 다시 올리자.
docker compose logs palworld | grep -i reboot
서버가 실행된 후 해당 명령어를 통해 잘 적용되었는지 확인해보자. Adding cronjob for auto rebooting via REST API가 뜬다면 성공한 것이다.
크론잡 이용 (현재 사용 X, 참고용)
crontab -e
crontab 편집기를 열자. crontab이란 리눅스의 예약 작업 목록으로 언제 무슨 명령을 자동 실행할지 적을 수 있다. 처음에 열면 편집기를 고르라고 나오는데 1을 누르고 엔터를 하자. (nano 편집기가 쉽다.)
그러면 이제 뭐가 잔뜩 적힌 텍스트가 뜰 텐데 맨 아래에 해당 글을 추가하자.
CRON_TZ=Asia/Seoul
0 5 * * * cd /mc_server/Palworld && /usr/bin/docker compose restart palworld
CRON_TZ=Asia/Seoul는 한국 시간대로 설정한다는 뜻이다. 0 5 * * *는 분, 시, 일, 월, 요일 순이다. 따라서 해당 텍스트는 매일 새벽 5시가 된다. 그 뒤는 동작할 명령어이다. cd /mc_server/Palworld 는 Palworld 디렉터리로 이동하라는 뜻이다. 본인이 서버를 생성한 폴더로 지정하면 된다. 그 뒤는 컨테이너를 재시작하는 명령어이다.
저장은 Ctrl+O → Enter → Ctrl+X를 하면 된다.

이런 식으로 아래 넣으면 된다.
서버 업데이트
tar -czf ~/palworld_save_before_update_$(date +%Y%m%d_%H%M).tar.gz \
-C /mc_server/Palworld/palworld/Pal/Saved .
서버 업데이트를 하기 전에 세이브 파일을 백업하자.
ls -lh ~/palworld_save_before_update_*.tar.gz
파일이 뜬다면 백업 파일이 성공적으로 생성된 것이다.
docker compose down
업데이트를 위해 서버를 종료하자.
docker compose pull
도커 이미지를 최신으로 받아오자.
vi compose.yaml
업데이트를 위해 AUTO_UPDATE_ENABLED=false → true로 변경(i 눌러서 입력 모드 들어간 후 변경)한 후 저장(Esc-:wq 입력 후 엔터)하자.
docker compose up -d
서버를 다시 올리자.
docker compose logs -f palworld
서버 로그를 봤을 때, Running Palworld dedicated server on :8211 이 뜨면 성공한 것이다. Ctrl+C로 로그 모드에서 나올 수 있다.
vi compose.yaml
# AUTO_UPDATE_ENABLED=true → false 로 다시
docker compose up -d
이건 선택인데 업데이트 끝나면 AUTO_UPDATE_ENABLED를 다시 false로 돌려놓는 게 좋다. 안 그러면 매번 재시작(새벽 5시 크론 포함)마다 업데이트 체크를 하면서 부팅이 느려지고, 원치 않는 시점에 업데이트될 수 있다.
디스코드 연동
쉽게 서버 정보를 보게 하기 위해 디스코드를 연동해보겠다.
먼저 웹 훅 URL 부터 만들자. 디스코드 서버를 만든 후 서버 설정-연동-웹후크-새 웹후크로 만든 후 만든 웹후크의 URL을 복사하자.
이후 아까 만들었던 compose.yaml에 추가하자.
vi compose.yaml
텍스트 편집기를 열어준다. i를 누른 후 environment: 목록 안에 해당 줄을 추가한다. COMMUNITY=false 밑이라고 생각하면 된다.
- DISCORD_WEBHOOK_URL=여기에_복사한_웹훅_URL_붙여넣기
- DISCORD_CONNECT_TIMEOUT=30
- DISCORD_PRE_UPDATE_BOOT_MESSAGE=서버 시작 중...
- DISCORD_POST_UPDATE_BOOT_MESSAGE=서버 온라인!
Esc - :wq를 눌러 저장 후 닫자.
docker compose down
docker compose up -d
docker compose logs -f
해당 내용을 적용하기 위해 컨터이너를 재시작한다.
이제 단순한 서버 open/close 알람이 뜰 것이다. 이제 Palbot 셀프호스팅으로 관리 명령어를 붙이자.
그러기 위해서는 봇을 생성하고 토큰을 발급받는 것이 필요하다.
https://discord.com/developers/home
Discord for Developers
Build games, experiences, and integrations for millions of users on Discord.
discord.com

애플리케이션을 눌러 신규 애플리케이션을 누른다.

봇을 누른 후 토큰 초기화 버튼을 눌러 토큰을 발급받는다. 해당 토큰은 어딘가에다가 저장해놓자. 이따 사용해야 한다.

아래 쪽에 해당 옵션을 켜주자.

이제 OAuth2로 들어가 해당 옵션을 클릭하자.

원하는 봇 권한을 체크해준다. 귀찮다면 관리자 권한을 줘도 되며 필자는 최소 권한을 주었다. 아래쪽에 생성된 URL이 있을 것인데 해당 URL에 접속하여 원하는 디스코드 서버에 해당 봇을 추가해준다.
이제 해당 봇을 서버와 연동해보자.
mkdir palbot && cd palbot
Palworld 폴더 안에 palbot이라는 디스코드 봇을 위한 디렉터리를 만들자.
vi bot.py
bot.py 파일을 만들자. 해당 파일은 디스코드 봇이 사용할 수 있는 명령어를 정의하는 파일이다. i를 누른 후 아래 내용을 붙여넣는다.
import os
import aiohttp
import discord
from discord import app_commands
from discord.ext import commands, tasks
TOKEN = os.environ["DISCORD_TOKEN"]
GUILD_ID = int(os.environ["DISCORD_GUILD_ID"])
API_BASE = os.environ.get("PAL_API_BASE", "http://palworld-server:8212/v1/api")
API_USER = os.environ.get("PAL_API_USER", "admin")
API_PASS = os.environ["PAL_ADMIN_PASSWORD"]
STATUS_CHANNEL_ID = int(os.environ.get("STATUS_CHANNEL_ID", "0"))
AUTH = aiohttp.BasicAuth(API_USER, API_PASS)
TIMEOUT = aiohttp.ClientTimeout(total=10)
intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)
async def api_get(path):
async with aiohttp.ClientSession(auth=AUTH, timeout=TIMEOUT) as s:
async with s.get(f"{API_BASE}/{path}") as r:
r.raise_for_status()
return await r.json()
async def api_post(path, payload=None):
async with aiohttp.ClientSession(auth=AUTH, timeout=TIMEOUT) as s:
async with s.post(f"{API_BASE}/{path}", json=payload or {}) as r:
r.raise_for_status()
try:
return await r.json()
except Exception:
return {}
@bot.event
async def on_ready():
guild = discord.Object(id=GUILD_ID)
bot.tree.copy_global_to(guild=guild)
await bot.tree.sync(guild=guild)
print(f"로그인: {bot.user} (명령 동기화 완료)")
if STATUS_CHANNEL_ID:
update_status.start()
@bot.tree.command(description="서버 정보(이름/버전/접속 인원)를 봅니다")
async def 서버정보(interaction: discord.Interaction):
await interaction.response.defer()
try:
info = await api_get("info")
players = await api_get("players")
n = len(players.get("players", []))
embed = discord.Embed(title="🖥️ 서버 정보", color=0x57F287)
embed.add_field(name="이름", value=info.get("servername", "-"), inline=False)
embed.add_field(name="버전", value=info.get("version", "-"), inline=True)
embed.add_field(name="접속 인원", value=f"{n}명", inline=True)
await interaction.followup.send(embed=embed)
except Exception as e:
await interaction.followup.send(f"⚠️ 서버 응답 없음: `{e}`")
@bot.tree.command(description="현재 접속자 목록을 봅니다")
async def 접속자(interaction: discord.Interaction):
await interaction.response.defer()
try:
data = await api_get("players")
players = data.get("players", [])
if not players:
await interaction.followup.send("👤 접속자가 없습니다.")
return
lines = [f"• {p.get('name','?')} (Lv.{p.get('level','?')})" for p in players]
embed = discord.Embed(
title=f"👥 접속자 {len(players)}명",
description="\n".join(lines),
color=0x5865F2,
)
await interaction.followup.send(embed=embed)
except Exception as e:
await interaction.followup.send(f"⚠️ 조회 실패: `{e}`")
@bot.tree.command(description="게임 내 전체 공지를 보냅니다")
@app_commands.describe(메시지="보낼 공지 내용")
async def 공지(interaction: discord.Interaction, 메시지: str):
await interaction.response.defer()
try:
await api_post("announce", {"message": 메시지})
await interaction.followup.send(f"📢 공지 전송: {메시지}")
except Exception as e:
await interaction.followup.send(f"⚠️ 전송 실패: `{e}`")
@bot.tree.command(description="월드를 강제 저장합니다")
async def 저장(interaction: discord.Interaction):
await interaction.response.defer()
try:
await api_post("save")
await interaction.followup.send("💾 저장 완료")
except Exception as e:
await interaction.followup.send(f"⚠️ 저장 실패: `{e}`")
@bot.tree.command(description="예고 후 서버를 재시작합니다")
@app_commands.describe(초="대기 시간(초)", 메시지="플레이어에게 보낼 안내")
async def 재시작(interaction: discord.Interaction, 초: int = 60, 메시지: str = "서버를 재시작합니다"):
await interaction.response.defer()
try:
await api_post("shutdown", {"waittime": 초, "message": 메시지})
await interaction.followup.send(f"🔄 {초}초 후 재시작 예약 (도커 자동 복구)")
except Exception as e:
await interaction.followup.send(f"⚠️ 실패: `{e}`")
@bot.tree.command(description="예고 후 서버를 종료합니다")
@app_commands.describe(초="대기 시간(초)", 메시지="플레이어에게 보낼 안내")
async def 종료(interaction: discord.Interaction, 초: int = 60, 메시지: str = "서버를 종료합니다"):
await interaction.response.defer()
try:
await api_post("shutdown", {"waittime": 초, "message": 메시지})
await interaction.followup.send(f"🛑 {초}초 후 종료 예약")
except Exception as e:
await interaction.followup.send(f"⚠️ 실패: `{e}`")
@bot.tree.command(description="플레이어를 추방합니다 (SteamID 필요)")
@app_commands.describe(steamid="대상 SteamID", 사유="추방 사유")
async def 킥(interaction: discord.Interaction, steamid: str, 사유: str = "관리자 추방"):
await interaction.response.defer()
try:
await api_post("kick", {"userid": f"steam_{steamid}", "message": 사유})
await interaction.followup.send(f"👢 추방: `{steamid}`")
except Exception as e:
await interaction.followup.send(f"⚠️ 실패: `{e}`")
@bot.tree.command(description="플레이어를 차단합니다 (SteamID 필요)")
@app_commands.describe(steamid="대상 SteamID", 사유="차단 사유")
async def 밴(interaction: discord.Interaction, steamid: str, 사유: str = "관리자 차단"):
await interaction.response.defer()
try:
await api_post("ban", {"userid": f"steam_{steamid}", "message": 사유})
await interaction.followup.send(f"🚫 차단: `{steamid}`")
except Exception as e:
await interaction.followup.send(f"⚠️ 실패: `{e}`")
@tasks.loop(minutes=5)
async def update_status():
ch = bot.get_channel(STATUS_CHANNEL_ID)
if ch is None:
return
try:
players = await api_get("players")
n = len(players.get("players", []))
new_name = f"🟢ㆍ접속-{n}명"
except Exception:
new_name = "🔴ㆍ서버-오프라인"
try:
await ch.edit(name=new_name)
except Exception as e:
print(f"채널 이름 갱신 실패: {e}")
bot.run(TOKEN)
Esc-:wq를 입력하여 저장하여 닫아준다. 위 코드를 통해 칠 수 있는 명령어는 이와 같다.
| /서버정보 | 서버 이름 · 버전 · 현재 접속 인원 |
| /접속자 | 지금 접속 중인 사람 목록 |
| /공지 [내용] | 게임 화면에 전체 공지 띄우기 |
| /저장 | 월드 강제 저장 |
| /재시작 [초] [메시지] | 예고 후 서버 재시작 |
| /종료 [초] [메시지] | 예고 후 서버 종료 |
| /킥 [스팀ID] [사유] | 플레이어 추방 |
| /밴 [스팀ID] [사유] | 플레이어 차단 |
이제 dockerfile을 생성하자.
vi Dockerfile
i를 눌러 아래 내용을 붙여 넣는다.
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir "discord.py>=2.4" aiohttp
COPY bot.py .
CMD ["python", "-u", "bot.py"]
Esc-:wq를 입력하여 저장하여 닫아준다.
vi .env
봇 운영에 필요한 정보들을 채워주어야 한다. i를 누른 후 아래 내용들을 채워준다.
DISCORD_TOKEN=봇_토큰
DISCORD_GUILD_ID=서버ID
PAL_ADMIN_PASSWORD=compose에_넣은_그_어드민비번
STATUS_CHANNEL_ID=0
본인의 값으로 채워준다. 참고로 서버 ID는 디스코드 설정 → 고급 → 개발자 모드 ON 하고 서버 아이콘 우클릭 → 서버 ID 복사로 알 수 있다. Esc-:wq를 입력하여 저장하여 닫아준다.
이제 해당 palbot을 compose.yaml에 추가하자.
vi compose.yaml
i를 눌러준다.
palbot:
build: ./palbot
restart: unless-stopped
container_name: palworld-bot
env_file:
- ./palbot/.env
depends_on:
- palworld
맨 아래 해당 부분을 추가한다. 띄어쓰기 2개가 있어야 한다. 전체 compose.yaml은 아래와 같아질 것이다.
services:
palworld:
image: thijsvanloef/palworld-server-docker:latest
restart: unless-stopped
container_name: palworld-server
stop_grace_period: 30s
ports:
- 8211:8211/udp
- 8212:8212/tcp
environment:
- (생략)
- DISCORD_WEBHOOK_URL=웹훅_URL
volumes:
- ./palworld:/palworld/
palbot:
build: ./palbot
restart: unless-stopped
container_name: palworld-bot
env_file:
- ./palbot/.env
depends_on:
- palworld
이후 Esc-:wq를 입력하여 저장 후 닫아준다.
docker compose up -d --build
봇 이미지를 빌드하고 전체를 다시 올린다.
docker compose logs -f palbot
봇 로그를 확인한다. 로그인: 봇이름#1234 (명령 동기화 완료)가 뜬다면 성공한 것이다. Ctrl+C를 눌러 빠져나올 수 있다.
'게임 > 팰월드' 카테고리의 다른 글
| [팰월드 / Palworld] 팰월드 (Palworld) EXCEPTION_ACCESS_VIOLATION 오류 (3) | 2024.07.07 |
|---|