Added Docker Support

This commit is contained in:
DerGrumpf 2024-10-09 18:29:51 +02:00
parent 47d44643c3
commit 04c5db2232
9 changed files with 554 additions and 162 deletions

View File

@ -5,4 +5,4 @@ RUN pip install --upgrade pip
RUN pip install -r requirements.txt
ADD . .
CMD ["python", "main.py"]
CMD ["python", "bot.py"]

View File

@ -3,6 +3,7 @@ from discord.ext import commands
from cogs.minecraft import Minecraft
from cogs.user_management import UserManager
from cogs.spawner import Spawner
# Setup Environment
import os
@ -12,11 +13,6 @@ load_dotenv()
# Discord Stuff
TOKEN = os.environ['TOKEN']
# Server Stuff
RCON_SERVER = os.environ['RCON_SERVER']
RCON_PASS = os.environ['RCON_PASS']
RCON_PORT = int(os.environ['RCON_PORT'])
# Setup Basic Permission
intents = discord.Intents.default()
intents.message_content = True
@ -25,8 +21,9 @@ intents.message_content = True
bot = commands.Bot(command_prefix='-', intents=intents)
async def setup():
await bot.add_cog(Minecraft(bot, RCON_SERVER, RCON_PASS, RCON_PORT))
await bot.add_cog(Minecraft(bot))
await bot.add_cog(UserManager(bot))
await bot.add_cog(Spawner(bot))
@bot.event
async def on_ready():

View File

