EST. 2026 — Notes from real server work SYSADMIN · SECURITY · AI · DIRECTADMIN Issue 01
THE SYSADMIN ALMANAC
Home / Tags / PHP
PHP

#PHP

40 articles
Web / CMS
Passing the Sandbox Doesn't Mean It Works — 7 False Signals
Linux

Passing the Sandbox Doesn't Mean It Works — 7 False Signals

The automated ordering system I wrote passed every command in a provider's sandbox, but almost everything the sandbox told me differed from reality, from response times and charges to accepted names and items that were created successfully but could not be found. Here are 7 false signals and how to write code that does not fall for them.

#api#rest-api#testing#debugging
9 min read·26 Oct
Bank Open API QR Payments — From Sandbox to Status Lifecycle
Web

Bank Open API QR Payments — From Sandbox to Status Lifecycle

A static PromptPay QR is easy to build yourself, but nothing tells you whether the customer has paid. This article walks through a bank's Open API sandbox, from requesting a token and issuing a dynamic QR to tracking REQUESTED/PAID/CANCELLED/VOIDED states and the requestDt trap that rejects requests even when the entire payload is correct.

#api#rest-api#payment#qrcode
10 min read·25 Oct
Let Clients Pick Photos on a Web Page Without a Login
Web

Let Clients Pick Photos on a Web Page Without a Login

When the person choosing photos for a website is not comfortable with computers, adding registration and login means the work stops at the login screen. This article covers a photo picker with no authentication at all, but with a server endpoint that checks every value against an allowlist strict enough to leave nothing from arbitrary input, plus limits at every layer and a backup before every write.

#php#javascript#web-security#permissions
8 min read·23 Oct
Build APM on Windows: Apache + PHP 8.4 + MariaDB Series Index
Web

Build APM on Windows: Apache + PHP 8.4 + MariaDB Series Index

A 7-part series on compiling Apache 2.4.69, PHP 8.4.27 and MariaDB 11.8.9 from source on Windows 11, so when PHP releases a security patch, I can change the version number in one file and rebuild within a day. This page covers the contents, overview, time, disk space and three-command shortcut.

#devops#php#apache#mariadb
9 min read·22 Oct
Build APM on Windows, Part 7: Deploy, Run, and Apply Security Patches
Web

Build APM on Windows, Part 7: Deploy, Run, and Apply Security Patches

The final part of the series on building Apache + PHP + MariaDB on Windows. Install into C:\APM, start and test it for the first time, open it to the LAN safely, walk through six real bugs (PidFile, MariaDB plugins, the DB port, the ASLR 500, paths with spaces and brackets), and cover the procedure for the next security release.

#devops#windows#apache#php
20 min read·22 Oct
Build APM on Windows Part 6: PHP 8.4 with redis and imagick
Web

Build APM on Windows Part 6: PHP 8.4 with redis and imagick

Part 6 of the series on building Apache + PHP + MariaDB on Windows from source walks through 30-php.ps1 step by step, from php-sdk and the full configure command to copying DLLs and testing that JIT, redis and imagick actually work.

#php#windows#powershell#tutorial
14 min read·22 Oct
Build APM on Windows, Part 5: Compile ImageMagick 7 for PHP imagick
Web

Build APM on Windows, Part 5: Compile ImageMagick 7 for PHP imagick

Part 5 of the Build APM on Windows series compiles ImageMagick 7.1.2-34 and around 40 delegate libraries from source with the official toolchain, including the channel mask patch from 64 to 32 that lets imagick build, and why HEIC cannot be written

#windows#php#devops#powershell
14 min read·22 Oct
Build APM on Windows, Part 1: Prepare Tools and Unpack the Build Kit
Web

Build APM on Windows, Part 1: Prepare Tools and Unpack the Build Kit

The first part of a series on building Apache + PHP 8.4 + MariaDB yourself on Windows. Install Visual Studio 2022 Build Tools with the required components, Git, CMake, Ninja and PowerShell 7, configure Defender, unpack the build kit into C:\apm-build, then examine the folders, versions.json and the lib.ps1 and build-all.ps1 scripts line by line.

#windows#devops#powershell#cpp
14 min read·22 Oct
Two Critical PHP RCE Flaws (php2shell): Patch Today
Security

Two Critical PHP RCE Flaws (php2shell): Patch Today

CVE-2026-109618 and CVE-2026-109619 are PHP RCE vulnerabilities with a CVSS score of 9.2 and public exploits already available. They affect every machine using PHP to handle web requests. The patched versions are 8.5.12, 8.4.27, 8.3.36, and 8.2.35. This note covers version checks, updates on Debian, RHEL, DirectAdmin, Docker, and Windows, and temporary settings while waiting for patches.

#cve#php#security#rce
10 min read·22 Oct
Building a Graph API Page App with Three Required Endpoints
Web

Building a Graph API Page App with Three Required Endpoints

When a backend needs to read and reply to Page messages on its own, the only proper route is to build an app and pass the platform's review. This article covers three endpoints the platform requires but people forget until they fail review, the correct way to verify incoming request signatures, and the steps to exchange a short-lived token for a Page token stored outside the web root.

#api#rest-api#php#web-security
10 min read·22 Oct
A 26-Page Bilingual Site: Why .php Broke Every Link
Web

A 26-Page Bilingual Site: Why .php Broke Every Link

The production page displayed PHP tags as raw text because the web server served .html as static files. Changing all 26 pages to .php broke links across the site. Then, after adding clean URLs, subpages bounced back to .php because the rewrite and 301 setup was only half done. This article includes the full .htaccess, eight cases for $base, and scripts that handle the whole site in one pass.

#htaccess#apache#nginx#php
9 min read·19 Oct
A Cookie Banner That Loads GTM and GA4 Only After Consent
Web

A Cookie Banner That Loads GTM and GA4 Only After Consent

Most cookie banners look good but block nothing, because GTM and GA4 have already loaded from the first line of the page. This article covers a setup that loads the container only after acceptance, puts GA4 inside GTM to prevent double counting, hides AdSense slots initially, and checks that outgoing requests to those services stay at zero.

#seo#adsense#javascript#playwright
9 min read·18 Oct
A Blank Page with No Errors, All Because I Named a Function top
Web

A Blank Page with No Errors, All Because I Named a Function top

After deployment, the page was blank, the logs had no errors, and node --check found nothing wrong. But the browser stopped the entire script before the first line ran because a function named top collided with window.top. Here are the names to avoid, the script loading order trap, and the Playwright check every project should have.

#javascript#debugging#troubleshooting#playwright
8 min read·18 Oct
Writing to DirectAdmin Through Panel Forms When the API Only Reads
DirectAdmin

Writing to DirectAdmin Through Panel Forms When the API Only Reads

Notes on letting a back-office app create and edit hosting packages in DirectAdmin when the official CMD_API only provides read operations, requiring POST requests directly to control panel forms, with verification rules that prohibit regex and two layers of kill switches disabled by default.

#directadmin#shared-host#php#laravel
9 min read·14 Oct
Ingesting Mail Without Disrupting Someone Else's Mailbox
Linux

Ingesting Mail Without Disrupting Someone Else's Mailbox

The mailbox the app fetches from is one people actually use every day — fetching with UNSEEN silently misses mail when the owner reads it on their phone before cron runs. The right approach is UID SEARCH paired with UIDVALIDITY and BODY.PEEK, with deduplication handled on our side.

#mail#imap#pop3#dovecot
10 min read·13 Oct
Let AI Read Documents, but Let Code Do the Math
AI

Let AI Read Documents, but Let Code Do the Math

