Deploy a Kirby CMS Site on Linode with NGINX: Step-by-Step Guide
Prerequisites #
Before you begin, ensure you have:
- A Linode (Akamai) account.
- SSH access to your terminal.
- Basic knowledge of Linux commands.
Step 1: Create Compute Instance #
Read: workflow.
Step 2: Set Up NGINX & PHP #
Install NGINX #
Install:
sudo apt install nginx -y
Start:
sudo systemctl start nginx
Auto start at boot:
sudo systemctl enable nginx
Verify status:
sudo systemctl status nginx
Press Q
to exit the status output.
Confirm in browser: visit http://<public_ipv4>
Expected output:
Welcome to nginx! ...
Install PHP #
Install PHP-FPM:
sudo apt install php-fpm -y
Verify PHP installation:
php -v
List the PHP-FPM socket file:
ls /run/php/
Look for a file similar to: php8.3-fpm.sock
Verify PHP-FPM service status:
sudo systemctl status php<version>-fpm
Example:
sudo systemctl status php8.3-fpm
Press Q
to exit the status output.
Configure PHP #
Backup php.ini
:
sudo cp /etc/php/8.3/fpm/php.ini /etc/php/8.3/fpm/php.ini.bak
Edit php.ini
:
sudo vim /etc/php/8.3/fpm/php.ini
Modify:
max_execution_time = 60
memory_limit = 256M
post_max_size = 25M
upload_max_filesize = 25M
Save and exit.
Configure NGINX #
Backup default site configuration:
sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak
Edit default site configuration:
sudo vim /etc/nginx/sites-available/default
Modify:
server {
...
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php<version>-fpm.sock; # Add installed PHP version
}
...
}
Make sure:
- The block is uncommented
- The closing
}
at the end of theserver
block is not commented
Test NGINX configuration:
sudo nginx -t
Expected output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Restart NGINX:
sudo systemctl restart nginx
Verify Installation #
Create a test PHP page:
sudo vim /var/www/html/test.php
Add:
<?php phpinfo(); ?>
Save and exit.
Access via browser
Visit: http://<public_ipv4>/test.php
Expected output:
PHP Version 8.x ...
This confirms PHP is correctly installed and processed by NGINX.
After verifying PHP is working, delete the test file:
sudo rm /var/www/html/test.php
This prevents exposing server configuration details to the public.
Step 3: Install PHP Extensions & ImageMagick #
Install required packages:
sudo apt install imagemagick php-common php-curl php-gd php-imagick php-intl php-mbstring php-xml php-zip -y
Verify installed PHP modules:
php -m
Look for the following modules in the output to confirm successful installation:
curl
gd
imagick
intl
mbstring
xml
zip
Step 4: Install Kirby CMS Starterkit #
Create website directory:
sudo mkdir -p /var/www/<site>/public_html
Replace <site>
with actual name (e.g. example.com
).
Download Kirby CMS starterkit:
sudo git clone https://github.com/getkirby/starterkit /var/www/<site>/public_html/
The starterkit is perfect for testing if everything works as expected.
Step 5: Create Server Block Configuration #
Create NGINX virtual host file:
sudo vim /etc/nginx/sites-available/<site>
Replace <site>
with actual domain (e.g. example.com
).
Add:
server {
listen 80;
listen [::]:80;
server_name <name>; # Add: <domain>, www.<domain> or <public_ipv4>
root /var/www/<site>/public_html;
index index.php; # Serve index.php when a directory is requested
# Restrict access to core and hidden files
rewrite ^/(content|site|kirby)/(.*)$ /error last;
rewrite ^/\.(?!well-known/) /error last;
# Redirect all requests except specified assets to index.php
rewrite ^/(?!favicon(?:-\d+x\d+)?\.png|favicon\.ico|favicon\.svg|apple-touch-icon\.png|site\.webmanifest|robots\.txt$)[^/]+$ /index.php last;
# Serve static files or fallback to index.php
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php<version>-fpm.sock; # Add installed PHP version
}
}
Or with HTTP to HTTPS redirect:
server {
listen 80;
listen [::]:80;
server_name <name>; # Add: <domain>, www.<domain> or <public_ipv4>
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name <name>; # Add: <domain>, www.<domain> or <public_ipv4>
root /var/www/<site>/public_html;
index index.php; # Serve index.php when a directory is requested
ssl_certificate <path/to>cert.pem;
ssl_certificate_key <path/to>key.pem;
# Restrict access to core and hidden files
rewrite ^/(content|site|kirby)/(.*)$ /error last;
rewrite ^/\.(?!well-known/) /error last;
# Redirect all requests except specified assets to index.php
rewrite ^/(?!favicon(?:-\d+x\d+)?\.png|favicon\.ico|favicon\.svg|apple-touch-icon\.png|site\.webmanifest|robots\.txt$)[^/]+$ /index.php last;
# Serve static files or fallback to index.php
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php<version>-fpm.sock; # Add installed PHP version
}
}
Create symbolic link to enable site:
sudo ln -s /etc/nginx/sites-available/<site> /etc/nginx/sites-enabled/
Test NGINX configuration:
sudo nginx -t
Expected output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Reload NGINX:
sudo systemctl reload nginx
Step 6: Add a Catch-All "Deny" Server Block #
Create a default server block to handle all incoming requests that do not match any configured domain. This ensures that:
- NGINX does not accidentally serve another site's content (e.g. staging content on the wrong domain)
- Unrecognised or unconfigured domains are denied immediately
- Cloudflare or other reverse proxies are not confused by unexpected responses
This catch-all block acts as a fail-safe and improves both security and predictability of your server’s behaviour. It should be defined using default_server
on both port 80 and 443.
Edit default site configuration:
sudo vim /etc/nginx/sites-available/default
Modify:
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444; # Close connection immediately (silent drop)
}
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_certificate <path/to>cert.pem;
ssl_certificate_key <path/to>key.pem;
return 444; # Close connection immediately (silent drop)
}
444
is an NGINX-specific status code that closes the connection with no response. You can replace it with 403
, 404
, or a custom page if preferred.
Step 7: Set Permissions #
Apply permissions to either /var/www/
or a specific project directory, e.g. /var/www/<site>
.
Permission goals:
- Directories:
755
- Owner (
www-data
): Read, write, execute. - Group and others: Read and execute (required for NGINX to access directory contents).
- Owner (
- Files:
644
- Owner (
www-data
): Read and write. - Group and others: Read-only (NGINX only needs to read files).
- Owner (
Set ownership:
Ensure the web server user owns all files. Default user:
www-data
.Verify the user in
/etc/nginx/nginx.conf
if uncertain.
sudo chown -R www-data:www-data /var/www/<site>
Set file permissions:
sudo find /var/www/<site> -type f -exec chmod 644 {} \;
Set folder permissions:
sudo find /var/www/<site> -type d -exec chmod 755 {} \;
Verify:
ll /var/www
This ensures secure and functional access for the NGINX web server.
Step 8: Test Website #
Check HTTP Headers
Run:
curl -I <host> # Add: <domain>, www.<domain> or <public_ipv4>
Expected output:
HTTP/1.1 200 OK
A 200 OK
status confirms the server is running and responding correctly.
Enable Kirby Panel
Edit config file:
sudo vim /var/www/<site>/public_html/site/config/config.php
Add or modify:
<?php
return [
'url' => 'https://example.com' // Enforce domain for maximum security
'panel' => [
'install' => true
],
];
Create admin account by visiting:
http://<public_ipv4>/panel
Security Checks
Verify that access to sensitive files and folders is properly blocked by confirming these URLs redirect to the error page:
- Content folder:
/content/site.txt
- Kirby folder:
/kirby/bootstrap.php
- Site folder:
/site/config/config.php
- Hidden files and folders:
/git/config
Various Checks
- Confirm all pages load without errors.
- Use browser developer tools (Network and Console) to check for errors or failed requests.
- Verify the Panel functions correctly (account creation, login).
- Test content creation, editing, and deletion via the Panel.
- Check file details (images, documents, archives) are viewable in the Panel.
- Confirm images load promptly.
- Test uploading images, documents, and archives through the Panel.
Create robots.txt
file:
sudo vim /var/www/<site>/public_html/robots.txt
Add:
User-agent: *
Disallow: /
This blocks all web crawlers from indexing any part of the site.
Verify access, visit:
https://example.com/robots.txt
You should see the file contents displayed in the browser.
Step 9: Deploy Website #
Objective
Replace the default Starterkit with Kirby's Plainkit and set up a fresh environment for your project.
Install #
Wipe starterkit files:
sudo rm -rfv /var/www/<site>/public_html/{*,.*}
Clone the official plainkit repository:
sudo git clone https://github.com/getkirby/plainkit /var/www/<site>/public_html/
Enable Kirby Panel #
Create config directory:
sudo mkdir -p /var/www/<site>/public_html/site/config/
Create config file:
sudo vim /var/www/<site>/public_html/site/config/config.php
Modify:
<?php
return [
'panel' => [
'install' => true
],
];
After creating your account, set 'install' => false
to disable further installations.
Set ownership and permissions:
sudo chown -R www-data:www-data /var/www/<site>
sudo find /var/www/<site> -type f -exec chmod 644 {} \;
sudo find /var/www/<site> -type d -exec chmod 755 {} \;
Create Panel account, visit:
http://<public_ipv4>/panel
Follow the setup instructions to create admin account.
Synchronise Website Files #
Objective
Synchronise local Kirby project to the server using rsync
.
1. Temporarily change ownership to user:
Required to allow rsync
to read and write files during synchronisation.
sudo chown -R <user>:<user> /var/www/<site>
2. Synchronise website from local to remote:
rsync -avz --delete \
--exclude='.*' \
--exclude='/kirby' \
--exclude='/media' \
--exclude='/site/accounts' \
--exclude='/site/cache' \
--exclude='/site/sessions' \
--exclude='_.log*' \
<path_to_local_folder> \
<host>:/var/www/<site>/public_html/
Explanation:
--delete
: removes files on the server that no longer exist locally.--exclude
: skips uploading:- Hidden files
- Core and runtime directories (
kirby
,media
,site/accounts
, etc.) - Log files
3. Reset ownership to www-data
:
sudo chown -R www-data:www-data /var/www/<site>