Skip to content

Technical guide · Overview

Page status: Active

NutsNews VPS Same-Host Staging Capacity Budget

The production VPS can host a tightly bounded staging application, but this

Visual overview

Primary diagram

System map

The production VPS can host a tightly bounded staging application, but this

Render the repository-owned system map when you need it.

Diagram is not rendered yet.

View as text
NutsNews VPS Same-Host Staging Capacity Budget

The production VPS can host a tightly bounded staging application, but this

flowchart TB
  accTitle: NutsNews VPS Same-Host Staging Capacity Budget
  accDescr {
    The production VPS can host a tightly bounded staging application, but this
  }
  A["NutsNews VPS Same-Host Staging Capacity Budget"] --> B["NUTSNEWS_VPS_STAGING_CAPACITY.md"]
  B --> C["Auto-generated placeholder diagram"]

NutsNews VPS Same-Host Staging Capacity Budget

Fullscreen diagram view.

NutsNews VPS Same-Host Staging Capacity Budget

Section titled “NutsNews VPS Same-Host Staging Capacity Budget”

The production VPS can host a tightly bounded staging application, but this does not authorize a staging deployment. Staging remains disabled until the later data-isolation, credential, route, deployment, and qualification work is reviewed and applied.

Go for same-host staging with the fixed budget below. The decision is based on a read-only SSH audit at 2026-07-13T15:24:06Z, not on inventory values alone. The host had four vCPUs, 9.36 GB available memory, no zram use, 74.2 GB (69.1 GiB) free root disk, and 9.66 million free inodes. No OOM event was found in the preceding seven days.

This decision is deliberately conservative because the host retains only short current samples and swap history; it does not retain CPU or memory peak series. Do not treat the current low use as a measured peak.

The audit connected as nutsnews_ops with BatchMode=yes and used existing passwordless sudo only for read operations. The relevant commands were:

Terminal window
lscpu; uptime; vmstat 1 5; free -b; swapon --show --bytes; zramctl
df -B1 -T; df -i
sudo docker system df --format '{{json .}}'
sudo docker ps -a
sudo docker inspect <each-container>
sudo docker stats --no-stream
sudo du -sB1 /var/lib/docker /var/lib/containerd /var/log /opt/nutsnews
sudo journalctl -k --since '7 days ago' --no-pager
sudo journalctl -u docker.service --since '7 days ago' --no-pager
sudo journalctl -p err..alert --since '7 days ago' --no-pager
systemctl show docker.service alloy.service nutsnews-restic-backup.service

The Docker output reported 3.57 GB of images (1.63 GB reclaimable), 1.98 GB of build cache (1.85 GB reclaimable), three running healthy containers, and a zero-byte app cache volume. Container JSON logs totalled 0.74 MB; journald used 187.6 MB. The seven-day kernel OOM count was zero. The single broad disk-pattern match was the normal boot option errors=remount-ro, not ENOSPC. Docker log messages were release/restart and resolver warnings; priority-error messages were almost entirely internet SSH probes, not capacity faults.

ResourceObserved production/platform useProduction safety reserveStaging budgetRemaining after staging budget
CPU0.20 one-minute load; 1.7% host sample; app 0.01%, Caddy 4.01%, Ops auth 0.02%3 vCPUs1 vCPU maximum; CPU shares 256At least 3 vCPUs remain outside the staging cap
Memory1.06 GB host used / 9.36 GB available; app 161 MiB, Caddy 14 MiB, Ops auth 16 MiB; Alloy process RSS 402 MiB4 GiB512 MiB limit; 256 MiB reservation5.2 GiB after reserve and staging ceiling, before normal reclaimable cache
PIDsapp 12 / 256, Caddy 9 / 128, Ops auth 1 / 128Production app stays at 256128 maximumSeparate staging process cap; production allowance unchanged
Root disk7.34 GiB used / 69.1 GiB free; Docker images 3.57 GB, build cache 1.98 GB, volumes 0 B20 GiB free1 GiB image/cache preflight budget plus 30 MiB logsAt least 48.1 GiB after reserve and preflight budget
Inodes251,014 used / 9,658,746 free1,000,000 freeNo portable per-volume inode quota; check before deployMore than 8.6 million outside reserve at audit time
Swap/zram0 B / 1.5 GiBzram is transient-spike protection, not planned capacityDo not plan to consume it1.5 GiB unused at audit time

The Docker Engine runs Compose on cgroup v2. cpus, mem_limit, mem_reservation, pids_limit, and per-service json-file log options are enforced by Docker. cpu_shares is a lower scheduling weight, not an absolute CPU reservation; the one-vCPU staging cap preserves the three-vCPU reserve by construction.