The controller requires manually entered values to fall between 0 and 100, but the path where AI reads values from documents and feeds them in only checks whether they are numeric. A single negative value inflated the result to 5.25 times what it should have been without an error — a lesson in treating AI output as untrusted input, just like values entered by people.

#ai#llm#claude#claude-code
10 min read·12 Oct
The App Uses UTC, People Use Thai Time — 7 Hours Off, No Errors
Linux

The App Uses UTC, People Use Thai Time — 7 Hours Off, No Errors

Scheduled for three in the afternoon, published at ten at night. No errors, nothing wrong in the logs, because the app stores UTC while the person entering the time thinks in Thai time. This article traces the issue from an env setting that has no effect to the single line that writes Thai wall-clock time into a column everyone reads as UTC.

#laravel#php#mariadb#debugging
9 min read·11 Oct
Secrets Set to 600, but 0644 Logs Go Offsite Every Night
Security

Secrets Set to 600, but 0644 Logs Go Offsite Every Night

The .env is encrypted and permissions are locked down on every secret file, but customer data still leaks off the machine every night through something nobody classifies as confidential: logs that every account on the machine can read, bundled into backups uploaded to object storage. Here is how I stop it at the source by logging only result codes.

#security#logging#backup#offsite
10 min read·10 Oct
Web vs. Worker — mkdir 0700 Disables Inherited ACL Permissions
Linux

Web vs. Worker — mkdir 0700 Disables Inherited ACL Permissions

One app runs as two users, with php-fpm handling web requests and a separate account running the queue. Even with default ACLs fully configured, they still cannot write to each other's directories. The cause is mkdir with mode 0700, which sets a mask that wipes out the effective ACL permissions, plus a deletion trap that requires permissions on the directory, not the file.

#linux#permissions#posix-acl#php-fpm
9 min read·9 Oct
tinker Sends Mail but the Web Can't — proc_open Disabled in FPM
DirectAdmin

tinker Sends Mail but the Web Can't — proc_open Disabled in FPM

Sending mail from tinker works perfectly, but submitting the actual form sends nothing, and the mail log has no entries — because CLI and PHP-FPM are not the same PHP environment. Includes a working SMTP AUTH configuration for DirectAdmin hosting and the From rule you must get right.

#mail#smtp#laravel#php
9 min read·8 Oct
Site Not Updating After Deploy — Clear All Five Cache Layers
DirectAdmin

Site Not Updating After Deploy — Clear All Five Cache Layers

The files are uploaded, but the site still shows the old version, even after Ctrl+F5 — because five cache layers sit between the files on disk and what people see, and each needs a different clearing method. This article covers the sequence I use regularly, along with the trap that leaves the site stuck on 500 after clearing the view cache.

#deployment#laravel#php#php-fpm
10 min read·8 Oct
route:cache Turns Every GET into 405 — The Sub-path Trap
DirectAdmin

route:cache Turns Every GET into 405 — The Sub-path Trap

Deployment finished, I ran optimize as the guides suggested, and every page returned 405 Method Not Allowed even though route:list confirmed the routes were correct — a route cache that breaks only on certain servers, how to isolate it in 30 seconds, and three other issues I found while deploying an app under a sub-path.

#laravel#php#deployment#apache
9 min read·7 Oct
php -l Passes, Web Pages Return 500 — A Compile Gate Before Commit
Linux

php -l Passes, Web Pages Return 500 — A Compile Gate Before Commit

Templates broke twice in one month even though every php -l check passed, because php -l does not understand Blade syntax. This article covers the six-stage gate I put before commit — compiling the views and then linting the compiled files, plus a script that sweeps every GET route through the Kernel.

#laravel#php#blade#testing
9 min read·7 Oct
Always Claim the State Before Sending Anything Out
Linux

Always Claim the State Before Sending Anything Out

A "read the state, then send" guard looks safe until a worker gets killed after sending but before writing to the database, and a retry sends the same thing to the customer twice. This article covers the full set of duplicate prevention measures I use, from atomic claims to retry_after and failed() handlers that must never overwrite a successful state.

