My OpenCode background server had reached a physical footprint of about 1.1 GiB. Investigating it with Codex led to a surprisingly small piece of code: a plugin loader that forgot its source fingerprints whenever a Location was disposed.
Node.js remembered the imported modules. OpenCode forgot that it could reuse them. The next Location imported the same files under fresh URLs, retaining another copy of the plugin and its dependency graph.
After applying the fix, the server's reported process footprint was 493.5 MB. A separate reproduction that repeatedly created and disposed the loader stayed around 5.4 MiB of heap after 40 loads; the original implementation reached 35.57 MiB. This post walks through the evidence, the lifetime mistake, and the tests that made the repair convincing.
This guide upgrades a dedicated Rocky Linux 9.2 server from PostgreSQL 16.x to
17.x in place with pg_upgrade. PostgreSQL 17 is installed alongside PostgreSQL
16, and the old data directory is retained for rollback.
The commands use --copy, not --link. Copy mode requires enough free disk
space for another copy of the cluster, but PostgreSQL 16 remains usable until
PostgreSQL 17 receives new production writes.
The paths below match installations made with the official PostgreSQL Yum
repository:
Set the PostgreSQL upgrade paths
exportOLD_BIN=/usr/pgsql-16/bin
exportNEW_BIN=/usr/pgsql-17/bin
exportOLD_DATA=/var/lib/pgsql/16/data
exportNEW_DATA=/var/lib/pgsql/17/data
exportUPGRADE_DIR=/var/lib/pgsql/upgrade-16-to-17
Adjust them if your current server uses different paths.
1. Inventory the PostgreSQL 16 cluster
Confirm the running version and data directory:
Check the PostgreSQL 16 version and data directory
Check tablespaces because their data also needs space during a copy-mode
upgrade:
List PostgreSQL tablespaces
sudo-upostgres"$OLD_BIN/psql"-X-c"
SELECT spcname, pg_tablespace_location(oid)
FROM pg_tablespace
ORDER BY spcname;
"
List extensions in every connectable database:
List extensions in every PostgreSQL database
sudo-iupostgresbash<<'EOF'
while IFS= read -r database; do
echo "=== Database: $database ==="
/usr/pgsql-16/bin/psql -X -d "$database" -c \
"SELECT extname, extversion FROM pg_extension ORDER BY extname;"
done < <(
/usr/pgsql-16/bin/psql -XAtc \
"SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate"
)
EOF
Install a PostgreSQL 17-compatible build of every extension containing native
code before running pg_upgrade. Do not manually create those extensions in the
empty PostgreSQL 17 cluster; pg_upgrade migrates their definitions.
2. Update PostgreSQL 16 to its latest minor release
First update the existing 16.x packages. Plan a short outage for this step.
Check disk space for the data directory and every user tablespace:
Check data size and available disk space
sudodu-sh"$OLD_DATA"
sudodf-h"$OLD_DATA"
A filesystem or VM snapshot taken while PostgreSQL is stopped provides an
additional rollback layer.
4. Install PostgreSQL 17 and extensions
Install PostgreSQL 17 alongside PostgreSQL 16:
Install PostgreSQL 17 packages
sudodnfinstall-y\
postgresql17-server\
postgresql17-contrib\
postgresql17-devel
If pgvector was installed from the PostgreSQL Yum repository, install its
PostgreSQL 17 package:
Install pgvector for PostgreSQL 17
sudodnfinstall-ypgvector_17
Prefer the RPM package when available and do not install pgvector with both RPM
and PGXN. If an extension is available only through PGXN, select the PostgreSQL
17 pg_config explicitly:
Install a PGXN extension for PostgreSQL 17
sudopgxnclientinstall\
--pg_config/usr/pgsql-17/bin/pg_config\
vector
Confirm that both versions are installed:
Check the installed PostgreSQL binaries
"$OLD_BIN/postgres"--version
"$NEW_BIN/postgres"--version
"$NEW_BIN/pg_upgrade"--version
5. Initialize an empty PostgreSQL 17 cluster
Initialize the new cluster with the encoding and locale recorded earlier. Based
on the original Rocky Linux installation, these are likely UTF8 and
en_US.UTF-8:
Initialize the PostgreSQL 17 cluster
sudoinstall-d-opostgres-gpostgres-m700"$NEW_DATA"
sudo-upostgres"$NEW_BIN/initdb"\
--pgdata="$NEW_DATA"\
--encoding=UTF8\
--locale=en_US.UTF-8
If pg_controldata reported checksum version 1, remove the newly initialized
empty directory and initialize it with checksums instead:
Initialize PostgreSQL 17 with data checksums
sudorm-rf"$NEW_DATA"
sudoinstall-d-opostgres-gpostgres-m700"$NEW_DATA"
sudo-upostgres"$NEW_BIN/initdb"\
--pgdata="$NEW_DATA"\
--encoding=UTF8\
--locale=en_US.UTF-8\
--data-checksums
Only remove NEW_DATA at this point, while it is a newly initialized empty
cluster. Do not start PostgreSQL 17 yet.
For example, an error about not loading $libdir/vector means the PostgreSQL 17
pgvector library is missing. Install pgvector_17, then rerun the check.
8. Run the PostgreSQL major upgrade
Run the real upgrade in copy mode:
Upgrade PostgreSQL 16 to PostgreSQL 17
sudo-iupostgresbash<<'EOF'
cd /var/lib/pgsql/upgrade-16-to-17
/usr/pgsql-17/bin/pg_upgrade \
--old-bindir=/usr/pgsql-16/bin \
--new-bindir=/usr/pgsql-17/bin \
--old-datadir=/var/lib/pgsql/16/data \
--new-datadir=/var/lib/pgsql/17/data \
--username=postgres \
--jobs="$(nproc)" \
--copy
EOF
Do not use --no-sync on a production server. Read the complete output and run
any required extension-update or index-rebuild scripts exactly as printed. Keep
the generated old-cluster deletion script, but do not run it yet.
9. Migrate configuration
Do not overwrite PostgreSQL 17's entire postgresql.conf with the PostgreSQL 16
file. Compare the files and manually reapply custom settings that remain valid:
Compare PostgreSQL 16 and 17 configuration files
sudodiff-u\
"$NEW_DATA/postgresql.conf"\
"$OLD_DATA/postgresql.conf"\
|less
Review settings such as listen_addresses, port, memory limits, WAL settings,
logging, SSL, and shared_preload_libraries. Any library named in
shared_preload_libraries must also be installed for PostgreSQL 17.
The client authentication files can normally be copied after review:
Switch the enabled service and start PostgreSQL 17:
Enable and start PostgreSQL 17
sudosystemctldisablepostgresql-16
sudosystemctlenablepostgresql-17
sudosystemctlstartpostgresql-17
sudosystemctlstatuspostgresql-17--no-pager
Verify the server identity, data directory, encoding, and locale:
Verify the upgraded PostgreSQL 17 cluster
sudo-upostgres"$NEW_BIN/psql"-X-c"
SELECT version();
SHOW data_directory;
SHOW server_encoding;
SHOW lc_collate;
SHOW lc_ctype;
"
sudo-upostgres"$NEW_BIN/pg_isready"
Check that all databases are present:
List upgraded PostgreSQL databases and sizes
sudo-upostgres"$NEW_BIN/psql"-X-c"
SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size
FROM pg_database
WHERE datallowconn
ORDER BY datname;
"
11. Update extensions
First run any extension-update script produced by pg_upgrade. Then list the
installed extension versions:
Check extensions after the PostgreSQL upgrade
sudo-iupostgresbash<<'EOF'
while IFS= read -r database; do
echo "=== Database: $database ==="
/usr/pgsql-17/bin/psql -X -d "$database" -c \
"SELECT extname, extversion FROM pg_extension ORDER BY extname;"
done < <(
/usr/pgsql-17/bin/psql -XAtc \
"SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate"
)
EOF
Update pgvector in each database that uses it:
Update and verify the pgvector extension
sudo-upostgres"$NEW_BIN/psql"\
--dbname=your_database\
--command="ALTER EXTENSION vector UPDATE;"
sudo-upostgres"$NEW_BIN/psql"\
--dbname=your_database\
--command="SELECT extversion FROM pg_extension WHERE extname = 'vector';"
Update other extensions only where they are installed:
Update PostgreSQL contrib extensions
ALTEREXTENSIONpg_trgmUPDATE;
ALTER EXTENSION btree_gist UPDATE;
12. Regenerate optimizer statistics
pg_upgrade does not transfer optimizer statistics. Regenerate them in stages
before returning the server to normal traffic:
Regenerate PostgreSQL optimizer statistics in stages
sudo-upostgres"$NEW_BIN/vacuumdb"\
--all\
--analyze-in-stages\
--jobs="$(nproc)"
After normal operation resumes, run a complete analyze:
Run a complete PostgreSQL analyze
sudo-upostgres"$NEW_BIN/vacuumdb"\
--all\
--analyze-only\
--jobs="$(nproc)"
13. Validate before deleting PostgreSQL 16
Review the service log:
Check recent PostgreSQL 17 service logs
sudojournalctl\
-upostgresql-17\
--since"1 hour ago"\
--no-pager
Check for invalid indexes in each important database:
Find invalid PostgreSQL indexes
sudo-upostgres"$NEW_BIN/psql"-X-dyour_database-c"
SELECT n.nspname AS schema_name, c.relname AS index_name
FROM pg_index i
JOIN pg_class c ON c.oid = i.indexrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE NOT i.indisvalid
ORDER BY 1, 2;
"
Validate database and table counts, important row counts, application login,
reads and writes, scheduled jobs, backups, monitoring, connection pools, and
pgvector searches and indexes. Keep the PostgreSQL 16 packages and
/var/lib/pgsql/16/data for an agreed retention period.
Roll back before production writes
Because the upgrade used --copy, PostgreSQL 16 remains unchanged. Before
PostgreSQL 17 receives production writes, rollback is straightforward:
Roll back to PostgreSQL 16 before new writes
sudosystemctlstoppostgresql-17
sudosystemctldisablepostgresql-17
sudosystemctlenablepostgresql-16
sudosystemctlstartpostgresql-16
sudosystemctlstatuspostgresql-16--no-pager
After clients write to PostgreSQL 17, restarting PostgreSQL 16 would discard
those new writes. Stop all clients and plan how to export or reconcile the
PostgreSQL 17 changes before attempting rollback.
Final cleanup
Only after PostgreSQL 17 has passed validation and fresh backups are working,
disable PostgreSQL 16 permanently:
Disable the old PostgreSQL 16 service
sudosystemctldisablepostgresql-16
After the retention period, remove the old packages. Review the DNF transaction
before accepting it so that libraries required by other software are not
removed:
Remove the old PostgreSQL 16 packages
sudodnfremove\
postgresql16-server\
postgresql16-contrib\
postgresql16-devel
Finally, delete the old cluster only when you are certain it is no longer needed.
Use the deletion script generated by pg_upgrade, or archive the old data
directory according to your backup and retention policy.
My previous Snell installation note was written for CentOS 7 in 2020. Several years later, CentOS 7 is obsolete, predictable network interface names are common, and Snell has moved to version 5.
This guide installs Snell Server on Rocky Linux 9, manages it with systemd, opens the required firewall ports, and optionally limits the server’s outbound network throughput using Linux Traffic Control.
Snell is a lightweight encrypted proxy protocol developed for Surge. The official server is distributed as a single binary with no external runtime dependencies other than glibc. Snell v5 also adds a QUIC proxy mode, which requires the server’s UDP port to be reachable.
1. Update Rocky Linux and install the required tools
Log in as root, or prefix the commands with sudo.
Update Rocky Linux and install prerequisites
dnfupdate-y
dnfinstall-ywgetunzipfirewalldiproute-tc
The iproute-tc package provides the tc command used later for bandwidth limiting.
Enable and start the firewall:
Enable and start firewalld
systemctlenable--nowfirewalld
systemctlstatusfirewalld
Rocky Linux 9 uses firewalld for common firewall management. Its runtime and permanent configurations are separate, so permanent rules must be explicitly added when they should survive a reboot.
2. Create a dedicated Snell account
Snell does not need an interactive login account:
Create the non-login Snell service account
useradd\
--system\
--home-dir/opt/snell\
--create-home\
--shell/sbin/nologin\
snell
Create the application directory:
Create and secure the Snell application directory
mkdir-p/opt/snell
chownsnell:snell/opt/snell
chmod750/opt/snell
3. Download Snell Server
At the time of writing, the latest official release is Snell Server 5.0.1.
YOUR_RANDOM_SECRET with the PSK from snell-server.conf.
The Snell v5 server remains backward compatible with v4 clients. However, QUIC proxy mode is a v5 feature.
8. Limit outbound network throughput
Some VPS providers charge for excess bandwidth or impose fair-use limits. Linux Traffic Control can apply a maximum outbound rate to a network interface.
Linux tc manages queueing disciplines that schedule packets as they leave an interface. The Token Bucket Filter, or TBF, is suitable for applying a simple maximum transmission rate.
Find the public network interface
Do not assume that the interface is named eth0. Rocky Linux systems frequently use names such as:
ens3
ens18
enp1s0
enp0s3
Find the interface used by the default route:
Show the default-route network interface
iprouteshowdefault
For example:
Example default-route output
default via 192.0.2.1 dev eth0 proto static metric 100
In this example, the interface is eth0.
You can extract it directly with:
Extract the default-route interface name
iprouteshowdefault|awk'{print $5; exit}'
Rocky Linux recommends modern tools such as ip and nmcli for network inspection and configuration.
Test the rule manually
The following command limits outbound traffic on eth0 to approximately 36 Mbit/s:
Apply a 36 Mbit/s outbound TBF limit
tcqdiscreplacedeveth0roottbf\
rate36mbit\
burst128kbit\
latency200ms
Check the active queueing discipline:
Inspect the active TBF queueing discipline
tc-sqdiscshowdeveth0
Remove the rule:
Remove the temporary TBF limit
tcqdiscdeldeveth0root
The limit applies to traffic transmitted through eth0. It therefore affects Snell, SSH, package downloads, web servers, and any other outbound service using that interface.
It does not directly limit inbound traffic. TCP downloads may nevertheless slow down indirectly because acknowledgements and response traffic leave through the rate-limited interface.
Replace eth0 with the actual public network interface found earlier.
The use of replace, rather than add, makes the service more tolerant of an existing root queueing discipline. The leading - in ExecStop tells systemd not to treat a missing queueing discipline as a fatal stop error.
Restarting the service reapplies the configured rate:
Restart netlimit to reapply the rate
systemctlrestartnetlimit
To disable bandwidth limiting:
Disable the persistent bandwidth limit
systemctldisable--nownetlimit
Then confirm that the TBF rule is gone:
Confirm that the TBF rule is removed
tcqdiscshowdeveth0
9. Testing the bandwidth limit
The most reliable test is to transfer a sufficiently large file from the Snell server or run a speed test from a remote client.
Remember the unit conversion:
Convert the configured Mbit/s rate to MB/s
36 Mbit/s ÷ 8 = approximately 4.5 MB/s
Protocol overhead means that the observed application-level transfer speed will normally be slightly lower than 4.5 MB/s.
You can watch the TBF counters while testing:
Watch TBF counters while testing
watch-n1'tc -s qdisc show dev eth0'
Pay attention to:
Sent bytes
Sent packets
Dropped packets
Overlimits
Backlog
An increasing overlimits counter is normal: it shows that TBF is delaying packets to enforce the configured rate. A large number of dropped packets or a continuously growing backlog may indicate that the burst or latency values need adjustment.
10. Maintenance commands
Restart Snell:
Restart the Snell service
systemctlrestartsnell
Check Snell:
Check Snell service status during maintenance
systemctlstatussnell--no-pager
View logs:
View Snell logs since today
journalctl-usnell--sincetoday
Review the configuration:
Inspect the Snell configuration
sudo-usnellcat/opt/snell/snell-server.conf
Check the firewall:
List the configured firewall ports
firewall-cmd--zone=public--list-ports
Check bandwidth limiting:
Inspect the active bandwidth-limit rule
tc-sqdiscshowdeveth0
Check both services after a reboot:
Check Snell and netlimit after reboot
systemctlis-activesnellnetlimit
systemctlis-enabledsnellnetlimit
11. Upgrading Snell Server
Download and extract the newer binary into a temporary directory, then stop the service and replace the existing executable:
Upgrading Snell Server
systemctlstopsnell
install\
-osnell\
-gsnell\
-m750\
/path/to/new/snell-server\
/opt/snell/snell-server
systemctlstartsnell
systemctlstatussnell--no-pager
The configuration file can normally remain in place, but release notes should be reviewed before each upgrade.
Conclusion
Compared with the old CentOS 7 setup, the Rocky Linux 9 version is mostly familiar:
Snell still runs as a small standalone binary.
systemd manages startup and recovery.
firewalld exposes the selected TCP and UDP ports.
tc and TBF provide a simple outbound bandwidth ceiling.
The main detail to watch is the network interface name. Copying eth0 blindly may cause netlimit.service to fail on servers whose public interface is named ens3, ens18, or enp1s0.
Finally, remember that the simple TBF rule limits the entire network interface. It is suitable for a dedicated Snell VPS. On a shared server, more advanced tc classes and filters would be required to limit only Snell traffic.
Today I encountered a classic "works on my machine... wait, no it doesn't" moment while setting up the Opencode Desktop project. Running bun run dev failed with a cryptic Error: Electron uninstall message. Here's the full story of how I diagnosed and fixed it.
To pin a specific version (e.g. 1.14.42), set the OPENCODE_VERSION env var before the build:
Build a specific x64 package version
OPENCODE_VERSION=1.14.42bun./scripts/prepare.ts
bunrunbuild
bunrunpackage:win----x64--publishnever
Follow-up: Native Module Mismatch
After building with --x64, the .exe may launch but crash with:
Follow-up: Native Module Mismatch
Error: Cannot find module './windowsTerminal'
Root cause: bun run build resolves native modules (e.g. @lydell/node-pty) based on the host platform and architecture — darwin-arm64 — rather than the target win32-x64. The bundle ends up importing the macOS native binding, which fails when the Windows .exe tries to load it at runtime.
The Fix
Set RUST_TARGET=x86_64-pc-windows-msvc so the build step selects the correct native bindings for the target platform:
The .exe built on macOS will not be Windows-code-signed. Electron-builder's signing routines typically gate on process.platform === "win32" (and often GITHUB_ACTIONS === "true"), so they intentionally skip signing on macOS. If you need a signed Windows release artifact, you must either build and sign on a Windows machine or add a macOS-compatible signing path to your config.
CLI build
Build the linux x64 cli
bunrunbuild:node--target=linux-x64
opencodeserve--service# to test if thape-config can write
In fact, the binary was fine. The code signature was not.
Diagnosis
A quick check with codesign revealed the real culprit:
Check the binary code signature
$ codesign -vv ./opencode
./opencode: invalid or unsupported format for signature
In architecture: arm64
On Apple Silicon, macOS enforces code signing strictly. A malformed or corrupt LC_CODE_SIGNATURE load command will cause the kernel to terminate the process immediately with SIGKILL (Killed: 9)—before a single line of your code runs.
You can confirm the signature load command exists with otool:
Confirm the code-signature load command
$otool-l./opencode|grepLC_CODE_SIGNATURE
cmdLC_CODE_SIGNATURE
The Fix
Remove the broken signature and replace it with a local ad-hoc signature: