Im building EpicWing, an open source (MIT) self-hosted panel for running game servers, Discord bots, websites and other services in Docker containers. The backend is FastAPI, the frontend is Vue, and the agent that runs on each node is written in Go. It's still in beta.

In v0.2.0-beta I worked on one question: what happens when one container misbehaves? Here is what I changed and why.

1. Containers could talk to each other

All containers sat in Docker's default bridge network, and inter-container traffic is on by default there. A hacked game server could scan the ports of its neighbours.

The agent now creates a separate network at startup:

docker network create --ipv6 \ -o com.docker.network.bridge.enable_icc=false epicwing-servers 

Containers in it still reach the internet and their published ports, but not each other. I tested this with a throwaway container before writing any code: a direct connection to another container's IP failed, the published port through the gateway and the internet worked.

The setting only applies when a container is created. Existing containers keep the old network until they are recreated, and a normal restart is not enough.

2. A memory limit that wasn't one

The agent already passed --memory. On a host with swap, a container with a 2 GB limit can still use up to 4 GB in total, because without --memory-swap the swap allowance defaults to the same size as the limit.

The fix is one flag: set --memory-swap to the same value as --memory. There is a test that checks both flags are present when a limit is set and absent when it isn't.

3. Logs that grow forever

Docker's default json-file logging has no size cap. A server that prints constantly will fill the disk over time. Each container now gets:

--log-opt max-size=10m --log-opt max-file=3 

I set this per container in the agent instead of in daemon.json, because changing the daemon config needs a Docker restart, which would also restart everything else on the host.

4. Per-node limits, tested under load

Each node can now have a memory limit and a storage limit. The panel refuses a new server if it would go over what is left. The tricky part is two requests arriving at the same time: both read the free memory, both pass the check, and the node ends up overcommitted.

The check uses a row lock. My normal tests run on SQLite, which ignores row locks, so they could never catch a broken lock. I wrote a separate test that starts a throwaway PostgreSQL container and sends 12 server-create requests at once against a 1024 MB limit, each asking for 512 MB. Exactly two succeed, ten get a 409.

What was already there

Containers run with --cap-drop ALL, no-new-privileges, a pids limit, a non-root user, and only their own data directory mounted. File access in the agent uses Go's os.Root, so paths and symlinks can't leave the server directory.

What's still missing

The disk quota is only used when assigning servers, it isn't enforced at runtime yet. That's next.

Code and releases: https://github.com/EpicWing/epicwing-panel

Feedback is welcome, especially on the network isolation: if you see a case where it breaks something, I'd like to know.