Skip to main content

Debian 13 (Trixie) server set-up

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.

There 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.

Step 1: Install Packages

Run the following command to install the packages:

sudo 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

Step 2: Configure Apache and PHP

Configure 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.

sudo 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

Tune PHP for larger uploads (128MB), longer-running scripts and more memory-heavy applications. Note, these values are quite generous, so adjust to suit.

; --- Recommended production changes ---

expose_php = Off                     ; Hides PHP version in HTTP headers to prevent version-specific attacks
 
max_execution_time = 120             ; Limits script runtime (seconds) to prevent hung processes from hogging CPU
max_input_time = 120                 ; Max time (seconds) a script is allowed to parse input data (like POST/GET)
memory_limit = 256M                  ; Max amount of RAM a single script is allowed to consume

post_max_size = 64M                  ; Maximum size allowed for the entire POST body (must be >= upload_max_filesize)
upload_max_filesize = 64M            ; Maximum size allowed for a single uploaded file
max_file_uploads = 20                ; Maximum number of files that can be uploaded in one request

max_input_vars = 3000                ; Limits number of input variables (GET/POST/Cookie) to prevent Hash DoS attacks

date.timezone = Europe/London        ; Sets the default timezone for all date/time functions

display_errors = Off                 ; Prevents error details from being shown to users (security risk in prod)
display_startup_errors = Off         ; Hides errors that occur during PHP's startup sequence
log_errors = On                      ; Ensures errors are recorded to a log file for debugging

realpath_cache_size = 4096k          ; Size of the cache used by PHP to store file path resolutions for speed
realpath_cache_ttl = 600             ; How long (seconds) to cache file path information

session.use_strict_mode = 1          ; Prevents session fixation attacks by rejecting uninitialized session IDs
session.cookie_httponly = 1          ; Makes cookies inaccessible to JS, mitigating Cross-Site Scripting (XSS)
session.cookie_samesite = Lax        ; Restricts cookie sending to same-site requests to help prevent CSRF

mysqli.allow_persistent = Off        ; Disables persistent MySQL connections to avoid leaking connection slots
pgsql.allow_persistent = Off         ; Disables persistent PostgreSQL connections
odbc.allow_persistent = Off          ; Disables persistent ODBC connections

[opcache]
opcache.enable=1                     ; Enables the opcode cache to store precompiled script bytecode in RAM
opcache.enable_cli=0                 ; Disables opcode caching for the Command Line Interface (usually unnecessary)
opcache.memory_consumption=256       ; Amount of RAM (MB) dedicated to storing compiled PHP scripts
opcache.interned_strings_buffer=16   ; RAM (MB) used to store identical strings (like variable names) once
opcache.max_accelerated_files=20000  ; Max number of script files that can be cached (aim for prime numbers)
opcache.max_wasted_percentage=10     ; Max wasted memory allowed before a restart is scheduled
opcache.use_cwd=1                    ; Uses current working directory to avoid script collisions with same names
opcache.validate_timestamps=1        ; Tells PHP to check if a file has changed based on its timestamp
opcache.revalidate_freq=2            ; How often (seconds) to check file timestamps for changes
opcache.save_comments=1              ; Keeps file comments; required by many modern frameworks for annotations
opcache.enable_file_override=0       ; Prevents OPcache from overriding file_exists() and similar calls
opcache.optimization_level=0x7FFEBFFF; Bitmask determining which internal compiler optimizations are applied
opcache.file_update_protection=2     ; Prevents caching files that were modified less than X seconds ago

Save the file and restart the PHP service:

sudo systemctl restart php8.4-fpm

Step 3: Install Node and Composer

Install Node LTS with the following commands:

curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - &&\
sudo apt-get install -y nodejs
sudo npm install -g grunt-cli eslint

Install Composer with the following command:

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php
php -r "unlink('composer-setup.php');"
sudo mv composer.phar /usr/local/bin/composer

Step 4: Configure MariaDB

Create a new user:

sudo mariadb
CREATE USER 'username'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON *.* TO 'username'@'localhost' WITH GRANT OPTION;

Create 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):

nano ~/.my.cnf

Enter the following, changing the username and password to suit:

[mysql]
user = username
password = secret

[mysqldump]
user = username
password = secret

Fix permissions on the file with the following command:

chmod 600 ~/.my.cnf

Edit MariaDB config file for performance:

sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf

Use something similar to the below, adjust to suit server resources:

[mysqld]

# GENERAL
bind-address = 127.0.0.1           ; Restricts MySQL to listen only on localhost for security
max_connections = 200              ; Limits the total number of simultaneous client connections

# INNODB (most important)
innodb_buffer_pool_size = 12G      ; Main memory cache for data/indexes (usually set to 70-80% of total RAM)
innodb_buffer_pool_instances = 8   ; Splits the buffer pool into chunks to reduce contention on multi-core systems
innodb_log_file_size = 1G          ; Size of redo logs; larger values improve write speed but slow down recovery
innodb_flush_method = O_DIRECT     ; Flushes data directly to disk, bypassing the OS cache to avoid double buffering
innodb_flush_log_at_trx_commit = 1 ; Ensures ACID compliance by flushing every transaction to disk (safest setting)

# CACHE
query_cache_type = 0               ; Disables the legacy Query Cache (deprecated/removed in newer MySQL versions)
query_cache_size = 0               ; Sets Query Cache memory to zero to prevent overhead/locking issues

# TEMP / TABLES
tmp_table_size = 256M              ; Max size of internal in-memory temporary tables before they flip to disk
max_heap_table_size = 256M         ; Must match tmp_table_size to effectively handle large in-memory operations

# THREADS
thread_cache_size = 100            ; How many threads are kept in a pool to be reused for new connections

# FILE LIMITS
open_files_limit = 65535           ; Max number of file descriptors the OS allows MySQL to open
table_open_cache = 4000            ; Number of open tables MySQL can keep cached to avoid repeated file opening

# LOGGING (optional but useful)
slow_query_log = 1                 ; Enables logging for queries that take a long time to execute
slow_query_log_file = /var/log/mysql/slow.log ; Defines the physical path where the slow query log is saved
long_query_time = 2                ; Threshold (seconds) for what defines a "slow" query

Step 5: File and Directory Permissions

I 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:

sudo groupadd webshared

Add both users to the group:

sudo usermod -aG webshared username
sudo usermod -aG webshared www-data

Log out/in (or newgrp webshared) for it to take effect.

Next, change the ownership of the web directory and set the permissions to group write:

sudo chown -R username:webshared /home/username/public_html
sudo chmod -R 2775 /home/username/public_html

Ensure new files stay writable (umask), set:

umask 002

(in ~/.zshrc or ~/.bashrc or ~/.profile etc)

For the www-data user, edit the config:

sudo nano /etc/php/8.4/fpm/pool.d/www.conf

Adding:

php_admin_value[umask] = 0002

The full file should look something like:

[www]                                       ; Defines the name of this specific process pool
user = www-data                             ; The Unix user that the PHP processes will run as
group = www-data                            ; The Unix group that the PHP processes will run as
php_admin_value[umask] = 0002               ; Sets default file permissions for new files (gives group write access)

listen = /run/php/php8.4-fpm.sock           ; Path to the Unix socket for Nginx/Apache to talk to PHP
listen.owner = www-data                     ; Owner of the socket file (must match web server user)
listen.group = www-data                     ; Group of the socket file
listen.mode = 0660                          ; Permissions for the socket (owner and group can read/write)
listen.backlog = 1024                       ; Max number of queued connection requests before refusal

pm = dynamic                                ; Enables dynamic scaling of child processes based on demand
pm.max_children = 40                        ; Max number of simultaneous child processes (prevents RAM exhaustion)
pm.start_servers = 8                        ; Number of child processes created on startup
pm.min_spare_servers = 4                    ; Minimum number of idle processes kept ready for spikes
pm.max_spare_servers = 12                   ; Maximum number of idle processes kept alive
pm.max_requests = 500                       ; Re-spawns a child after X requests to prevent memory leaks

pm.status_path = /fpm-status                ; URI to view real-time stats (load, active processes, etc.)
ping.path = /fpm-ping                       ; URI used by monitoring tools to check if PHP is alive
ping.response = pong                        ; The specific text response returned by the ping path

request_slowlog_timeout = 10s               ; Log requests that take longer than 10 seconds to execute
slowlog = /var/log/php8.4-fpm/www-slow.log  ; File path where slow execution stack traces are saved

request_terminate_timeout = 180s            ; Force-kill processes that run longer than 3 minutes
request_terminate_timeout_track_finished = yes ; Log if a process finished successfully after the timeout was reached

