Skip to content

Feat/UI redesign production docker - #6

Merged
iamfarhad merged 3 commits into
mainfrom
feat/ui-redesign-production-docker
Jul 18, 2026
Merged

iamfarhad merged 3 commits into
mainfrom
feat/ui-redesign-production-docker

Conversation

@iamfarhad

Copy link
Copy Markdown
Owner

No description provided.

farhadzand and others added 3 commits July 18, 2026 10:13
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>
@iamfarhad
iamfarhad merged commit 576c113 into main Jul 18, 2026
2 of 3 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +183 to +189
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)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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.

Suggested change
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)
}

Comment on lines +200 to +201
func (s *Server) ReapplyDomainConfig(ctx context.Context) {
settings, err := s.Store.GetSettings(ctx)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If s.CaddyAdmin is nil, we can return early from ReapplyDomainConfig to avoid unnecessary database queries and retries on startup.

func (s *Server) ReapplyDomainConfig(ctx context.Context) {
	if s.CaddyAdmin == nil {
		return
	}
	settings, err := s.Store.GetSettings(ctx)

Comment on lines +82 to +91
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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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
}

Comment thread deploy/install.sh
Comment on lines +253 to +254
avail_mb="$(df -Pm / 2>/dev/null | awk 'NR==2{print $4}')"
avail_mb="${avail_mb:-0}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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 ))

Comment thread deploy/install.sh
Comment on lines +296 to 310
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)."
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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)."
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant