{
    "title": "Debian 13 (Trixie) server set-up",
    "slug": "debian-13-trixie-server-set-up",
    "excerpt": "A practical, production-ready guide to setting up a Debian 13 web server using Apache, PHP-FPM, and MariaDB. Covers installation, performance tuning, security basics, and modern best practices.",
    "body": "Debian 13 has been out a while now and so I guess it is time for me to update my web server set-up guide.\n\nThere are lots of ways to set-up Debian 13 as a web server, I tend to go for a vanilla'ish Apache, MariaDB and PHP approach. This guide assumes you have already installed the base operating system and have shell access with sudo privileges.\n\n## Step 1: Install Packages\n\nRun the following command to install the packages:\n\n```\nsudo apt install git curl bat apache2 apache2-bin apache2-data apache2-utils mariadb-client mariadb-server php php-fpm php8.4 php-common php-gd php-getid3 php-mysql php8.4-fpm php8.4-cli php8.4-common php8.4-gd php8.4-mysql php-ldap php8.4-redis php8.4-opcache php8.4-soap php8.4-readline php8.4-curl php8.4-xml php-imagick php8.4-intl php8.4-zip php8.4-mbstring ssl-cert imagemagick php-imagick wget zip unzip redis-server php-curl php-bcmath php-gmp php-sqlite3 sqlite3 certbot ufw fail2ban\n```\n\n## Step 2: Configure Apache and PHP\n\nConfigure Apache to talk to PHP-FPM, activate the Apache config that connects Apache to PHP 8.4 via PHP-FPM (*this is the modern alternative to mod_php*) and enable rewrite, ssl, http2 and headers modules.\n\n```\nsudo a2enmod proxy_fcgi setenvif && sudo a2enconf php8.4-fpm && sudo systemctl reload apache2 && sudo a2enmod rewrite && sudo a2enmod ssl && sudo a2enmod http2 && sudo a2enmod headers && sudo systemctl restart apache2\n```\n\nTune PHP for larger uploads (128MB), longer-running scripts and more memory-heavy applications. Note, these values are quite generous, so adjust to suit.\n\n```\nsudo nano /etc/php/8.4/fpm/php.ini\n```\n\n```\n; --- Recommended production changes ---\n\nexpose_php = Off                     ; Hides PHP version in HTTP headers to prevent version-specific attacks\n\nmax_execution_time = 120             ; Limits script runtime (seconds) to prevent hung processes from hogging CPU\nmax_input_time = 120                 ; Max time (seconds) a script is allowed to parse input data (like POST/GET)\nmemory_limit = 256M                  ; Max amount of RAM a single script is allowed to consume\n\npost_max_size = 64M                  ; Maximum size allowed for the entire POST body (must be >= upload_max_filesize)\nupload_max_filesize = 64M            ; Maximum size allowed for a single uploaded file\nmax_file_uploads = 20                ; Maximum number of files that can be uploaded in one request\n\nmax_input_vars = 3000                ; Limits number of input variables (GET/POST/Cookie) to prevent Hash DoS attacks\n\ndate.timezone = Europe/London        ; Sets the default timezone for all date/time functions\n\ndisplay_errors = Off                 ; Prevents error details from being shown to users (security risk in prod)\ndisplay_startup_errors = Off         ; Hides errors that occur during PHP's startup sequence\nlog_errors = On                      ; Ensures errors are recorded to a log file for debugging\n\nrealpath_cache_size = 4096k          ; Size of the cache used by PHP to store file path resolutions for speed\nrealpath_cache_ttl = 600             ; How long (seconds) to cache file path information\n\nsession.use_strict_mode = 1          ; Prevents session fixation attacks by rejecting uninitialized session IDs\nsession.cookie_httponly = 1          ; Makes cookies inaccessible to JS, mitigating Cross-Site Scripting (XSS)\nsession.cookie_samesite = Lax        ; Restricts cookie sending to same-site requests to help prevent CSRF\n\nmysqli.allow_persistent = Off        ; Disables persistent MySQL connections to avoid leaking connection slots\npgsql.allow_persistent = Off         ; Disables persistent PostgreSQL connections\nodbc.allow_persistent = Off          ; Disables persistent ODBC connections\n\n[opcache]\nopcache.enable=1                     ; Enables the opcode cache to store precompiled script bytecode in RAM\nopcache.enable_cli=0                 ; Disables opcode caching for the Command Line Interface (usually unnecessary)\nopcache.memory_consumption=256       ; Amount of RAM (MB) dedicated to storing compiled PHP scripts\nopcache.interned_strings_buffer=16   ; RAM (MB) used to store identical strings (like variable names) once\nopcache.max_accelerated_files=20000  ; Max number of script files that can be cached (aim for prime numbers)\nopcache.max_wasted_percentage=10     ; Max wasted memory allowed before a restart is scheduled\nopcache.use_cwd=1                    ; Uses current working directory to avoid script collisions with same names\nopcache.validate_timestamps=1        ; Tells PHP to check if a file has changed based on its timestamp\nopcache.revalidate_freq=2            ; How often (seconds) to check file timestamps for changes\nopcache.save_comments=1              ; Keeps file comments; required by many modern frameworks for annotations\nopcache.enable_file_override=0       ; Prevents OPcache from overriding file_exists() and similar calls\nopcache.optimization_level=0x7FFEBFFF; Bitmask determining which internal compiler optimizations are applied\nopcache.file_update_protection=2     ; Prevents caching files that were modified less than X seconds ago\n```\n\nSave the file and restart the PHP service:\n\n```\nsudo systemctl restart php8.4-fpm\n```\n\n## Step 3: Install Node and Composer\n\nInstall Node LTS with the following commands:\n\n```\ncurl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - &&\\\nsudo apt-get install -y nodejs\nsudo npm install -g grunt-cli eslint\n```\n\nInstall Composer with the following command:\n\n```\nphp -r \"copy('https://getcomposer.org/installer', 'composer-setup.php');\"\nphp composer-setup.php\nphp -r \"unlink('composer-setup.php');\"\nsudo mv composer.phar /usr/local/bin/composer\n```\n\n## Step 4: Configure MariaDB\n\nCreate a new user:\n\n```\nsudo mariadb\nCREATE USER 'username'@'localhost' IDENTIFIED BY 'password';\nGRANT ALL PRIVILEGES ON *.* TO 'username'@'localhost' WITH GRANT OPTION;\n```\n\nCreate MariaDB user config file (*this prevents having to enter passwords when using mysql and mysqldump on the command line, handy for automating backups with cron*):\n\n```\nnano ~/.my.cnf\n```\n\nEnter the following, changing the username and password to suit:\n\n```\n[mysql]\nuser = username\npassword = secret\n\n[mysqldump]\nuser = username\npassword = secret\n```\n\nFix permissions on the file with the following command:\n\n```\nchmod 600 ~/.my.cnf\n```\n\nEdit MariaDB config file for performance:\n\n```\nsudo nano /etc/mysql/mariadb.conf.d/50-server.cnf\n```\n\nUse something similar to the below, adjust to suit server resources:\n\n```\n[mysqld]\n\n# GENERAL\nbind-address = 127.0.0.1           ; Restricts MySQL to listen only on localhost for security\nmax_connections = 200              ; Limits the total number of simultaneous client connections\n\n# INNODB (most important)\ninnodb_buffer_pool_size = 12G      ; Main memory cache for data/indexes (usually set to 70-80% of total RAM)\ninnodb_buffer_pool_instances = 8   ; Splits the buffer pool into chunks to reduce contention on multi-core systems\ninnodb_log_file_size = 1G          ; Size of redo logs; larger values improve write speed but slow down recovery\ninnodb_flush_method = O_DIRECT     ; Flushes data directly to disk, bypassing the OS cache to avoid double buffering\ninnodb_flush_log_at_trx_commit = 1 ; Ensures ACID compliance by flushing every transaction to disk (safest setting)\n\n# CACHE\nquery_cache_type = 0               ; Disables the legacy Query Cache (deprecated/removed in newer MySQL versions)\nquery_cache_size = 0               ; Sets Query Cache memory to zero to prevent overhead/locking issues\n\n# TEMP / TABLES\ntmp_table_size = 256M              ; Max size of internal in-memory temporary tables before they flip to disk\nmax_heap_table_size = 256M         ; Must match tmp_table_size to effectively handle large in-memory operations\n\n# THREADS\nthread_cache_size = 100            ; How many threads are kept in a pool to be reused for new connections\n\n# FILE LIMITS\nopen_files_limit = 65535           ; Max number of file descriptors the OS allows MySQL to open\ntable_open_cache = 4000            ; Number of open tables MySQL can keep cached to avoid repeated file opening\n\n# LOGGING (optional but useful)\nslow_query_log = 1                 ; Enables logging for queries that take a long time to execute\nslow_query_log_file = /var/log/mysql/slow.log ; Defines the physical path where the slow query log is saved\nlong_query_time = 2                ; Threshold (seconds) for what defines a \"slow\" query\n```\n\n## Step 5: File and Directory Permissions\n\nI tend to serve websites from my user's home directory. I use a shared group so both my user and the httpd user can read and write to the directories and files. Create a shared group with:\n\n```\nsudo groupadd webshared\n```\n\nAdd both users to the group:\n\n```\nsudo usermod -aG webshared username\nsudo usermod -aG webshared www-data\n```\n\nLog out/in (or `newgrp webshared`) for it to take effect.\n\nNext, change the ownership of the web directory and set the permissions to group write:\n\n```\nsudo chown -R username:webshared /home/username/public_html\nsudo chmod -R 2775 /home/username/public_html\n```\n\nEnsure new files stay writable (umask), set:\n\n```\numask 002\n```\n\n(in ~/.zshrc or ~/.bashrc or ~/.profile etc)\n\nFor the `www-data` user, edit the config:\n\n```\nsudo nano /etc/php/8.4/fpm/pool.d/www.conf\n```\n\nAdding:\n\n```\nphp_admin_value[umask] = 0002\n```\n\nThe full file should look something like:\n\n```\n[www]                                       ; Defines the name of this specific process pool\nuser = www-data                             ; The Unix user that the PHP processes will run as\ngroup = www-data                            ; The Unix group that the PHP processes will run as\nphp_admin_value[umask] = 0002               ; Sets default file permissions for new files (gives group write access)\n\nlisten = /run/php/php8.4-fpm.sock           ; Path to the Unix socket for Nginx/Apache to talk to PHP\nlisten.owner = www-data                     ; Owner of the socket file (must match web server user)\nlisten.group = www-data                     ; Group of the socket file\nlisten.mode = 0660                          ; Permissions for the socket (owner and group can read/write)\nlisten.backlog = 1024                       ; Max number of queued connection requests before refusal\n\npm = dynamic                                ; Enables dynamic scaling of child processes based on demand\npm.max_children = 40                        ; Max number of simultaneous child processes (prevents RAM exhaustion)\npm.start_servers = 8                        ; Number of child processes created on startup\npm.min_spare_servers = 4                    ; Minimum number of idle processes kept ready for spikes\npm.max_spare_servers = 12                   ; Maximum number of idle processes kept alive\npm.max_requests = 500                       ; Re-spawns a child after X requests to prevent memory leaks\n\npm.status_path = /fpm-status                ; URI to view real-time stats (load, active processes, etc.)\nping.path = /fpm-ping                       ; URI used by monitoring tools to check if PHP is alive\nping.response = pong                        ; The specific text response returned by the ping path\n\nrequest_slowlog_timeout = 10s               ; Log requests that take longer than 10 seconds to execute\nslowlog = /var/log/php8.4-fpm/www-slow.log  ; File path where slow execution stack traces are saved\n\nrequest_terminate_timeout = 180s            ; Force-kill processes that run longer than 3 minutes\nrequest_terminate_timeout_track_finished = yes ; Log if a process finished successfully after the timeout was reached\n\ncatch_workers_output = yes                  ; Redirects script stdout/stderr to the main FPM error log\nclear_env = yes                             ; Clears environment variables to prevent data leakage between scripts\nsecurity.limit_extensions = .php            ; Limits PHP execution to specific extensions for security\n\nphp_admin_flag[log_errors] = on             ; Forces error logging for this specific pool\nphp_admin_value[error_log] = /var/log/php8.4-fpm/www-error.log ; Path to the error log for this pool\n```\n\n## Step 6: Auto/Unattended Upgrades\n\nProbably best to keep the server software up-to-date.\n\n```\nsudo apt install unattended-upgrades apt-listchanges && sudo dpkg-reconfigure --priority=low unattended-upgrades\n```\n\n## Step 7: Firewall\n\nConfigure the firewall to allow SSH, HTTP and HTTPS, before enabling it:\n\n```\nsudo ufw allow OpenSSH\nsudo ufw allow WWW\nsudo ufw allow \"WWW Secure\"\nsudo ufw enable\nsudo ufw status verbose\n```\n\nYou can check the firewall status at any time with the following command:\n\n```\nsudo ufw status\n```\n\n## Step 8: Fail2Ban\n\nCreate a custom config file to work with the ufw firewall:\n\n```\nsudo nano /etc/fail2ban/jail.local\n```\n\nEnter the following:\n\n```\n[DEFAULT]\nbantime = 1h\nfindtime = 10m\nmaxretry = 5\nbanaction = ufw\n\n[sshd]\nenabled = true\n```\n\nEnable and start the service:\n\n```\nsudo systemctl enable fail2ban\nsudo systemctl start fail2ban\n```\n\n## Step 9: Misc\n\n### Set hostname\n\nYou may or may not need to adjust the system's hostname:\n\n```\nsudo hostnamectl set-hostname new-hostname\n```\n\n### Set timezone\n\nYou may or may not need to adjust the server's timezone:\n\n```\nsudo timedatectl set-timezone Europe/London\n```\n\n## Finishing touches\n\nFinishing touches might include things such as setting a different shell, importing or creating SSH keys etc. Other than that, any other changes would be specific to the applications being hosted or developed on the server.",
    "tags": [],
    "published_at": "2026-04-22 21:30:00",
    "url": "https://corenominal.com/blog/posts/debian-13-trixie-server-set-up",
    "featured_image": "https://corenominal.com/media/og-749e8c12-a72d-4a7c-8ffc-a3d9c875f849.png"
}