catch_workers_output = yes                  ; Redirects script stdout/stderr to the main FPM error log
clear_env = yes                             ; Clears environment variables to prevent data leakage between scripts
security.limit_extensions = .php            ; Limits PHP execution to specific extensions for security

php_admin_flag[log_errors] = on             ; Forces error logging for this specific pool
php_admin_value[error_log] = /var/log/php8.4-fpm/www-error.log ; Path to the error log for this pool

Step 6: Auto/Unattended Upgrades

Probably best to keep the server software up-to-date.

sudo apt install unattended-upgrades apt-listchanges && sudo dpkg-reconfigure --priority=low unattended-upgrades

Step 7: Firewall

Configure the firewall to allow SSH, HTTP and HTTPS, before enabling it:

sudo ufw allow OpenSSH
sudo ufw allow WWW
sudo ufw allow "WWW Secure"
sudo ufw enable
sudo ufw status verbose

You can check the firewall status at any time with the following command:

sudo ufw status

Step 8: Fail2Ban

Create a custom config file to work with the ufw firewall:

sudo nano /etc/fail2ban/jail.local

Enter the following:

[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
banaction = ufw

[sshd]
enabled = true

Enable and start the service:

sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Step 9: Misc

Set hostname

You may or may not need to adjust the system's hostname:

sudo hostnamectl set-hostname new-hostname

Set timezone

You may or may not need to adjust the server's timezone:

sudo timedatectl set-timezone Europe/London

Finishing touches

Finishing 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.

View as: JSON Markdown

If you enjoyed this post or found it useful, you can subscribe to my RSS feed.

Similar posts

  1. How to install PHP extension for Microsoft SQL Server under Fedora

    I found myself needing to connect to a Microsoft SQL Server via a PHP application running under Fedora. Finding concise details about installing the necessary drivers and extensions was not easy, so here is a blog post detailing how I did it.

    php microsoft fedora mssql sql linux
  2. How to set-up WatchGuard VPN with IKEv2 under Debian and Fedora

    A blog post detailing how to set-up WatchGuard VPN with IKEv2 under both Debian and Fedora Linux. This guide works for me under Debian 12 (bookworm) and Fedora 40/41, but your mileage may vary depending on how your VPN service is configured.

    debian vpn watchguard ikev2 fedora ipsec
  3. My Debian 12 (bookworm) server set-up

    I've been running Debian on my servers for years. It's dependable. I guess my server set-up is pretty common, consisting of Apache, PHP and MariaDB, but I figure it is still worth sharing details of how I provision my servers.

    php composer mariadb apache debian linux node fish
  4. My Debian 12 (bookworm) desktop set-up

    Creating a good Debian desktop experience is not too difficult, thanks to the excellent work of the Debian developers, but I thought it might be interesting to share how I set-up my Debian systems.

    debian linux
  5. How to add a custom search engine to Firefox

    I thought it would be trivial to add a custom search engine to Firefox. To be fair, it is fairly trivial, but not quite as easy as navigating to the correct Firefox settings page and adding a new entry. Instead, I found the process to be somewhat hidden and less obvious.

    opensearch search firefox mozilla php
  6. Calling Puppeteer via PHP

    A blog post detailing an issue where a Puppeteer screenshot script, triggered through a PHP application using CodeIgniter, stopped working due to Chromium not starting under the Apache www-data user on Debian.

    php javascript node debian apache
  7. Switching desktop Linux from Debian to Fedora

    Last week I switched the operating system on my daily driver (Lenovo ThinkPad T14s) from Debian 12 to Fedora 40. In this post I write a little about why I switched and how the switch went.

    debian linux fedora
  8. Firefox Nightly as a daily driver

    I believe that it's really important to support and use Firefox. Not only do I think that Mozilla understand/support user's privacy more than Google, but I also think it's important for the health of the web that more than one option exists when it comes to rendering engines. Also, it's a really good web browser.

    debian chrome firefox mozilla
  9. Single computing device lifestyle

    I've recently decided to simplify my life by moving away from using multiple computers to using a single laptop. What are the main advantages and disadvantages of using a single computer?

    debian thinkpad
  10. Redux

    As a web developer, I like to build and rebuild websites. My own website is no different.

    markdown fediverse mastodon codeigniter php bootstrap jquery debian
  11. How to create Bash aliases in Fedora

    Creating your own Bash aliases is a relatively easy process. That said, I recently switched my desktop linux distribution from Debian to Fedora and there are subtle differences.

    linux fedora debian bash