#laravel#php#queue#worker
11 min read·6 Oct
One Codebase, Two Roles: Hosted Web, Worker at Home
Linux

One Codebase, Two Roles: Hosted Web, Worker at Home

The whole system used to run on one machine behind a home internet connection. When that machine went down, the team could not use anything. I split it into two roles using the same codebase — the web runs on an always-on host, while heavy work stays on the home machine, which pulls jobs itself, with an atomic job-claim table and an 18-step cutover plan with a rollback path.

#laravel#php#queue#worker
12 min read·6 Oct
Too Many Alerts Mean No Alerts: Tuning a Bot People Will Read
Linux

Too Many Alerts Mean No Alerts: Tuning a Bot People Will Read

In one day, 28 events became 56 messages, and 57 percent of all events were repeat alerts every 30 minutes. This article covers recipient tiers, merging overlapping cycles, state that survives restarts, and why you must not add keepalive to a chat app session.

#monitoring#alerting#bot#logging
9 min read·5 Oct
A Single-Page PHP and SQLite Document Vault That Keeps Originals
Linux

A Single-Page PHP and SQLite Document Vault That Keeps Originals

A document photo storage app for government submissions, using photos taken on site with a phone. Each upload produces three files—the display image, the original, and saved crop settings—so adjustments can be made later without losing quality, with automatic migration from JSON to SQLite and web server restrictions on folders containing personal data.

#php#sqlite#document#security
9 min read·27 Sep
Printing Dozens of Government Forms from HTML with Correct Margins
Linux

Printing Dozens of Government Forms from HTML with Correct Margins

A government submission document system where users click Print in their browsers and every sheet must fit within its margins, from measuring the actual printable area in millimeters and repeating headers and footers without position fixed, to four image frame types for different documents and automated overflow checks with Playwright instead of inspecting every sheet.

#pdf#php#css#printing
10 min read·26 Sep
Auditing prod with real customer data without leaving a single row
Linux

Auditing prod with real customer data without leaving a single row

The system owner asked me to test the entire system on the live server holding customer data without leaving even a single junk row behind. My approach was to bootstrap Laravel myself and call controllers directly inside a transaction that I rolled back, with a COUNT of every table before and after as evidence.

#laravel#php#mysql#mariadb
11 min read·13 Sep
Moving Computation from PHP to the Browser: 12 Seconds to 12 ms
Linux

Moving Computation from PHP to the Browser: 12 Seconds to 12 ms

A single page ran tens of thousands of subqueries and took 12 seconds to load. I fixed it by precomputing a summary table, sending a single 107 KB dataset, and moving the formulas to the browser, bringing computation down to 12 milliseconds, with a parity test that requires JS results to match PHP on every row.

#performance#php#javascript#laravel
9 min read·29 Aug
Encrypt Secrets in .env and Decrypt Them When Config Loads
Security

Encrypt Secrets in .env and Decrypt Them When Config Loads

How to store passwords and API keys as ciphertext in Laravel's .env and have config decrypt them on load, using a single helper that detects encrypted values, plus the APP_KEY mismatch across environments that can bring everything down if you get it wrong.

#laravel#php#security#encryption
9 min read·18 Aug
Laravel Queues on Shared Hosting — Don't Share Redis with Cache
Linux

Laravel Queues on Shared Hosting — Don't Share Redis with Cache

If you're enabling queues for Laravel on shared hosting and planning to separate the queue from cache with different REDIS_DB numbers, that won't help. maxmemory and eviction apply to the whole instance, not individual DBs. This article explains why I chose the database driver instead, with complete systemd units to replace Supervisor.

#laravel#redis#queue#systemd
10 min read·17 Aug
Laravel Behind a Proxy Sees 127.0.0.1 — Silent Form, No Error
DirectAdmin

Laravel Behind a Proxy Sees 127.0.0.1 — Silent Form, No Error

