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:

bash
sudo apt install nginx -y

Start:

bash
sudo systemctl start nginx

Auto start at boot:

bash
sudo systemctl enable nginx

Verify status:

bash
sudo systemctl status nginx

Press Q to exit the status output.

Confirm in browser: visit http://<public_ipv4>

Expected output:

text
Welcome to nginx! ...

Install PHP #

Install PHP-FPM:

bash
sudo apt install php-fpm -y

Verify PHP installation:

bash
php -v

List the PHP-FPM socket file:

bash
ls /run/php/

Look for a file similar to: php8.3-fpm.sock

Verify PHP-FPM service status:

bash
sudo systemctl status php<version>-fpm

Example:

bash
sudo systemctl status php8.3-fpm

Press Q to exit the status output.

Configure PHP #

Backup php.ini:

bash
sudo cp /etc/php/8.3/fpm/php.ini /etc/php/8.3/fpm/php.ini.bak

Edit php.ini:

bash
sudo vim /etc/php/8.3/fpm/php.ini

Modify:

/etc/php/8.3/fpm/php.ini
max_execution_time = 60
memory_limit = 256M
post_max_size = 25M
upload_max_filesize = 25M

Save and exit.

Configure NGINX #

Backup default site configuration:

bash
sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak

Edit default site configuration:

bash
sudo vim /etc/nginx/sites-available/default

Modify:

/etc/nginx/sites-available/default
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 the server block is not commented

Test NGINX configuration:

bash
sudo nginx -t

Expected output:

text
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Restart NGINX:

bash
sudo systemctl restart nginx

Verify Installation #

Create a test PHP page:

bash
sudo vim /var/www/html/test.php

Add:

/var/www/html/test.php
<?php phpinfo(); ?>

Save and exit.

Access via browser

Visit: http://<public_ipv4>/test.php

Expected output:

text
PHP Version 8.x ...

This confirms PHP is correctly installed and processed by NGINX.

After verifying PHP is working, delete the test file:

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

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

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

bash
sudo mkdir -p /var/www/<site>/public_html

Replace <site> with actual name (e.g. example.com).

Download Kirby CMS starterkit:

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

bash
sudo vim /etc/nginx/sites-available/<site>

Replace <site> with actual domain (e.g. example.com).

Add:

/etc/nginx/sites-available/<site>
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:

/etc/nginx/sites-available/<site>
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:

bash
sudo ln -s /etc/nginx/sites-available/<site> /etc/nginx/sites-enabled/

Test NGINX configuration:

bash
sudo nginx -t

Expected output:

text
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Reload NGINX:

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

bash
sudo vim /etc/nginx/sites-available/default

Modify:

/etc/nginx/sites-available/default
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).
  • Files: 644
    • Owner (www-data): Read and write.
    • Group and others: Read-only (NGINX only needs to read files).

Set ownership:

  • Ensure the web server user owns all files. Default user: www-data.

  • Verify the user in /etc/nginx/nginx.conf if uncertain.

bash
sudo chown -R www-data:www-data /var/www/<site>

Set file permissions:

bash
sudo find /var/www/<site> -type f -exec chmod 644 {} \;

Set folder permissions:

bash
sudo find /var/www/<site> -type d -exec chmod 755 {} \;

Verify:

bash
ll /var/www

This ensures secure and functional access for the NGINX web server.

Step 8: Test Website #

Check HTTP Headers

Run:

bash
curl -I <host>   # Add: <domain>, www.<domain> or <public_ipv4>

Expected output:

text
HTTP/1.1 200 OK

A 200 OK status confirms the server is running and responding correctly.

Enable Kirby Panel

Edit config file:

bash
sudo vim /var/www/<site>/public_html/site/config/config.php

Add or modify:

/var/www/<site>/public_html/site/config/config.php
<?php
return [
  'url' => 'https://example.com'   // Enforce domain for maximum security
  'panel' => [
    'install' => true
  ],
];

Create admin account by visiting:

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

bash
sudo vim /var/www/<site>/public_html/robots.txt

Add:

/var/www/<site>/public_html/robots.txt
User-agent: *
Disallow: /

This blocks all web crawlers from indexing any part of the site.

Verify access, visit:

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

bash
sudo rm -rfv /var/www/<site>/public_html/{*,.*}

Clone the official plainkit repository:

bash
sudo git clone https://github.com/getkirby/plainkit /var/www/<site>/public_html/

Enable Kirby Panel #

Create config directory:

bash
sudo mkdir -p /var/www/<site>/public_html/site/config/

Create config file:

bash
sudo vim /var/www/<site>/public_html/site/config/config.php

Modify:

/var/www/<site>/public_html/site/config/config.php
<?php
return [
  'panel' => [
    'install' => true
  ],
];

After creating your account, set 'install' => false to disable further installations.

Set ownership and permissions:

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

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

bash
sudo chown -R <user>:<user> /var/www/<site>

2. Synchronise website from local to remote:

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

bash
sudo chown -R www-data:www-data /var/www/<site>

Next Steps #