@ -2,79 +2,14 @@ import discord
from discord.ext import commands
from mcrcon import MCRcon
import enum
from statemachine import StateMachine, State
from statemachine.states import States
from transitions import Machine
from cogs.spawner import containers
class BorderWarsSession(StateMachine):
"A workflow maschine for managing InGame States"
Nothing = State(initial=True)
Initialization = State()
Safe = State()
Fight = State()
SuddenDeath = State()
End = State()
init_game = Nothing.to(Initialization)
start_game = Initialization.to(Safe)
start_fight = Safe.to(Fight)
start_last_round = Fight.to(SuddenDeath)
end_game = Fight.to(End) | SuddenDeath.to(End)
abort = Initialization.to(Nothing) | Safe.to(Nothing) | Fight.to(Nothing) | SuddenDeath.to(Nothing) | End.to(Nothing) | Nothing.to(Nothing)
reset = End.to(Initialization)
@Initialization.enter
def initialization(self) -> list:
@Safe.enter
def safe(self) -> list:
# Chat countdown
return [
'''/title @a subtitle ["",{"text":"bei ","color":"blue"},{"text":"BORDER WARS!","bold":true,"color":"red"}]''',
'''/title @a title {"text":"Viel Glück!","bold":true,"color":"blue"}''',
"playsound minecraft:entity.wither.spawn ambient @a 0 64 080",
"/worldborder set 1000",
"/gamerule keepInventory true"
]
@Fight.enter
def fight(self) -> list:
# Timer Starten
return [
'''/title @a subtitle {"text":"ÜBERLEBEN!","bold":true,"color":"red"}''',
'''/title @a title {"text":"Möge der beste","color":"blue"}''',
"/playsound minecraft:item.totem.use ambient @a 0 64 0 80",
"/worldborder set 75 3600",
"/gamerule keepInventory false"
]
@SuddenDeath.enter
def death(self) -> list:
# Timer Starten
return [
'''/title @a title ["",{"text":"Sudden ","color":"dark_blue"},{"text":"DEATH!","bold":true,"color":"red"}]''',
"/playsound minecraft:entity.ender_dragon.growl ambient @a 0 64 0 80",
"/worldborder set 5 600"
]
@End.enter
def end(self, playername: str) -> list:
return [
"/worldborder center 0 0",
"/worldborder set 75",
"/gamerule keepInventory true",
'''/title @a subtitle ["",{"text":"''' + playername + '''","bold":true,"color":"red"},{"text":" gewinnt","color":"dark_blue"}]''',
'''/title @a title {"text":"ENDE!","color":"dark_blue"}''',
"/playsound minecraft:entity.ender_dragon_death ambient @a 0 64 0 80",
]
class Whitelist(StateMachine):
class Whitelist:
"A workflow machine for managing Whitelist states"
On = State(initial=True)
Off = State()
toggle = On.to(Off) | Off.to(On)
On = None
Off = None
toggle = None
class RCON(MCRcon):
def __init__(self, ip: str, secret: str, port: int = 31066):
@ -86,50 +21,43 @@ class RCON(MCRcon):
cmds = "/whitelist {}".format(self.whitelist.current_state.id)
print(cmds)
def _sendcmd(self, cmds: str | list) -> None:
def sendcmd(self, cmds) -> None:
if isinstance(cmds, str):
return self.server.command(str)
return self.command(str)
if isinstance(cmds, list):
return [self.server.commands(cmd) for cmd in cmds]
return [self.command(cmd) for cmd in cmds]
def __del__(self):
self.disconnect()
class States(enum.Enum):
NOTHING = 0
INIT = 1
SAFE = 2
FIGHT = 3
SUDDENDEATH = 4
END = 5
class Minecraft(commands.Cog):
def __init__(self, bot: commands.Bot, ip: str, secret: str, port: int = 31066):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.server = RCON(ip, secret, port)
self.session = BorderWarsSession()
self.whitelist = Whitelist()
self.servers = dict()
@commands.hybrid_command(name='whitelist')
async def whitelist(self, ctx: commands.Context):
"""
Toggles Servers Whitelist
transitions = [
['init_game', States.NOTHING, States.INIT],
['start_game', States.INIT, States.SAFE],
['start_fight', States.SAFE, States.FIGHT],
['start_last_round', States.FIGHT, States.SUDDENDEATH],
['end_game', States.FIGHT, States.END],
['end_game', States.SUDDENDEATH, States.END],
['reset', States.END, States.INIT],
['abort', '*', States.NOTHING]
]
Parameters
----------
ctx: commands.Context
The context of the command invocation
"""
await self.whitelist.activate_initial_state()
await ctx.send("Whitelist")
@commands.hybrid_command(name='start')
async def start(self, ctx: commands.Context):
"""
Starts a Border Wars Session
Parameters
----------
ctx: commands.Context
The context of the command invocation
"""
cmds =
await ctx.send("Start")
self.machine = Machine(states=States, transitions=transitions, initial=States.NOTHING)
@commands.hybrid_command(name='init')
async def init(self, ctx: commands.Context):
async def init(self, ctx: commands.Context, server_name: str):
"""
Initialize a Border Wars session
@ -137,18 +65,165 @@ class Minecraft(commands.Cog):
----------
ctx: commands.Context
The context of the command invocation
server_name: str
Server on which the Session should be initialized
"""
server_name = server_name.title()
c = None
for container in containers:
if server_name == container.name:
c = container
break
if not c:
await ctx.send("---The server doesn't run---")
return
conn = RCON(str(c.ip), c.rcon_pass, c.rcon_port)
self.servers[server_name] = conn
cmds = [
"/effect give @a minecraft:resistance infinite 255 true",
"/effect give @a minecraft:saturation infinite 4 true",
"/tp @a 0 200 0",
"/gamemode adventure @a",
"/worldborder center 0 0",
"/worldborder set 5",
"/whitelist off"
]
await self.session.activate_initial_state()
await self.session.init_game()
await ctx.send(self.session.current_state)
conn.sendcmd(cmds)
await ctx.send("init Border Wars Game")
@commands.hybrid_command(name='safe')
async def safe(self, ctx: commands.Context, server_name: str):
"""
Switches to Safe Phase on a Border Wars session
Parameters
----------
ctx: commands.Context
The context of the command invocation
server_name: str
Server on which the Session should be initialized
"""
server_name = server_name.title()
c = None
for container in containers:
if server_name == container.name:
c = container
break
if not c:
await ctx.send("---The server doesn't run---")
return
conn = self.servers.get(server_name)
if not conn:
await ctx.send("---Border Wars Session not Initialized---")
return
cmds = [
'''/title @a subtitle ["",{"text":"bei ","color":"blue"},{"text":"BORDER WARS!","bold":true,"color":"red"}]''',
'''/title @a title {"text":"Viel Glück!","bold":true,"color":"blue"}''',
"playsound minecraft:entity.wither.spawn ambient @a 0 64 080",
"/worldborder set 1000",
"/gamerule keepInventory true",
"/gamemode survival @a",
"/effect clear @a"
]
conn.sendcmd(cmds)
await ctx.send("Switched to Safe Phase")
@commands.hybrid_command(name='fight')
async def fight(self, ctx: commands.Context, server_name: str):
"""
Switches to Fight Phase on a Border Wars session
Parameters
----------
ctx: commands.Context
The context of the command invocation
server_name: str
Server on which the Session should be initialized
"""
server_name = server_name.title()
c = None
for container in containers:
if server_name == container.name:
c = container
break
if not c:
await ctx.send("---The server doesn't run---")
return
conn = self.servers.get(server_name)
if not conn:
await ctx.send("---Border Wars Session not Initialized---")
return
cmds = [
'''/title @a subtitle {"text":"ÜBERLEBEN!","bold":true,"color":"red"}''',
'''/title @a title {"text":"Möge der beste","color":"blue"}''',
"/playsound minecraft:item.totem.use ambient @a 0 64 0 80",
"/worldborder set 75 3600",
"/gamerule keepInventory false"
]
conn.sendcmd(cmds)
await ctx.send("Switched to Fight Phase")
@commands.hybrid_command(name='death')
async def death(self, ctx: commands.Context, server_name: str):
"""
Switches to Sudden Death Phase on a Border Wars session
Parameters
----------
ctx: commands.Context
The context of the command invocation
server_name: str
Server on which the Session should be initialized
"""
server_name = server_name.title()
c = None
for container in containers:
if server_name == container.name:
c = container
break
if not c:
await ctx.send("---The server doesn't run---")
return
conn = self.servers.get(server_name)
if not conn:
await ctx.send("---Border Wars Session not Initialized---")
return
cmds = [
'''/title @a title ["",{"text":"Sudden ","color":"dark_blue"},{"text":"DEATH!","bold":true,"color":"red"}]''',
"/playsound minecraft:entity.ender_dragon.growl ambient @a 0 64 0 80",
"/worldborder set 5 600"
]
conn.sendcmd(cmds)
await ctx.send("Switched to Sudden Death Phase")
@commands.hybrid_command(name='end')
async def end(self, ctx: commands.Context):
async def end(self, ctx: commands.Context, server_name: str, playername: str):
"""
Ends a Border Wars session
@ -156,30 +231,41 @@ class Minecraft(commands.Cog):
----------
ctx: commands.Context
The context of the command invocation
server_name: str
Server on which the Session should be initialized
playername: str
Player which is announced as the Winner
"""
await ctx.send("End")
server_name = server_name.title()
@commands.hybrid_command(name='rules')
async def rules(self, ctx: commands.Context):
"""
Displays the Border Wars rules
c = None
for container in containers:
if server_name == container.name:
c = container
break
Parameters
----------
ctx: commands.Context
The context of the command invocation
"""
await ctx.send("Rules")
if not c:
await ctx.send("---The server doesn't run---")
return
conn = self.servers.get(server_name)
if not conn:
await ctx.send("---Border Wars Session not Initialized---")
return
cmds = [
"/worldborder center 0 0",
"/worldborder set 75",
"/gamerule keepInventory true",
'''/title @a subtitle ["",{"text":"''' + playername + '''","bold":true,"color":"red"},{"text":" gewinnt","color":"dark_blue"}]''',
'''/title @a title {"text":"ENDE!","color":"dark_blue"}''',
"/playsound minecraft:entity.ender_dragon_death ambient @a 0 64 0 80",
]
conn.sendcmd(cmds)
await ctx.send("Ended Border Wars Session")
@commands.hybrid_command(name='custom')
async def custom(self, ctx: commands.Context):
"""
Register a custom command
Parameters
----------
ctx: commands.Context
The context of the command invocation
"""
await ctx.send("Custom")

