How website hosting works here
Dedicated RAM, and where it actually goes
A Puranode website server is a container of its own, with its own nginx, its own php-fpm and the RAM on the plan card reserved for it rather than shared out. Starter is 1 GB with 1 vCPU, Business 2 GB with 2 vCPU, Pro 4 GB with 4 vCPU. A neighbour's traffic spike is genuinely not your problem. The flip side is that your own spike is entirely your problem, because there is no larger pool left to absorb it for you.
That makes the php-fpm pool the number worth understanding. nginx serving static files costs almost nothing; the RAM goes on PHP workers, and each worker holds a whole request in memory while it runs. memory_limit is a ceiling per request, not per container, so the arithmetic that matters is worker count multiplied by the realistic per-request footprint, measured against the container limit. Raising memory_limit on a 1 GB container does not buy safety. It buys a state where a few simultaneous heavy requests push the container past its limit and the process gets killed, instead of one request failing cleanly.
Disk behaves the same way. The 15, 35 and 75 GB NVMe figures are ceilings that Pterodactyl enforces, and the plan does not quietly grow to fit what you put in it.
- Static files are cheap; PHP workers are what consume the plan's RAM.
- Size the php-fpm worker count so the worst case fits inside the container, not so the average does.
- Everything written inside the container counts against the same disk allowance, including archives written by backup plugins.
Choosing between PHP 8.2 and 8.5
The container image is available for PHP 8.2, 8.3, 8.4 and 8.5, and the default is 8.3. Two things decide which one you want, and a third decides whether the change actually takes effect.
What usually forces the choice is not your code but somebody else's. A WordPress site is held to the oldest version its worst-maintained plugin or theme still runs on. A Composer application is held to whatever its platform constraint allows. Start a new site on the newest version everything in it declares support for, and treat anything that pins you to 8.2 as a dependency to plan your way off, since 8.2 is the oldest option on the list and the first to run out of upstream support.
Moving an existing site up a version normally shows up as log noise well before it shows up as breakage. PHP 8.2 deprecated dynamic properties, so older code that assigns undeclared properties to an object emits a deprecation for every one. PHP 8.4 deprecated implicitly nullable parameter types, the function f(Foo $x = null) shape that older libraries are full of. Neither stops a site working, and both fill logs fast. The egg's log-cleaner module is on by default and runs at container startup.
The mechanical trap is that the version lives in two places. You select a Docker image whose tag carries the version, and there is a separate PHP_VERSION variable that has to match that tag. Change one without the other and the container will not come up the way you expect. The egg's own field description warns about this, and it is still the step people skip.
- Test a version bump on a copy first. Deprecations are noisy long before anything breaks.
- Change the image tag and the PHP_VERSION variable together, then restart.
- A version change is a container restart, not a reinstall. Your files and database are untouched.
The free subdomain, and pointing a domain you own at it
Every website server gets a hostname under sites.puranode.com automatically. A wildcard record covers the whole zone, so there is nothing for you to create and nothing to wait for at your end. A sync job on our proxy picks up the new server, writes its vhost and requests a Let's Encrypt certificate for the hostname, which is why a new site is reachable over HTTPS without anyone raising a support ticket. Plain HTTP is redirected to HTTPS.
Bringing your own domain is a CNAME, created wherever your DNS is actually hosted, which is not always the registrar you bought the name from. Point the hostname you want at your sites.puranode.com address, email hello@puranode.com with the hostname, and we add it to the proxy and issue a certificate for it. Order matters here: the certificate is validated over HTTP against the live name, so DNS has to resolve to us before a certificate can exist. If your DNS provider has a proxying mode in front of the record, leave it off until HTTPS is working.
The apex is the part that catches people out. A bare example.com cannot be a CNAME, because the DNS specification does not allow a CNAME to sit alongside the SOA and NS records every zone apex has to carry. Some providers work around that with ALIAS, ANAME or CNAME flattening. If yours does not, put the site on www and redirect the apex with your registrar's forwarding.
One thing this does not touch: a CNAME on a hostname has no effect on your MX records. Puranode does not sell mailboxes and does not want to move your email, and pointing a hostname at us leaves it exactly where it is.
- Lower the record's TTL a day before moving a live site. The old TTL governs how long stale answers linger, not the new one.
- Create the CNAME first, then ask us to add the domain. Validation checks the live name.
- Apex domains need ALIAS, ANAME or flattening. Otherwise use www and redirect the apex.
- Leave your MX records alone. Nothing about this hosting requires changing them.
Your site runs behind a proxy, and PHP has to be told
TLS terminates at our proxy, which then speaks plain HTTP to your container on its allocated port. That arrangement is invisible until an application tries to work out how it was reached, and then it produces two very specific problems.
The first is the redirect loop. Your site's saved address is https, but the request arriving at PHP came in over http, so WordPress's is_ssl() returns false, WordPress redirects to its https URL, the proxy forwards that as http again, and the browser eventually gives up. The proxy sends X-Forwarded-Proto, so the fix is the standard wp-config.php snippet that sets $_SERVER['HTTPS'] to on when that header says https, placed above the line that requires wp-settings.php. Laravel and Symfony solve the same problem with their trusted-proxy settings.
The second is addresses. REMOTE_ADDR is the proxy, so from inside PHP every visitor looks like the same client. Security plugins that rate-limit logins, comment spam filters, country rules and any analytics reading the remote address will all see one address until you point them at X-Forwarded-For, which the proxy also sets. This is worth doing before you rely on a login limiter, not after.
A related note on the Certbot module inside the container: leave it switched off, which is how it ships. Certificates for your sites.puranode.com hostname and for any custom domain are issued and renewed on the proxy, which is where TLS actually terminates.
- Add the X-Forwarded-Proto snippet before you set the site URL to https, not after the loop starts.
- Configure security and analytics plugins to trust X-Forwarded-For, or they will treat every visitor as one client.
- Leave the container's own Certbot and Cloudflare Tunnel modules off unless you have a specific reason to run them.
Deploying over SFTP, and what lives beside the web root
SFTP credentials come from the panel, the same way they do for a game server. You land in the container's home directory, and the document root is /home/container/www. The directories beside it are all real: nginx/conf.d/default.conf is your vhost, php/ holds the PHP configuration, logs/ is where the server's own logs land, and tmp/ is scratch space. Upload into www, not into the level above it, which is the most common first-deploy mistake.
Two things about that vhost. You can edit it, add rewrite rules, change the index directive and restart. You should not touch the listen line, because the panel rewrites it from the server's allocation every time the container starts. And because our proxy passes the original Host header through, one container's nginx can serve more than one hostname from a single allocation, each with its own root inside www. That applies to Business and Pro; Starter is sold as a single-website plan. Every extra hostname still needs its own CNAME and its own certificate on the proxy, so that part is a request to us rather than something that appears on its own.
A fresh server with WordPress switched off and no git repository configured serves one index.php containing phpinfo(). Replace or delete it before pointing a real domain at the site, because it publishes paths, loaded extensions and environment details that nobody outside needs.
The git module clones your repository into www, and updates use git pull --ff-only, which refuses rather than merges. Editing tracked files in www over SFTP on a git-deployed site therefore makes the next update fail instead of merging. Choose one method per site. Optional Composer and cron modules sit alongside it, the cron one being how you run a real scheduler instead of leaving WordPress to fire its own on page loads.
- Web root is /home/container/www. Anything above it is configuration, not public.
- Delete the default phpinfo() page on day one.
- SFTP or git deploy, not both, in the same web root.
- The listen line in the vhost is managed by the panel. Edit anything else you like.
Databases, backups and the limits we are not going to pretend away
Starter is sold as one website with one MySQL database. Business includes three databases and Pro five, and on those plans the stated limit counts databases rather than sites, which is a difference worth thinking about. WordPress wants one each. Two installs can share a single database using different table prefixes, and it does work, but it couples them permanently: restoring one overwrites the other, and a plugin that drops its own tables can reach across. A staging copy also wants a database of its own, which is normally why the allowance turns out to be tighter than it looks.
The database does not run inside your container. The panel gives you an endpoint host and port, a generated username and a password, and that endpoint is what belongs in wp-config.php rather than localhost. Databases also count against the allowance whether anything is using them or not, so remove the ones you have finished with.
Backups run nightly at 04:17 server time, keeping seven days locally and three days off-site in Cloudflare R2. That is our own disaster-recovery copy rather than a self-serve restore in your panel, and it is worth being exact about what it does not do. It is nightly, so up to a day of changes sits outside it. It cannot help with something you notice after the retention window has passed. And a snapshot faithfully preserves a site that was already broken when it ran, so it is not a substitute for taking your own copy before a core or plugin update.
The remaining two limits are structural. There is one region, EU-Central in Helsinki, which is good for Europe and adds a round trip for everyone else on every uncached request. And a RAM change is a manual resize we do the same day you ask, during business hours, not a self-serve slider.
- Use the panel's database endpoint in wp-config.php. It is not localhost.
- Take your own copy before any core, theme or plugin update.
- One region, Helsinki. If most of your visitors are outside Europe, put a CDN in front of the site or host closer to them.