Repository navigation
Feat/UI redesign production docker - #6
Conversation
A 2GB swap on a 512MB-1GB VPS still wasn't enough - the self-node container then OOM'd with "out of memory allocating heap arena map". Base the guard on total RAM+swap against a 3GB target and size the swap file to actually reach it (min 2GB, rounded up), set vm.swappiness so the kernel uses it, and make it persistent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 3GB swapfile filled a small disk, causing "no space left on device" from docker-proxy/runc mid-start. Now the swap step checks free disk first, caps the swapfile so it always leaves 5GB headroom for images + the database, refuses to create swap (with an explicit continue/abort prompt) when there's neither enough RAM nor disk, and warns up front when the disk is already tight. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Settings gains sub_domain/sub_port (migration 0019): when set, Caddy gets
a second HTTPS site block - own auto-provisioned certificate - that serves
ONLY /api/v1/sub/* and 404s everything else, so the links handed to end
users neither reveal nor expose the admin panel.
- Pushed live via the existing admin-socket mechanism, and re-applied from
the DB on API startup (Caddy always boots from the static Caddyfile),
which also fixes UI-set panel domains vanishing on stack restarts.
- Account responses gain subscription_url built from the sub origin; the
frontend prefers it over window.location.origin + subscription_path.
- sub_domain ""/sub_port 0 explicitly clear back to off/default, since the
existing omitted-means-unchanged PATCH convention can't express that.
- compose publishes ${SUB_PORT:-8443} so a non-443 port works out of the
box; port 443 shares :443 with the panel via SNI and needs nothing.
- api container now receives PANEL_DOMAIN as the fallback for rendering a
complete Caddy config when panel_settings.panel_domain was never set.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces support for a separate subscription domain and port, allowing operators to isolate subscription links from the main admin panel. It updates the backend APIs, database schema, Caddy configuration templates, and frontend settings page to manage this feature, and adds a startup routine to reapply domain configurations to Caddy. Additionally, the installer script is updated to dynamically size and configure swap space based on system resources. Feedback on these changes highlights several critical improvements: adding defensive checks to prevent nil pointer dereferences when the Caddy admin client is unconfigured, fixing a bug where a subscription port of 0 results in an invalid URL, improving the portability of disk space checks in the installer, and ensuring robust swap file creation on filesystems like btrfs.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| func (s *Server) applyDomainConfig(ctx context.Context, settings store.PanelSettings) error { | ||
| cfg, err := s.domainConfigFromSettings(settings) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| return s.CaddyAdmin.Apply(ctx, cfg) | ||
| } |
There was a problem hiding this comment.
If s.CaddyAdmin is nil (which is a valid state according to the struct definition in server.go), calling s.CaddyAdmin.Apply will cause a nil pointer dereference panic. Add a defensive check to return an error or handle the nil case gracefully.
| func (s *Server) applyDomainConfig(ctx context.Context, settings store.PanelSettings) error { | |
| cfg, err := s.domainConfigFromSettings(settings) | |
| if err != nil { | |
| return err | |
| } | |
| return s.CaddyAdmin.Apply(ctx, cfg) | |
| } | |
| func (s *Server) applyDomainConfig(ctx context.Context, settings store.PanelSettings) error { | |
| if s.CaddyAdmin == nil { | |
| return errors.New("caddy admin client is not configured") | |
| } | |
| cfg, err := s.domainConfigFromSettings(settings) | |
| if err != nil { | |
| return err | |
| } | |
| return s.CaddyAdmin.Apply(ctx, cfg) | |
| } |
| func (s *Server) ReapplyDomainConfig(ctx context.Context) { | ||
| settings, err := s.Store.GetSettings(ctx) |
There was a problem hiding this comment.
| func subscriptionBaseURL(settings store.PanelSettings) *string { | ||
| if settings.SubDomain == nil || *settings.SubDomain == "" { | ||
| return nil | ||
| } | ||
| base := "https://" + *settings.SubDomain | ||
| if settings.SubPort != nil && *settings.SubPort != 443 { | ||
| base += ":" + strconv.Itoa(*settings.SubPort) | ||
| } | ||
| return &base | ||
| } |
There was a problem hiding this comment.
If settings.SubPort is explicitly set to 0 (the sentinel value for the default port), subscriptionBaseURL will append :0 to the URL, resulting in an invalid address like https://sub.example.com:0. Add a check to exclude 0 in addition to 443.
| func subscriptionBaseURL(settings store.PanelSettings) *string { | |
| if settings.SubDomain == nil || *settings.SubDomain == "" { | |
| return nil | |
| } | |
| base := "https://" + *settings.SubDomain | |
| if settings.SubPort != nil && *settings.SubPort != 443 { | |
| base += ":" + strconv.Itoa(*settings.SubPort) | |
| } | |
| return &base | |
| } | |
| func subscriptionBaseURL(settings store.PanelSettings) *string { | |
| if settings.SubDomain == nil || *settings.SubDomain == "" { | |
| return nil | |
| } | |
| base := "https://" + *settings.SubDomain | |
| if settings.SubPort != nil && *settings.SubPort != 0 && *settings.SubPort != 443 { | |
| base += ":" + strconv.Itoa(*settings.SubPort) | |
| } | |
| return &base | |
| } |
| avail_mb="$(df -Pm / 2>/dev/null | awk 'NR==2{print $4}')" | ||
| avail_mb="${avail_mb:-0}" |
There was a problem hiding this comment.
Using df -Pm is not fully portable across all Unix/Linux environments and can wrap lines or fail. If it fails, avail_mb defaults to 0, which triggers a false low-disk warning and blocks the installation. Using df -Pk with END{print $4} is highly portable and robust. We also safely handle empty values to prevent bash syntax errors.
| avail_mb="$(df -Pm / 2>/dev/null | awk 'NR==2{print $4}')" | |
| avail_mb="${avail_mb:-0}" | |
| avail_mb="$(df -Pk / 2>/dev/null | awk 'END{print $4}')" | |
| avail_mb=$(( ${avail_mb:-0} / 1024 )) |
| create_swap() { | ||
| local gb="${1:-2}" | ||
| log "Creating a ${gb} GB swap file at /swapfile..." | ||
| swapoff /swapfile 2>/dev/null || true | ||
| rm -f /swapfile | ||
| fallocate -l "${gb}G" /swapfile 2>/dev/null || dd if=/dev/zero of=/swapfile bs=1M count=$(( gb * 1024 )) status=none | ||
| chmod 600 /swapfile | ||
| mkswap /swapfile >/dev/null 2>&1 || true | ||
| mkswap /swapfile >/dev/null 2>&1 | ||
| swapon /swapfile | ||
| grep -q '^/swapfile ' /etc/fstab 2>/dev/null || echo '/swapfile none swap sw 0 0' >> /etc/fstab | ||
| log "Swap enabled ($(swapon --show=NAME,SIZE --noheadings 2>/dev/null | tr '\n' ' '))." | ||
| # On a low-RAM box the kernel needs a nudge to actually lean on swap under pressure. | ||
| sysctl -w vm.swappiness=60 >/dev/null 2>&1 || true | ||
| grep -q '^vm.swappiness' /etc/sysctl.d/99-wgpanel.conf 2>/dev/null || echo 'vm.swappiness=60' > /etc/sysctl.d/99-wgpanel.conf | ||
| log "Swap enabled - total memory now $(awk '/^MemTotal:|^SwapTotal:/{s+=$2} END{printf "%d MB", s/1024}' /proc/meminfo)." | ||
| } |
There was a problem hiding this comment.
On some filesystems (such as btrfs or thin-provisioned disks), fallocate can succeed but create a file with holes, which swapon will subsequently reject. Checking if swapon succeeds and falling back to dd ensures the swap file is created reliably. Additionally, /etc/sysctl.d might not exist on some minimal systems, so we should check for its existence before writing to it, falling back to /etc/sysctl.conf.
create_swap() {
local gb="${1:-2}"
log "Creating a ${gb} GB swap file at /swapfile..."
swapoff /swapfile 2>/dev/null || true
rm -f /swapfile
if fallocate -l "${gb}G" /swapfile 2>/dev/null; then
chmod 600 /swapfile
mkswap /swapfile >/dev/null 2>&1
if ! swapon /swapfile 2>/dev/null; then
warn "swapon failed (likely due to holes in fallocated file). Re-creating with dd..."
rm -f /swapfile
dd if=/dev/zero of=/swapfile bs=1M count=$(( gb * 1024 )) status=none
chmod 600 /swapfile
mkswap /swapfile >/dev/null 2>&1
swapon /swapfile || warn "Failed to enable swap."
fi
else
dd if=/dev/zero of=/swapfile bs=1M count=$(( gb * 1024 )) status=none
chmod 600 /swapfile
mkswap /swapfile >/dev/null 2>&1
swapon /swapfile || warn "Failed to enable swap."
fi
grep -q '^/swapfile ' /etc/fstab 2>/dev/null || echo '/swapfile none swap sw 0 0' >> /etc/fstab
sysctl -w vm.swappiness=60 >/dev/null 2>&1 || true
if [[ -d /etc/sysctl.d ]]; then
grep -q '^vm.swappiness' /etc/sysctl.d/99-wgpanel.conf 2>/dev/null || echo 'vm.swappiness=60' > /etc/sysctl.d/99-wgpanel.conf
else
grep -q '^vm.swappiness' /etc/sysctl.conf 2>/dev/null || echo 'vm.swappiness=60' >> /etc/sysctl.conf
fi
log "Swap enabled - total memory now $(awk '/^MemTotal:|^SwapTotal:/{s+=$2} END{printf "%d MB", s/1024}' /proc/meminfo)."
}
No description provided.