101.99.94.121 and hestiacp30usd.vpsme.win as examples. Replace them with your actual VPS IP and hostname in every step where they appear.
Purpose
This guide is for a fresh Debian 12 VPS installation of HestiaCP. It is designed to be reusable for every new server you provision.
- Clean install with no leftover packages
- No firewall conflicts (iptables / UFW)
- No File Manager crash
- Stable panel access with no session issues
- Good performance out of the box
- Cloudflare-compatible setup
- Reusable process for future installs
Important Rules
Follow these strictly. Every rule exists because breaking it has caused real production issues.
- Do not improvise
- Do not install UFW
- Do not flush iptables manually
- Do not use random fixes from forums without understanding them
- Do not use
apt dist-upgrade - Do not access the Hestia panel using mixed methods (IP and hostname) during the same session
- Use one access method consistently
Server Details Template
Fill in your values before starting. Keep this reference handy.
| Parameter | Value |
|---|---|
| VPS IP | YOUR_SERVER_IP |
| Hostname (FQDN) | your.hostname.tld |
| SSH Port | 20203 |
| Hestia Admin Username | admin |
| Hestia Admin Password | •••••••• |
| Admin Email | [email protected] |
Pre-Install Checks
Connect to the server and verify it is completely clean.
ssh -p 20203 root@YOUR_SERVER_IP
Check OS version and detect any pre-existing packages:
cat /etc/os-release | grep PRETTY
dpkg -l | grep -E "nginx|apache|mysql|mariadb|hestia"
Hostname Setup
Set the hostname correctly. This is critical for SSL, panel cookies, and session stability.
hostnamectl set-hostname hestiacp30usd.vpsme.win
Edit /etc/hosts:
nano /etc/hosts
Delete everything in the file — including the # Generated by SolusVM comment and any auto-generated lines from your VPS provider. Replace with this:
127.0.0.1 localhost localhost.localdomain
127.0.1.1 hestiacp30usd.vpsme.win hestiacp30usd
101.99.94.121 hestiacp30usd.vpsme.win hestiacp30usd
::1 localhost localhost.localdomain
101.99.94.121 with your actual VPS IP and hestiacp30usd.vpsme.win with your actual hostname before saving.
Save: press Ctrl+O, then press Enter to confirm the filename, then press Ctrl+X to exit nano.
Verify:
hostname -f
hestiacp30usd.vpsme.win
System Update
apt update && apt upgrade -y
apt install -y ca-certificates curl wget
apt dist-upgrade. It can replace kernel packages and break VPS environments.
Cloudflare DNS Before Install
Before installing HestiaCP, create the DNS record in Cloudflare Dashboard.
| Type | Name | Content | Proxy Status |
|---|---|---|---|
| A | hestiacp30usd |
101.99.94.121 |
DNS only |
101.99.94.121 with your actual VPS IP and hestiacp30usd with your actual hostname subdomain.
HestiaCP Installation
Download the official installer:
cd /root
wget https://raw.githubusercontent.com/hestiacp/hestiacp/release/install/hst-install.sh
Run the interactive installer. Answer prompts carefully:
bash hst-install.sh
When asked for FQDN hostname, enter: hestiacp30usd.vpsme.win
Recommended stack choices:
Reboot After Install
reboot
Wait 30 seconds, then reconnect:
ssh -p 20203 [email protected]
Verify Services
v-list-sys-services
If something is stopped, inspect it:
systemctl status SERVICE_NAME
journalctl -u SERVICE_NAME --no-pager -n 50
Panel Access
Preferred panel access URL:
https://hestiacp30usd.vpsme.win:8083
Firewall Golden Rule
One firewall system only. Hestia manages iptables directly.
- Use only Hestia firewall commands
- Do not install UFW
- Do not manually flush iptables
- Do not mix firewall systems
- All rule changes go through
v-add-firewall-ruleandv-update-firewall
SSH & File Manager — Critical Fix
This is the most important edit in this guide. It prevents File Manager crashes caused by SSH port changes.
Open the SSH config file:
nano /etc/ssh/sshd_config
You will see your current config already has Port 20203 (your custom SSH port). It looks like this:
Port 20203
Add Port 22 on a new line directly above Port 20203, so it becomes:
Port 22
Port 20203
Port 20203 line. Do not touch anything else in the file. Just add Port 22 above it.
Save: press Ctrl+O, then press Enter to confirm the filename, then press Ctrl+X to exit nano.
Restart SSH:
systemctl restart sshd
localhost:22 via SFTP to browse your files. If port 22 is missing, File Manager breaks — even though your external SSH on 20203 still works fine. The firewall blocks port 22 from outside, so it only works locally. Your SSH login still uses port 20203 as before.
Hestia Firewall Rule for Custom SSH
v-add-firewall-rule ACCEPT 0.0.0.0/0 20203 TCP "Custom SSH"
v-update-firewall
Verify the rule exists:
v-list-firewall
Fail2ban
fail2ban-client status
Check SSH jail specifically:
fail2ban-client status ssh-iptables
Hestia already created /etc/fail2ban/jail.local with all the jails, but the SSH jail is missing your custom port. Instead of editing inside nano (easy to mess up), run this single command to replace the file with the correct version. Copy and paste it directly in your terminal — not inside nano:
printf '[ssh-iptables]\nenabled = true\nfilter = sshd\naction = hestia[name=SSH]\nlogpath = /var/log/auth.log\nport = 20203\nmaxretry = 5\nbantime = 3600\nfindtime = 600\n\n[vsftpd-iptables]\nenabled = true\nfilter = vsftpd\naction = hestia[name=FTP]\nlogpath = /var/log/vsftpd.log\nmaxretry = 5\n\n[exim-iptables]\nenabled = true\nfilter = exim\naction = hestia[name=MAIL]\nlogpath = /var/log/exim4/mainlog\n\n[dovecot-iptables]\nenabled = true\nfilter = dovecot\naction = hestia[name=MAIL]\nlogpath = /var/log/dovecot.log\n\n[mysqld-iptables]\nenabled = false\nfilter = mysqld-auth\naction = hestia[name=DB]\nlogpath = /var/log/mysql/error.log\nmaxretry = 5\n\n[hestia-iptables]\nenabled = true\nfilter = hestia\naction = hestia[name=HESTIA]\nlogpath = /var/log/hestia/auth.log\nmaxretry = 5\n\n[roundcube-auth]\nenabled = false\nfilter = roundcube-auth\naction = hestia[name=WEB]\nlogpath = /var/log/roundcube/errors.log\nmaxretry = 5\n\n[phpmyadmin-auth]\nenabled = true\nfilter = phpmyadmin-syslog\naction = hestia[name=WEB]\nlogpath = /var/log/auth.log\nmaxretry = 5\n\n[recidive]\nenabled = true\nfilter = recidive\naction = hestia[name=RECIDIVE]\nlogpath = /var/log/fail2ban.log\nmaxretry = 5\nfindtime = 86400\nbantime = 864000\n' > /etc/fail2ban/jail.local
jail.local file with all Hestia default jails. The only change from the original is three lines added to [ssh-iptables]: port = 20203, bantime = 3600, and findtime = 600. The printf method avoids the double-spacing issue that cat heredocs can cause when pasting from a browser.
Now restart and verify:
systemctl restart fail2ban
fail2ban-client status
File Manager Fix & Policy
SessionStorage.php is missing the migrate() method that FileGator expects. Reinstalling the File Manager does not fix this — you must patch the file manually.
Run this command to replace the broken file with the fixed version. Paste directly in terminal:
printf '<?php\nnamespace Filegator\\Services\\Session\\Adapters;\n\nuse Filegator\\Kernel\\Request;\nuse Filegator\\Services\\Service;\nuse Filegator\\Services\\Session\\Session;\nuse Filegator\\Services\\Session\\SessionStorageInterface;\n\nclass SessionStorage implements Service, SessionStorageInterface {\n\n protected $request;\n protected $config;\n\n public function __construct(Request $request) {\n $this->request = $request;\n }\n\n public function init(array $config = []) {\n if (!$this->getSession()) {\n $handler = $config["handler"];\n $session = new Session($handler());\n $this->setSession($session);\n }\n }\n\n public function save() {\n $this->getSession()->save();\n }\n\n public function set(string $key, $data) {\n return $this->getSession()->set($key, $data);\n }\n\n public function get(string $key, $default = null) {\n return $this->getSession() ? $this->getSession()->get($key, $default) : $default;\n }\n\n public function invalidate() {\n if (!$this->getSession()->isStarted()) {\n $this->getSession()->start();\n }\n $this->getSession()->invalidate();\n }\n\n public function migrate($destroy = false, $lifetime = null): bool {\n if ($this->getSession() && $this->getSession()->isStarted()) {\n return $this->getSession()->migrate($destroy, $lifetime);\n }\n return false;\n }\n\n private function setSession(Session $session) {\n return $this->request->setSession($session);\n }\n\n private function getSession(): ?Session {\n return $this->request->getSession();\n }\n}\n' > /usr/local/hestia/web/fm/backend/Services/Session/Adapters/SessionStorage.php
Restart Hestia:
systemctl restart hestia
Test the File Manager in your browser — it should work now.
v-add-sys-filemanager.
General File Manager policy:
Do not rely heavily on Hestia File Manager for production work. Use it only as a convenience tool for quick checks.
Preferred workflow tools:
- VS Code Remote SSH
- SFTP via FileZilla or WinSCP
nanovia SSH
NGINX Performance Tuning
You can verify by checking the config:
cat /etc/nginx/nginx.conf
What Hestia already sets for you:
worker_processes auto— matches CPU coresworker_connections 1024+multi_accept on+use epollgzip onwith a comprehensive list of MIME typesopen_file_cache max=10000— caches file metadata in memoryclient_max_body_size 1024m— allows large uploads- Cloudflare real IP headers included
- SSL/TLS 1.2 + 1.3 with strong ciphers
- FastCGI and proxy cache preconfigured
/etc/nginx/nginx.conf — Hestia may overwrite your changes on updates. If you need custom NGINX settings for a specific site, use Hestia's template system instead.
PHP-FPM Tuning
/etc/php/8.3/fpm/pool.d/yourdomain.conf). These files say "DO NOT MODIFY" at the top — Hestia overwrites them when rebuilding domains. To change pool settings, use Hestia's PHP-FPM templates instead.
What you can safely edit is php.ini — this applies globally to all PHP-FPM pools.
First check your PHP version:
php -v
Then check the current php.ini values (replace 8.3 with your version if different):
cat /etc/php/8.3/fpm/php.ini | grep -E "upload_max_filesize|post_max_size|memory_limit|max_execution_time|max_input_vars"
Hestia defaults are already reasonable. If you need more memory or execution time for heavy apps like WordPress + WooCommerce, run these commands (replace 8.3 with your PHP version):
sed -i 's/^memory_limit = 128M/memory_limit = 256M/' /etc/php/8.3/fpm/php.ini
sed -i 's/^max_execution_time = 60/max_execution_time = 300/' /etc/php/8.3/fpm/php.ini
systemctl restart php8.3-fpm
Verify the changes:
cat /etc/php/8.3/fpm/php.ini | grep -E "memory_limit|max_execution_time"
What these changes do:
memory_limit 256M— allows PHP scripts to use more RAM (needed for heavy plugins). Does not reserve RAM permanently, just raises the ceiling per requestmax_execution_time 300— gives slow tasks (imports, backups, updates) up to 5 minutes instead of 60 seconds. Normal pages still finish in under 1 second
The rest of the defaults are already good:
upload_max_filesize = 100M— already generouspost_max_size = 100M— matches upload limitmax_input_vars = 4000— already higher than the typical 3000 recommendation
Save: press Ctrl+O, then press Enter to confirm the filename, then press Ctrl+X to exit nano. Then restart PHP-FPM:
systemctl restart php8.3-fpm
8.3 with your actual PHP version in all commands. Check with php -v.
MariaDB Basic Optimization
Hestia's default MariaDB config is mostly comments with few active settings. Adding some InnoDB and connection tuning improves performance for WordPress and other database-heavy apps.
First check your RAM to decide buffer sizes:
free -m
Then run this command to add optimization settings. These are safe for a 4GB RAM VPS — adjust innodb_buffer_pool_size if you have more or less RAM:
printf '\n# === Custom optimization (added by setup guide) ===\n[mysqld]\ninnodb_buffer_pool_size = 256M\ninnodb_log_file_size = 64M\ninnodb_flush_log_at_trx_commit = 2\ninnodb_flush_method = O_DIRECT\nmax_connections = 100\nwait_timeout = 300\ninteractive_timeout = 300\ntmp_table_size = 32M\nmax_heap_table_size = 32M\nslow_query_log = 1\nslow_query_log_file = /var/log/mysql/slow.log\nlong_query_time = 2\n' >> /etc/mysql/mariadb.conf.d/50-server.cnf
>> not >) — it does not overwrite existing settings.
Restart MariaDB:
systemctl restart mariadb
Verify MariaDB is running:
systemctl status mariadb | head -5
What these settings do:
innodb_buffer_pool_size = 256M— RAM reserved for caching database data. 256M is safe for a 4GB VPS. Use 512M if you have 8GB+innodb_flush_log_at_trx_commit = 2— faster writes, tiny risk of losing 1 second of data on crash (fine for web hosting)innodb_flush_method = O_DIRECT— avoids double-buffering with the OS, saves RAMmax_connections = 100— prevents too many connections from overwhelming the serverwait_timeout = 300— closes idle connections after 5 minutes instead of 8 hours (default)slow_query_log— logs queries taking over 2 seconds so you can find and fix slow queries later
journalctl -u mariadb --no-pager -n 20. To undo, edit the file with nano /etc/mysql/mariadb.conf.d/50-server.cnf and remove the lines you just added at the bottom.
Cloudflare for Websites
Rules for running websites behind Cloudflare with HestiaCP:
| Record | Proxy | Notes |
|---|---|---|
| Panel hostname | DNS only | Always gray cloud |
| Website domains | Proxied | Orange cloud for CDN + protection |
| Mail records (MX) | DNS only | Cannot be proxied |
- SSL mode: Full (strict) — never Flexible
- Cloudflare does not proxy port 8083
- Respect existing cache headers for static assets
- Cache CSS, JS, and images
- Do not cache admin panels or authenticated pages
Recommended Daily Workflow
| Task | Tool |
|---|---|
| Code editing & deployment | VS Code Remote SSH |
| File transfers | SFTP (FileZilla / WinSCP) |
| Domain & user management | HestiaCP Panel (browser) |
| Database management | phpMyAdmin (via Hestia) |
| Emergency file browsing | Hestia File Manager (backup only) |
Backups
Use Hestia built-in backups and keep off-server copies for critical data.
v-list-user-backups admin
Quick Commands
Click any command to copy it.
Final Checklist
Verify every item before going live.
- Hostname resolves correctly (
hostname -f) - SSH works on port 20203
- SSH still listens on port 22 internally (for File Manager)
- Panel opens correctly at
:8083 - Firewall has port 20203 allowed
- No UFW installed
- Fail2ban active with correct SSH port
- Cloudflare hostname record is DNS-only (gray cloud)
- Website domains can be proxied through Cloudflare (orange cloud)
- Cloudflare SSL mode is Full (strict)
- PHP-FPM tuned and running
- NGINX gzip enabled
- MariaDB optimized
- File Manager tested — opens without errors
- Backups configured