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