Brenton Cleeland

Step-by-step: Deploying Kanboard to a Debian 12 VPS

Published on

Kanboard made the Hacker News front page a few weeks which reminded me that I'd been meaning to experiment with it. Here's the end to end for deploying it to a fresh Debian 12 VPS.

If you choose to follow along and deploy to Vultr, you can use my referral link to receive $100 credit to experiment with.

Spin up a new VPS and point your domain name to it's IP address. We'll use <your-domain> in the examples below but you should replace that with your domain. SSH to the server using the root account created by the VPS provider.

ssh root@<your-domain>

If you didn't pre-install an SSH key using your VPS provider's mechanism you may need to use the password for the initial login.

Kick off by making sure that everything is up to date:

apt update
apt dist-upgrade

Once the upgrade has completed we'll create a new user named kanboard.

useradd kanboard
usermod -aG sudo kanboard
usermod -s /bin/bash kanboard
mkdir -p /home/kanboard/.ssh
chmod 700 /home/kanboard/.ssh
passwd kanboard

The passwd command at the end will require you to interactively add a password for the new user. You'll need this to sudo as the user later. Make sure that you generate a secure password and save it in your password manager.

Add your SSH public key to /home/kanboard/.ssh/authorized_keys:

nano /home/kanboard/.ssh/authorized_keys

Fix the permissions of the .ssh directory and it's contents. Without doing this modern versions of sshd will not allow you to use the key.

chmod 400 /home/kanboard/.ssh/authorized_keys
chown kanboard:kanboard /home/kanboard -R

Let's SSH as our new user to make sure that everything is working as expected.

ssh kanboard@<your-domain>

If you are prompted for a password, or if the login is denied, then use the root user to double check your SSH key and permissions.

Check that you can use sudo in your new account:

sudo ls

Once you've confirmed that you can log into the new user with your SSH key we are going to disable password authentication.

As your root user:

nano /etc/ssh/sshd_config

Include / update the following settings:

PermitRootLogin no
PasswordAuthentication no

Check /etc/ssh/sshd_config.d/ for configuration files that might have been put there by your VPS provider. Vultr includes 50-cloud-init.conf that allows password authentication. This can safely be deleted.

Restart sshd and test that connecting as root fails:

systemctl restart sshd
> ssh root@<your-domain>
root@<your-domain>: Permission denied (publickey).

For the rest of the steps we'll connect as the use you've just created. This means that we'll be using sudo to execute a bunch of them.

ssh kanboard@<your-domain>

Install nginx and the PHP related packages:

sudo apt install nginx
sudo apt install php-fpm php-pgsql php-cli php-mbstring php-opcache php-json php-gd php-xml php-pgsql php-curl php-zip

Let's update your nginx config to serve challenges for ACME. For now, we can quickly edit /etc/nginx/sites-enabled/default:

server {
    listen 80 default_server;
    server_name _;

    # no security problem here, since / is alway passed to upstream
    root /var/www/html;

    # always serve this directory for settings up let's encrypt
    location /.well-known/acme-challenge/ {
        root /var/www/challenges/;
        try_files $uri =404;
    }
}

Setup UFW to allow access on ports 80 and 443:

sudo ufw allow 80
sudo ufw allow 443

Ensure that nginx is running:

sudo systemctl restart nginx

Double check that you're running as the user that we created earlier, the following steps should not be completed as root. We're going to request an SSL certificate for our domain before setting anything else up. To do this, we'll use acme.sh to request a certificate from Let's Encrypt.

curl https://get.acme.sh | sh -s email=<your-email>
sudo mkdir -p /var/www/challenges/
sudo mkdir -p /etc/acme.sh/live/<your-domain>
sudo chown kanboard:kanboard /var/www/challenges/
sudo chown kanboard:kanboard /etc/acme.sh/live/<your-domain>

Now, to actually request and install the certificate:

./.acme.sh/acme.sh --issue -d <your-domain> --server letsencrypt -w /var/www/challenges/
./.acme.sh/acme.sh --install-cert -d <your-domain> --key-file /etc/acme.sh/live/<your-domain>/key.pem  --fullchain-file /etc/acme.sh/live/<your-domain>/cert.pem

Once "installed", lets add a new nginx configuration that uses the certificate. Add a new file called /etc/nginx/sites-available/kanboard.conf:

server {
    listen 443 ssl http2;
    server_name <your-domain>;

    ssl_certificate       /etc/acme.sh/live/<your-domain>/cert.pem;
    ssl_certificate_key   /etc/acme.sh/live/<your-domain>/key.pem;

    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
    ssl_session_tickets off;

    # modern settings from the Mozilla SSL config generator
    # https://mozilla.github.io/server-side-tls/ssl-config-generator/
    # intermediate configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    ssl_stapling on;
    ssl_stapling_verify on;

    # enable hsts
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload";

    client_max_body_size 50M;

    root /var/www/html/;
}

server {
    listen 80;
    server_name <your-domain>;

    location /.well-known/acme-challenge/ {
        root /var/www/challenges/;
        try_files $uri =404;
    }

    location / {
        return 301 https://$server_name$request_uri;
    }
}

Link to config into sites-enabled, then reload nginx to pick up the new configuration.

sudo ln -s /etc/nginx/sites-available/kanboard.conf /etc/nginx/sites-enabled/kanboard.conf
sudo systemctl reload nginx

You should now be able to load your domain and receive a response over HTTPS. It will be an error, but that's fine for now.

Lets move on and start installing our actual dependencies.

sudo apt install postgresql
sudo systemctl start postgresql

Switch to the postgres user to create our database and user:

sudo su postgres
psql

You'll now be in a PostgreSQL shell where we can create the database and role:

CREATE DATABASE kanboard;
CREATE ROLE kanboard WITH PASSWORD '<your-password>';
ALTER ROLE kanboard WITH LOGIN;
ALTER DATABASE kanboard OWNER TO kanboard;
GRANT ALL PRIVILEGES ON DATABASE kanboard TO kanboard;

User \q to exit the shell, then exit to go back to our user.

Now that we have nginx and PostgreSQL setup, it's finally time to install Kanboard.

We're about to install version v1.2.30, head to Kanboard's Releases page to make sure that's the latest version.

We'll use the instructions from the Debian Installation page to get started:

wget https://github.com/kanboard/kanboard/archive/v1.2.30.tar.gz
sudo tar xzvf v1.2.30.tar.gz -C /var/www/html/
sudo chown -R www-data:www-data /var/www/html/kanboard-1.2.30/data
rm v1.2.30.tar.gz

Copy the default config into a new config.php file for our installation:

sudo cp /var/www/html/kanboard-1.2.30/config.default.php /var/www/html/kanboard-1.2.30/config.php

Update /var/www/html/kanboard-1.2.30/config.php with our database settings:

// ...
define('DB_DRIVER', 'postgres');
define('DB_USERNAME', 'kanboard');
define('DB_PASSWORD', '<your-password>');
define('DB_HOSTNAME', 'localhost');
define('DB_NAME', 'kanboard');
define('DB_PORT', '5432');

While you're in the file, update the ENABLE_URL_REWRITE setting to set it to true:

define('ENABLE_URL_REWRITE', true);

Now, it's time to put in our "production" nginx configuration in. Update our site in /etc/nginx/sites-available/kanboard.conf:

server {
    listen 443 ssl http2;
    server_name <your-domain>;

    ssl_certificate       /etc/acme.sh/live/<your-domain>/cert.pem;
    ssl_certificate_key   /etc/acme.sh/live/<your-domain>/key.pem;

    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
    ssl_session_tickets off;

    # modern settings from the Mozilla SSL config generator
    # https://mozilla.github.io/server-side-tls/ssl-config-generator/
    # intermediate configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    ssl_stapling on;
    ssl_stapling_verify on;

    # enable hsts
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload";

    client_max_body_size 50M;

    root /var/www/html/kanboard-1.2.30/;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;

        fastcgi_pass unix:/var/run/php/php-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_index index.php;
        include fastcgi_params;
    }

    # Deny access to the directory data
    location ~* /data {
        deny all;
        return 404;
    }

    # Deny access to .htaccess
    location ~ /\.ht {
        deny all;
        return 404;
    }
}

server {
    listen 80;
    server_name <your-domain>;

    location /.well-known/acme-challenge/ {
        root /var/www/challenges/;
        try_files $uri =404;
    }

    location / {
        return 301 https://$server_name$request_uri;
    }
}

Reload nginx (again) and we should be good to go!

sudo systemctl reload nginx

You should now be able to go to your domain where you will see Kanboard running. Login with the default credentials (admin/admin), then immediately update the admin password!