242
bot/cogs/spawner.py Normal file
View File

@ -0,0 +1,242 @@
import discord
from discord.ext import commands
import docker
import random
import socket
from contextlib import closing
from datetime import datetime
import pytz
from dataclasses import dataclass
from ipaddress import IPv4Address
import secrets
import asyncio
@dataclass
class Server:
container: None
name: str
ip: IPv4Address
port: int
players: int
rcon_pass: str
rcon_port: int
# Global List of all running Containers
containers = list()
def seed_generator():
seed = random.randrange(1_000_000_000, 100_000_000_000_000)
if random.randrange(0,2) == 0:
seed *= -1
return str(seed)
def find_free_port():
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
s.bind(('', 0))
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return s.getsockname()[1]
class Spawner(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.client = docker.from_env()
self.client.images.pull('itzg/minecraft-server:latest')
@commands.hybrid_command(name='spawn')
async def spawn(self,
ctx: commands.Context,
server_name: str,
world_url: str = None,
seed: str = None,
enable_command_blocks: bool = False,
max_players: int = 10,
enable_hardcore: bool = False,
):
'''
Spawns a standard defined Minecraft Server
Either from a World Download Link or a Seed
Parameters
----------
ctx: commands.Context
The context of the command invocation
server_name: str
Name of the Server
world_url: str
Download link of a minecraft world (Should be a downloadable ZIPP Archive
seed: str
Seed to generate a World from
enable_command_blocks: bool
Enable or disable command Block
max_players: int
Maximum Number of Players who can join the Server
enable_hardcore: bool
Enables Hardcore Minecraft
'''
embed = discord.Embed(
title="Starting Server",
description=f'''
Setting up {server_name}
This could take up to **5 minutes**
''',
color=discord.Color.random(),
timestamp=datetime.now(pytz.timezone('Europe/Berlin'))
)
start = await ctx.send(embed=embed)
port = find_free_port()
server_name = server_name.title()
passwd = secrets.token_hex(32)
rcon_port = find_free_port()
env = {
"EULA": "true",
"TYPE": "FABRIC",
"VERSION": "1.21.1",
"SERVER_NAME": server_name,
"LEVEL": server_name,
"ONLINE_MODE": "true",
"TZ": "Europe/Berlin",
"MOTD": "\u00a7d\u00a7khhh\u00a76Powered by\u00a7b Garde Studios\u00a76!\u00a7d\u00a7khhh",
"OVERRIDE_SERVER_PROPERTIES": "true",
"ENABLE_COMMAND_BLOCK": enable_command_blocks,
"GAMEMODE": "survival",
"FORCE_GAMEMODE": "true",
"RCON_PASSWORD": passwd,
"RCON_PORT": rcon_port,
"BROADCAST_CONSOLE_TO_OPS": "false",
"BROADCAST_RCON_TO_OPS": "false",
"SERVER_PORT": port,
"FORCE_REDOWNLOAD": "true",
"INIT_MEMORY": "500M",
"MAX_MEMORY": "2G",
"USE_AIKAR_FLAGS": "true",
#"MODS_FILE": "/extras/mods.txt",
"OPS_FILE": "https://git.cyperpunk.de/Garde-Studios/Uno-MC/raw/branch/main/ops.json",
"SYNC_SKIP_NEWER_IN_DESTINATION": "false",
"MAX_PLAYERS": max_players,
"ANNOUNCE_PLAYER_ACHIEVMENTS": "true",
"HARDCORE": enable_hardcore,
"SNOOPER_ENABLED": "false",
"SPAWN_PROTECTION": 0,
"VIEW_DISTANCE": 12,
"ALLOW_FLIGHT": "false",
# "RESOURCE_PACK": "",
# "RESOURCE_PACK_SHA1": "",
}
if not seed and not world_url:
seed = seed_generator()
if seed:
env["SEED"] = seed
if world_url:
env["WORLD"] = world_url
container = self.client.containers.run(
image='itzg/minecraft-server:latest',
environment=env,
detach=True,
hostname=server_name,
name=server_name,
network_mode='bridge',
ports={port:port, rcon_port:rcon_port},
restart_policy={"Name": "always"},
volumes={'mods.txt': {'bind': '/extras/mods.txt', 'mode': 'ro'}}
)
net = self.client.networks.get('bot_rcon')
net.connect(container)
ip = self.client.containers.get(server_name).attrs['NetworkSettings']['Networks']['bot_rcon']['IPAddress']
server = Server(container, server_name, IPv4Address(ip), port, max_players, passwd, rcon_port)
containers.append(server)
embed = discord.Embed(
title="Success",
description=f'''
**{server_name}** has started!
**Connection URL**:
garde-studios.de:{port}
''',
color=discord.Color.random(),
timestamp=datetime.now(pytz.timezone('Europe/Berlin'))
)
await start.delete()
await ctx.send(embed=embed)
@commands.hybrid_command(name='servers')
async def servers(self, ctx: commands.Context):
'''
List all currently Running Servers
Parameters
----------
ctx: commands.Context
The context of the command invocation
'''
embed = discord.Embed(
title="Currently Running Servers",
description="List of all currently running Minecraft Servers",
color=discord.Color.random(),
timestamp=datetime.now(pytz.timezone('Europe/Berlin'))
)
for container in containers:
desc = f'''
*Status*: {container.container.status}
*URL*: garde-studios.de:{container.port}
'''
embed.add_field(name=f'{container.name} 0/{container.players}', value=desc)
await ctx.send(embed=embed)
@commands.hybrid_command(name='kill')
async def kill(self, ctx: commands.Context, server_name: str):
'''
Kill & remove currently Running Servers
Parameters
----------
ctx: commands.Context
The context of the command invocation
server_name: str
Name of the server that should be removed
'''
server_name = server_name.title()
# Check if Server exist
rm = str()
conn = None
for container in containers:
if container.name == server_name:
rm = server_name
conn = container
break
if not rm:
await ctx.send("---Server not found---")
return
conn.container.remove(force=True)
#self.client.volumes.get(server_name).remove()
containers.remove(conn)
await ctx.send(f"Server {server_name} killed successfully")
if __name__ == '__main__':
for _ in range(10):
print("|", seed_generator(), "|")
print("Port:", find_free_port())

View File

@ -1,16 +1,15 @@
services:
bot:
container_name: bot
build:
context: .
dockerfile: Dockerfile
# volumes:
# - bot_data:/home
volumes:
- ./bot_data:/home
- /var/run/docker.sock:/var/run/docker.sock
networks:
- rcon
volumes:
bot_data:
driver: local
driver_opts:
type: none
device: ./data
o: bind
networks:
rcon: {}

58
bot/mods.txt Normal file
View File

@ -0,0 +1,58 @@
# Fabric API
https://cdn.modrinth.com/data/P7dR8mSH/versions/bK6OgzFj/fabric-api-0.102.1%2B1.21.1.jar
# Cloth Config API
https://cdn.modrinth.com/data/9s6osm5g/versions/7jtvrmVP/cloth-config-15.0.130-fabric.jar
# Moonlight Lib
#https://cdn.modrinth.com/data/twkfQtEc/versions/tP7HsFBI/moonlight-1.21-2.14.12-fabric.jar
# Yungs API
https://cdn.modrinth.com/data/Ua7DFN59/versions/Nx7XHO30/YungsApi-1.21-Fabric-5.0.0.jar
# Performance
https://cdn.modrinth.com/data/gvQqBUqZ/versions/5szYtenV/lithium-fabric-mc1.21.1-0.13.0.jar
https://cdn.modrinth.com/data/fALzjamp/versions/dPliWter/Chunky-1.4.16.jar
https://cdn.modrinth.com/data/s86X568j/versions/uT1cdd3k/ChunkyBorder-1.2.18.jar
https://cdn.modrinth.com/data/LFJf0Klb/versions/7e8Rxgsk/ce-2.1.1.jar
# Proxy
#https://cdn.modrinth.com/data/8dI2tmqs/versions/AQhF7kvw/FabricProxy-Lite-2.9.0.jar
# Monitoring
#https://cdn.modrinth.com/data/dbVXHSlv/versions/YcE9H1C5/fabricexporter-1.0.11.jar
#https://cdn.modrinth.com/data/l6YH9Als/versions/qTSaozEL/spark-1.10.97-fabric.jar
# World Edit
https://cdn.modrinth.com/data/1u6JkXh5/versions/vBzkrSYP/worldedit-mod-7.3.6.jar
# Dynmap
# https://cdn.modrinth.com/data/fRQREgAc/versions/ipBhc6VW/Dynmap-3.7-beta-6-fabric-1.21.jar
# World Guard
https://cdn.modrinth.com/data/py6EMmAJ/versions/xpvSS4oW/yawp-0.0.2.10-alpha2.jar
https://cdn.modrinth.com/data/ohNO6lps/versions/gtorYSGm/ForgeConfigAPIPort-v21.1.0-1.21.1-Fabric.jar
# Permission Management
#https://cdn.modrinth.com/data/Vebnzrzj/versions/oLykW1F8/LuckPerms-Fabric-5.4.139.jar
# Custom
#https://cdn.modrinth.com/data/HjmxVlSr/versions/2Z4xpeH5/YungsBetterMineshafts-1.20.4-Fabric-4.4.0.jar
#https://cdn.modrinth.com/data/o1C1Dkj5/versions/4RpKnxDR/YungsBetterDungeons-1.20.4-Fabric-4.4.0.jar
#https://cdn.modrinth.com/data/3dT9sgt4/versions/V46v23Uz/YungsBetterOceanMonuments-1.20.4-Fabric-3.4.0.jar
#https://cdn.modrinth.com/data/kidLKymU/versions/Y05JQWx3/YungsBetterStrongholds-1.20.4-Fabric-4.4.0.jar
#https://cdn.modrinth.com/data/Z2mXHnxP/versions/QplnGAIz/YungsBetterNetherFortresses-1.20.4-Fabric-2.4.0.jar
#https://cdn.modrinth.com/data/t5FRdP87/versions/3CEVoaSN/YungsBetterWitchHuts-1.20.4-Fabric-3.4.0.jar
#https://cdn.modrinth.com/data/XNlO7sBv/versions/Rnvv7pHS/YungsBetterDesertTemples-1.20.4-Fabric-3.4.0.jar
#https://cdn.modrinth.com/data/Ht4BfYp6/versions/tx2e5Fjp/YungsBridges-1.20.4-Fabric-4.4.0.jar
#https://cdn.modrinth.com/data/ZYgyPyfq/versions/F1adMKW8/YungsExtras-1.20.4-Fabric-4.4.0.jar
#https://cdn.modrinth.com/data/z9Ve58Ih/versions/DIG3Vtjv/YungsBetterJungleTemples-1.20.4-Fabric-2.4.0.jar
#https://cdn.modrinth.com/data/2BwBOmBQ/versions/mRCm0pL5/YungsBetterEndIsland-1.20.4-Fabric-2.4.0.jar
https://cdn.modrinth.com/data/klXONLDA/versions/FQrJz3KA/villagesandpillages-fabric-mc1.21.1-1.0.1.jar
https://cdn.modrinth.com/data/tpehi7ww/versions/QmeQn0Mp/dungeons-and-taverns-v4.3.jar
#https://cdn.modrinth.com/data/fgmhI8kH/versions/5D3oDlLM/%5BFabric%5DCTOV-3.5.1.jar
#https://cdn.modrinth.com/data/7tKn1fLd/versions/65K7sgmM/MobCaptains-v3.2.1.jar
https://cdn.modrinth.com/data/cnIatHrN/versions/BfXSBkjs/universal_shops-1.7.1%2B1.21.jar

View File

@ -1,14 +1,24 @@
aiohttp==3.9.5
aiohappyeyeballs==2.4.0
aiohttp==3.10.5
aiosignal==1.3.1
attrs==23.2.0
async-timeout==4.0.3
attrs==24.2.0
certifi==2024.8.30
charset-normalizer==3.3.2
discord==2.3.2
discord.py==2.4.0
docker==7.1.0
frozenlist==1.4.1
greenlet==3.0.3
idna==3.7
greenlet==3.1.1
idna==3.10
mcrcon==0.7.0
multidict==6.0.5
multidict==6.1.0
python-dotenv==1.0.1
python-statemachine==2.3.1
SQLAlchemy==2.0.31
pytz==2024.2
requests==2.32.3
six==1.16.0
SQLAlchemy==2.0.35
transitions==0.9.2
typing_extensions==4.12.2
yarl==1.9.4
urllib3==2.2.3
yarl==1.12.1

7
bot/spawner.py Normal file
View File

@ -0,0 +1,7 @@
import docker
client = docker.from_env()
print(client.containers.run("itzg/minecraft-server", detach=True))

View File

@ -7,12 +7,5 @@ services:
ports:
- "5432:5432"
volumes:
- db_data:/var/lib/postgresql/data
- ./db_data:/var/lib/postgresql/data
volumes:
db_data:
driver: local
driver_opts:
type: none
device: ./data
o: bind