<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[EpicWing Panel]]></title><description><![CDATA[Notes from building EpicWing, an open source (MIT) self hosted panel for running game servers, bots, websites and other services in Docker containers. Posts abo]]></description><link>https://epicwing.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6aef10ffee988482572012c1/002807e1-21fd-4a6f-9a01-b8cc15a6bf0e.png</url><title>EpicWing Panel</title><link>https://epicwing.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 08 Nov 2026 10:21:49 GMT</lastBuildDate><atom:link href="https://epicwing.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I isolate containers in a self hosted server panel: Docker networks, swap limits and per node quotas]]></title><description><![CDATA[Im building EpicWing, an open source (MIT) self-hosted panel for running game servers, Discord bots, websites and other services in Docker containers. The…]]></description><link>https://epicwing.hashnode.dev/how-i-isolate-containers-in-a-self-hosted-server-panel-docker-networks-swap-limits-and-per-node-quotas</link><guid isPermaLink="true">https://epicwing.hashnode.dev/how-i-isolate-containers-in-a-self-hosted-server-panel-docker-networks-swap-limits-and-per-node-quotas</guid><category><![CDATA[#selfhosted]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[EpicWing]]></dc:creator><pubDate>Sat, 07 Nov 2026 12:05:27 GMT</pubDate><content:encoded><![CDATA[<p>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.</p>
<p>In v0.2.0-beta I worked on one question: what happens when one container misbehaves? Here is what I changed and why.</p>
<h2>1. Containers could talk to each other</h2>
<p>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.</p>
<p>The agent now creates a separate network at startup:</p>
<pre><code class="language-bash">docker network create --ipv6 \
  -o com.docker.network.bridge.enable_icc=false epicwing-servers
</code></pre>
<p>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.</p>
<p>Server databases keep working: the database host's published port is still reachable through the gateway address (<code>172.17.0.1:3306</code> in my setup). I checked this from inside a running server, not only from a test container.</p>
<p>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.</p>
<h2>2. A memory limit that wasn't one</h2>
<p>The agent already passed <code>--memory</code>. On a host with swap, a container with a 2 GB limit can still use up to 4 GB in total, because without <code>--memory-swap</code> the swap allowance defaults to the same size as the limit.</p>
<p>The fix is one flag: set <code>--memory-swap</code> to the same value as <code>--memory</code>. There is a test that checks both flags are present when a limit is set and absent when it isn't.</p>
<h2>3. Logs that grow forever</h2>
<p>Docker's default <code>json-file</code> logging has no size cap. A server that prints constantly will fill the disk over time. Each container now gets:</p>
<pre><code class="language-bash">--log-opt max-size=10m --log-opt max-file=3
</code></pre>
<p>I set this per container in the agent instead of in <code>daemon.json</code>, because changing the daemon config needs a Docker restart, which would also restart everything else on the host.</p>
<h2>4. Per-node limits, tested under load</h2>
<p>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.</p>
<p>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.</p>
<h2>What was already there</h2>
<p>Containers run with <code>--cap-drop ALL</code>, <code>no-new-privileges</code>, a pids limit, a non-root user, and only their own data directory mounted. File access in the agent uses Go's <code>os.Root</code>, so paths and symlinks can't leave the server directory.</p>
<h2>What's still missing</h2>
<p>The disk quota is only used when assigning servers, it isn't enforced at runtime yet. That's next.</p>
<p>Code and releases: <a href="https://github.com/EpicWing/epicwing-panel">https://github.com/EpicWing/epicwing-panel</a></p>
<p>Feedback is welcome, especially on the network isolation: if you see a case where it breaks something, I'd like to know.</p>
]]></content:encoded></item></channel></rss>