A contact form on DirectAdmin says the sender is not human, sends no mail, and leaves every log silent — the cause is Laravel behind nginx-to-apache seeing $request->ip() as 127.0.0.1, failing the bot check and returning before the mail is sent, with an explanation of why TrustProxies must not use '*'

#laravel#php#nginx#php-fpm
9 min read·16 Aug
WAF Returns 406 for Thai Text in POST: Fixing It Without Root
Security

WAF Returns 406 for Thai Text in POST: Fixing It Without Root

ModSecurity on the host returns 406 for certain Thai sentences in the POST body, preventing forms from saving — but as a hosting tenant, I cannot change the rules myself. This article shows how to isolate the offending text through black-box testing, then work around it with JS encoding on submit + middleware decoding in Laravel.

#modsecurity#waf#laravel#php
9 min read·15 Aug
Bulk Import Legacy .xls Files into Laravel with PhpSpreadsheet
Linux

Bulk Import Legacy .xls Files into Laravel with PhpSpreadsheet

Legacy .xls files with Thai headers encoded in tis-620 have unreadable column names, large files make PhpSpreadsheet exhaust memory and crash, and files users merge themselves contain duplicate rows — use column positions instead of names, add a read filter, and deduplicate with a natural key.

#php#laravel#excel#phpspreadsheet
9 min read·10 Aug
Factory to Cloud Behind NAT: Push Snapshots and a Command Queue
Network

Factory to Cloud Behind NAT: Push Snapshots and a Command Queue

The production machine sits in a LAN behind NAT with no public IP, but the owner wants to view data on a cloud website and make edits from a phone that reach the internal machine — possible without opening ports or setting up a VPN, using push snapshots + an edit command queue + idempotent acks

#automation#networking#nat#api
10 min read·9 Aug
Migrating vBulletin 4.2.5 to Flarum: Lessons from 200,000 Posts
DirectAdmin

Migrating vBulletin 4.2.5 to Flarum: Lessons from 200,000 Posts

Migrating an old vBulletin 4.2.5 forum with over seven thousand members, over twenty thousand threads, and over two hundred thousand posts to Flarum — notes on pitfalls that cost real time, from guest userid 0 breaking FK constraints across an entire batch, nested md5 hashes for old passwords, and converting BBCode to s9e XML, to attachments stored on disk instead of in the DB and why I had to scp scripts instead of using heredoc

#flarum#vbulletin#migration#mariadb
11 min read·25 Jul
~30 Seconds Turns Tiny: Flarum's Litedown Subscript Trap
Linux

~30 Seconds Turns Tiny: Flarum's Litedown Subscript Trap

Write "~30 seconds" in a Flarum post and 30 turns into a tiny subscript — because Flarum's Litedown interprets ~ as a subscript opener. Thai users who use ~ to mean "approximately" run into this regularly. This article explains the cause, workarounds, and database fixes for content stored as s9e XML rather than raw markdown.

#flarum#markdown#debugging#php-fpm
6 min read·18 Jul
Auditing a Deployed Laravel Site — 3 Layers in Priority Order
Security

Auditing a Deployed Laravel Site — 3 Layers in Priority Order

After deploying Laravel to production, check security in 3 layers — probe externally for exposed endpoints, check files/permissions on the server, then grep for vulnerabilities in the code. This article provides commands and P0-P2 priorities for deciding what to fix first.

#security#hardening#php-fpm#cloudflare
8 min read·17 Jul
Legacy Apps Break After Migration — MariaDB Strict Mode Errors
Linux

Legacy Apps Break After Migration — MariaDB Strict Mode Errors

After moving a website from an old server to a new one, the registration form breaks even though the code has not changed — the cause is STRICT_TRANS_TABLES in the sql_mode of newer MariaDB versions, with fixes at 3 levels, from conf.d to per-app SET SESSION

#mariadb#mysql#migration#server-migration
7 min read·7 Jul