<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: EpicWing</title>
    <description>The latest articles on DEV Community by EpicWing (@epicwing).</description>
    <link>https://dev.to/epicwing</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4232019%2Ff7a98a34-aa8e-46e7-9122-e7391724fcda.png</url>
      <title>DEV Community: EpicWing</title>
      <link>https://dev.to/epicwing</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/epicwing"/>
    <language>en</language>
    <item>
      <title>[Boost]</title>
      <dc:creator>EpicWing</dc:creator>
      <pubDate>Sat, 07 Nov 2026 12:31:48 +0000</pubDate>
      <link>https://dev.to/epicwing/-2cp5</link>
      <guid>https://dev.to/epicwing/-2cp5</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/epicwing/self-hosted-server-panel-1em8" class="crayons-story__hidden-navigation-link"&gt;Self-hosted Server Panel&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/epicwing" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4232019%2Ff7a98a34-aa8e-46e7-9122-e7391724fcda.png" alt="epicwing profile" class="crayons-avatar__image" width="512" height="512"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/epicwing" class="crayons-story__secondary fw-medium m:hidden"&gt;
              EpicWing
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                EpicWing
                
                
              
              &lt;div id="story-author-preview-content-4993380" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/epicwing" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4232019%2Ff7a98a34-aa8e-46e7-9122-e7391724fcda.png" class="crayons-avatar__image" alt="" width="512" height="512"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;EpicWing&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/epicwing/self-hosted-server-panel-1em8" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 7&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/epicwing/self-hosted-server-panel-1em8" id="article-link-4993380"&gt;
          Self-hosted Server Panel
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/python"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;python&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/docker"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;docker&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/epicwing/self-hosted-server-panel-1em8" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/epicwing/self-hosted-server-panel-1em8#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
          
          &lt;div class="only-sidebar-menu-item trusted-visible-block print-hidden align-center"&gt;
                
                    
                
          &lt;/div&gt;

        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Self-hosted Server Panel</title>
      <dc:creator>EpicWing</dc:creator>
      <pubDate>Sat, 07 Nov 2026 11:51:27 +0000</pubDate>
      <link>https://dev.to/epicwing/self-hosted-server-panel-1em8</link>
      <guid>https://dev.to/epicwing/self-hosted-server-panel-1em8</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;In v0.2.0-beta I worked on one question: what happens when one container misbehaves? Here is what I changed and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Containers could talk to each other
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The agent now creates a separate network at startup:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker network create --ipv6 \
  -o com.docker.network.bridge.enable_icc=false epicwing-servers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A memory limit that wasn't one
&lt;/h2&gt;

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

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

&lt;h2&gt;
  
  
  3. Logs that grow forever
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--log-opt max-size=10m --log-opt max-file=3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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

&lt;h2&gt;
  
  
  4. Per-node limits, tested under load
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was already there
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  What's still missing
&lt;/h2&gt;

&lt;p&gt;The disk quota is only used when assigning servers, it isn't enforced at runtime yet. That's next.&lt;/p&gt;

&lt;p&gt;Code and releases: &lt;a href="https://github.com/EpicWing/epicwing-panel" rel="noopener noreferrer"&gt;https://github.com/EpicWing/epicwing-panel&lt;/a&gt;&lt;/p&gt;

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

</description>
      <category>python</category>
      <category>docker</category>
      <category>opensource</category>
      <category>security</category>
    </item>
  </channel>
</rss>