The reviewed infrastructure configuration fixes the staging service at:

  • 1 vCPU maximum and CPU shares 256.
  • 512 MiB memory maximum and 256 MiB memory reservation.
  • 128 PIDs.
  • json-file logs at 10 MiB per file with three retained files: 30 MiB maximum per staging container.
  • Two qualification workers, at most five requests per second, 300 requests total, and five minutes maximum duration.
  • A 24-hour qualification TTL. The later staging deployment workflow must scale the staging service down and remove its cache volume at expiry.

Never run destructive load tests, chaos tests, full ZAP scans, unbounded crawlers, or other unbounded traffic against this shared host.

Docker’s local volume driver on this ext4/overlayfs host does not offer a portable hard per-volume quota. Do not claim that the 1 GiB image/cache budget is a cgroup-enforced quota. The enforceable write guard is the service’s 30 MiB log cap; the deploy workflow must additionally preflight Docker/root disk and inode headroom, and expiry cleanup must remove the staging cache volume.

No on-host image builds are allowed for staging. Pull only the reviewed digest. At the audit, Docker held eleven reclaimable prior app images (1.63 GB) and 1.85 GB of reclaimable build cache; those remain an operational cleanup item, not capacity assigned to staging.

The source configuration defaults Grafana Alloy to disabled. The audited host had alloy.service active, with systemd memory current about 127 MiB and process RSS about 402 MiB. This is a source/runtime divergence, not evidence of a manual change. Count the larger observed value in capacity planning and reconcile the service only through a later protected apply.

Production containers were healthy and had no restarts. The app used 161 MiB of its existing 768 MiB cap; Caddy and Ops auth used 14 MiB and 16 MiB of their 128 MiB caps. Backups were configured, successful, and fresh; their units have CPU/IO weight 50 but no memory ceiling, so the 4 GiB reserve includes backup burst room.

The reviewed production safeguards were applied through the protected workflow on 2026-07-13T16:55Z, with Vercel sync disabled, Cloudflare DDNS disabled, and Grafana Alloy explicitly enabled to preserve the active host service. The workflow completed successfully: run 29268383268 reported ok=156, changed=11, failed=0.

This was a production-only reconciliation. It did not deploy or enable staging, send staging traffic, or change the staging route, credentials, or release identity.

The post-apply read-only check at 2026-07-13T17:01:48Z confirmed that the app had been recreated once and was healthy with these enforced Docker values:

  • CPU cap 4000000000 nanocpus (4 vCPUs), CPU shares 1024.
  • Memory limit 805306368 bytes (768 MiB), reservation 536870912 bytes (512 MiB), and PID limit 256.
  • json-file logging, max-size=10m, max-file=3.

The same sample showed 0.65 one-minute load, 9,472,016,384 bytes available memory, zero zram use, 74,190,782,464 bytes (69.1 GiB) free root disk, and 9,658,763 free inodes. The app used 61.6 MiB and 12 PIDs; Caddy used 14.9 MiB and 10 PIDs; Ops auth used 15.5 MiB and one PID. Alloy remained active and enabled. There were no kernel OOM events or system priority-error entries since the apply. Docker recorded one expected restart canceled warning while the managed old app container was stopped for recreation; the replacement container was healthy with restart count zero.

Relevant post-apply read-only commands were:

Terminal window
uptime; free -b; swapon --show --bytes
df -B1 --output=size,used,avail,pcent /; df -iP /
systemctl is-active alloy.service; systemctl is-enabled alloy.service
sudo docker inspect nutsnews-app
sudo docker ps; sudo docker stats --no-stream; sudo docker system df
sudo find /var/lib/docker/containers -name '*-json.log' -printf '%f %s\n'
sudo journalctl -k --since '2026-07-13 16:55:00 UTC' --no-pager
sudo journalctl -p err..alert --since '2026-07-13 16:55:00 UTC' --no-pager
sudo journalctl -u docker --since '2026-07-13 16:55:00 UTC' --no-pager

Do not enable staging merely because this document or a CI check is green. After the relevant staging deployment PR is approved and applied, use read-only SSH during one bounded qualification run:

Terminal window
uptime; free -h; swapon --show; df -h; df -i
sudo docker stats --no-stream
sudo docker inspect nutsnews-app nutsnews-app-staging
sudo docker system df
sudo journalctl -k --since '15 minutes ago' --no-pager
sudo journalctl -u docker.service --since '15 minutes ago' --no-pager

Verify production remains healthy, production memory and PID use stay below their current limits, root disk/inodes remain above reserve, staging stays at its hard limits, and there are no new OOM, ENOSPC, Docker, or application error signals attributable to the qualification run. Inspect Caddy request logs only for the bounded test window and confirm the traffic stays within the test budget.