# BookStack @SLES16 (openSUSE v16.1) (with Internet access)

Let's deploy BookStack web application to the Internet reachable environment:

- Solution: BookStack ([www](https://www.bookstackapp.com/), [github](https://github.com/BookStackApp/BookStack))
- Resources: VM in the laptop lab.
- Stack:
  - mbp7dox2: openSUSE 16.1 + nginx + MariaDB

# Create VM.
Preparations:



```bash
sudo su
zypper install \
    tmux \
    git \
    php \
    php-gd \
    php-zip \
    php-mysqlnd \
    php8-cli \
    php8-phar \
    php8-curl \
    php8-fileinfo \
    php8-mbstring \
    php8-pdo \
    php8-mysql \



```



# Install MariaDB and perform recommended post-install
```bash
zypper install mariadb-server

systemctl enable --now mariadb
systemctl status mariadb
```
Secure the fresh setup
```bash
mariadb-secure-installation
```

[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/9dqPV0WEu9tFc3si-image-1780996406782.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/9dqPV0WEu9tFc3si-image-1780996406782.png)


# Create database and application user
```bash
export db_pass="Very-Strong-Password123"

mariadb -u root --execute="CREATE DATABASE bookstack;"
mariadb -u root --execute="CREATE USER 'bookstack'@'localhost' IDENTIFIED BY '${db_pass}';"
mariadb -u root --execute="GRANT ALL ON bookstack.* TO 'bookstack'@'localhost'; FLUSH PRIVILEGES;"
```

Verify freshly created user can login
```bash
mariadb -u bookstack -p
```
[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/IcNaByvdzDYXScD5-image-1780996580238.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/IcNaByvdzDYXScD5-image-1780996580238.png)


# Setup nginx, enable SSL
```bash
zypper install nginx
systemctl enable --now nginx
systemctl status nginx
ss -ntap | grep nginx
```
```bash
LISTEN    0      511           0.0.0.0:80           0.0.0.0:*     users:(("nginx",pid=14387,fd=6),("nginx",pid=14386,fd=6))
LISTEN    0      511              [::]:80              [::]:*     users:(("nginx",pid=14387,fd=7),("nginx",pid=14386,fd=7))
```


## Create of self-signed certificates and Enable SSL
We have local running environment (virtualized local VM) and will use self-signed certificate in this scenario), alternatively, point cert and key accordingly.
```bash
sudo su
export dir="/data/certs/"
export fqdn="dox.$(hostname)"
echo ${fqdn}

mkdir -p ${dir}/${fqdn}/current/
mkdir -p ${dir}/${fqdn}/archive/

openssl req -x509 \
    -nodes \
    -days 365 \
    -newkey rsa:2048 \
    -subj "/C=FI/ST=State/L=City/O=Home/OU=IT/CN=${fqdn}" \
    -out    ${dir}/${fqdn}/current/${fqdn}.crt \
    -keyout ${dir}/${fqdn}/current/${fqdn}.key

ls -la ${dir}/${fqdn}/current/
```

[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/fkqF31THxHJZTal6-image-1780996940809.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/fkqF31THxHJZTal6-image-1780996940809.png)



## Modify Nginx webserver's config
Open Nginx's default config file.
SLES has different Nginx's config structure.
```bash
export fqdn="$(hostname)"
echo ${fqdn}

vi /etc/nginx/vhosts.d/${fqdn}.conf
```


It is highly recommended to stop using and promote usage of unencrypted HTTP. Do not disable it, but use forwarders instead.

Before I used to disable (=comment or remove) "Server { Listen 80; }" definition (upper block) and enable (=uncomment) "Server { Listen 443 ssl http2; }" definition lower block.




But later felt in the trap, that some browsers does initiate first connections unencrypted, I have replaced comments with forwarders. Depending on the environment and use-cases.

for BookStack like this (using ```$request_uri``` variable)
```ini


Shortly, config should look like this
```bash
server {
    listen 80;

    server_name mbp7dox2;
    access_log /var/log/nginx/mbp7dox2.access.log;
    error_log  /var/log/nginx/mbp7dox2.error.log;

    return 301 https://$host$request_uri;
}


server {
    listen 443 ssl;

    server_name  _;
    access_log /var/log/nginx/mbp7dox2.access.log;
    error_log  /var/log/nginx/mbp7dox2.error.log;

    root /usr/share/nginx/html;

    ssl_certificate     "/data/certs/mbp7dox2/current/mbp7dox2.crt";
    ssl_certificate_key "/data/certs/mbp7dox2/current/mbp7dox2.key";

    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout  10m;
    ssl_ciphers PROFILE=SYSTEM;
    ssl_prefer_server_ciphers on;

}
```


SElinux
```bash
! TODO:fix context
```


Test config and reload it:
```bash
nginx -t
nginx -s reload
ss -ntap | grep nginx
```
[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/jrSWwFqT31HqgblS-image-1780997800190.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/jrSWwFqT31HqgblS-image-1780997800190.png)






Test it, requesting to show headers only, '-k' - will skip cert checks, as they will fail for not matching the CN (common name). Using local address to avoid DNS lookup.
```bash
curl -I https://127.0.0.1 -k
```
[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/Zykxyv3JCjSbu1p4-image-1780997848730.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/Zykxyv3JCjSbu1p4-image-1780997848730.png)



# Open firewall (create security policies to pass the traffic in)
```bash
firewall-cmd --add-service=http  --permanent
firewall-cmd --add-service=https --permanent
firewall-cmd --reload
firewall-cmd --list-all
```

Foundation is done. We can proceed with application setup.




# Install PHP-FPM
```bash
zypper install php-fpm

systemctl enable --now php-fpm
systemctl status php-fpm
```


We need to know, where PHP-FPM is listening, to point requests correctly from webserver below:

```bash
fgrep -irn listen /etc/php8/fpm/ 
```
[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/47rUohLirkCZJWQv-image-1780998229451.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/47rUohLirkCZJWQv-image-1780998229451.png)

port 9000 we shall be using and not local file socket
```ini
/etc/php8/fpm/php-fpm.d/www.conf:43:listen = 127.0.0.1:9000
```


Let's see which user and group is owning the php.
```bash
fgrep -irn user  /etc/php8/fpm/ | grep -v \;
fgrep -irn group /etc/php8/fpm/ | grep -v \;
```

[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/dQxmPFRc8VazBQOk-image-1780998342575.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/dQxmPFRc8VazBQOk-image-1780998342575.png)

these are
```bash
user=wwwrun
group=www
```

Let's see which user and group nginx is running with
```bash
fgrep -irn user /etc/nginx/
ss -ntap | grep nginx
```

[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/shSJVbVVxT1fKxYw-image-1780998437777.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/shSJVbVVxT1fKxYw-image-1780998437777.png)




-"Aha!". Let's change PHP-FPM be run as ```nginx```



```bash
vi /etc/php8/fpm/php-fpm.d/www.conf
```
Should be like this, using ```nginx``` user:
```ini
; user = wwwrun
user = nginx
; group = www
group = nginx
```

[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/qniiY1U39kUmuuLr-image-1780998571897.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/qniiY1U39kUmuuLr-image-1780998571897.png)


Remember to restart the service
```bash
systemctl restart php-fpm
```



# Install Composer and make available globally.

ref to
```url
https://getcomposer.org/download/
```

as a normal user, not root
```bash
mkdir -p ~/utils/composer
cd ~/utils/composer/

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"

php -r "if (hash_file('sha384', 'composer-setup.php') === 'c8b085408188070d5f52bcfe4ecfbee5f727afa458b2573b8eaaf77b3419b0bf2768dc67c86944da1544f06fa544fd47') { echo 'Installer verified'.PHP_EOL; } else { echo 'Installer corrupt'.PHP_EOL; unlink('composer-setup.php'); exit(1); }"
```

[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/SwLsL5fpEIxOZs9F-image-1780998774383.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/SwLsL5fpEIxOZs9F-image-1780998774383.png)

```bash
php composer-setup.php

php -r "unlink('composer-setup.php');"

php composer.phar
sudo mv composer.phar /usr/local/bin/composer
```

[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/6WvU4pTI2n1D9xNP-image-1780998886272.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/6WvU4pTI2n1D9xNP-image-1780998886272.png)




# Prepare home for application
Naming convention: application.host, in my case it is. Always bear in mind, that one host may have more than one application. If there are direct FQDN, then use it to make management easier. 
```bash
whoami
sudo su

# privileged user (you)
export pu="anton"

export host="$(hostname)"
export app="dox.${host}"
export dir="/var/www/${app}"
echo ${dir}

mkdir -p ${dir}

# permit priviledged user to own app directory
chown -R ${pu}:${pu} ${dir}
exit

# as priviledged user, not as root
export host="$(hostname)"
export app="dox.${host}"
export dir="/var/www/${app}"
cd ${dir}
```


Create application-specific configuration file in the webserver
```bash
sudo su
export host="$(hostname)"
export app="dox.${host}"
export dir="/var/www/${app}"

cat << _EOF_ > /etc/nginx/vhosts.d/${app}.conf
server {
    listen 443 ssl;

    server_name ${app};

    access_log /var/log/nginx/${app}_access.log;
    error_log  /var/log/nginx/${app}_error.log;

    ssl_certificate     "/data/certs/${app}/current/${app}.crt";
    ssl_certificate_key "/data/certs/${app}/current/${app}.key";

    ssl_protocols TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout 10m;
    ssl_ciphers PROFILE=SYSTEM;

    root ${dir}/BookStack/public;
    index index.php;
  
    client_max_body_size 1G;
    fastcgi_buffers 64 4K;

    location / {
        try_files \$uri \$uri/ /index.php?\$query_string;
    }

    location ~ \.php(?:$|/) {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
        fastcgi_param PATH_INFO \$fastcgi_path_info;
        fastcgi_pass 127.0.0.1:9000;
    }

    location ~* \.(?:jpg|jpeg|gif|bmp|ico|png|css|js|swf)$ {
        expires 30d;
        access_log off;
    }

    location ~ ^/(?:\.htaccess|data|config|db_structure\.xml|README) {
        deny all;
    }

}
_EOF_
```
Verify
```bash
ls -la /etc/nginx/vhosts.d/${app}.conf
```




Prepare hostnames to match certificates CNs (for local usage)
```bash
vi /etc/hosts
```
```
127.0.0.1   localhost mbp7dox2 dox.mbp7dox2
```

Test and reload the webserver:
```bash
nginx -t
nginx -s reload

export host="$(hostname)"
export app="dox.${host}"
echo ${app}

curl https://${app} -k
```


# Installation of BookStack application
Change directory before cloning the repo. As a normal user, not root

```bash
export host="$(hostname)"
export app="dox.${host}"
export dir="/var/www/${app}/"

cd ${dir}
pwd
```
```
/var/www/dox.lt58dox1
```

Clone repo
```bash
git clone https://github.com/BookStackApp/BookStack.git --branch release --single-branch
ls -la
```

[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/8SUDnZo6aNTtTYvg-image-1780999586944.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/8SUDnZo6aNTtTYvg-image-1780999586944.png)




## Build an application
```bash
cd Bookstack
which composer
composer update
composer install --no-dev
```

Should be built successfully:
[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/X4f8VoXIKjJCH4yB-image-1781000080129.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/X4f8VoXIKjJCH4yB-image-1781000080129.png)





# Creating missing directories
issue
```bash
#3 /var/www/dox.lt58dox1/BookStack/vendor/la...; PHP message: PHP Fatal error:  Uncaught UnexpectedValueException: The stream or file "/var/www/dox.lt58dox1/BookStack/storage/logs/laravel.log" could not be opened in append mode: Failed to open stream: Permission denied
```
```bash
sudo su

export host="$(hostname)"
export app="dox.${host}"
export dir="/var/www/${app}/"

ls -la ${dir}/BookStack/storage/logs/

# Create an empty log and directory for views
touch ${dir}/BookStack/storage/logs/laravel.log

chown nginx:nginx ${dir}/BookStack/storage/logs/laravel.log
namei -mo  ${dir}/BookStack/storage/logs/laravel.log
```
[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/DhncX2xhDK2Mmuud-image-1781000207760.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/DhncX2xhDK2Mmuud-image-1781000207760.png)




# Configure application
Copy template, generate salt and configure .env
```bash
sudo su

export host="$(hostname)"
export app="dox.${host}"
export dir="/var/www/${app}"
cd ${dir}/BookStack/
pwd

cp .env.example .env
php artisan key:generate
```
Are you sure you want to run this command? Yes [Enter]
```
vi .env
```
Should be simple as
```ini
APP_KEY=base64:goKeJWQsFFboBGOSF5+eti2Yv1auP4rXvxVbQ4Iupgc=
APP_URL=https://dox.lt58dox1

DB_HOST=localhost
DB_DATABASE=bookstack
DB_USERNAME=bookstack
DB_PASSWORD=Very-Strong-Password123

# not in use
MAIL_DRIVER=smtp
MAIL_FROM_NAME="BookStack"
MAIL_FROM=bookstack@example.com
MAIL_HOST=localhost
MAIL_PORT=587
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
```


# Create database schema
```bash
php artisan migrate
```
[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/jYJtkcrkFC6Uujp3-image-1781021385766.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/jYJtkcrkFC6Uujp3-image-1781021385766.png)





## Set least necessary permissions for application
```bash
export host="$(hostname)"
export app="dox.${host}"
export dir="/var/www/${app}"

# not inside app dir, but level up (not to be affected by itself)
vi fixperm-${app}.sh
```
```bash
#!/bin/bash

export host="$(hostname)"
export app="dox.${host}"
export dir="/var/www/${app}/BookStack/"

# priviledged user
export pu="anton"

# webserver's user
export wsu="nginx"

chown -R ${wsu}:${pu} ${dir}
subdirs=("" "/storage/" "/bootstrap/cache/" "/public/uploads/")
for subdir in "${subdirs[@]}" ; do
    echo "--[ subdir: ${subdir} ]--"
    chown -R ${wsu}:${pu} ${dir}/${subdir}
done 
for subdir in "${subdirs[@]}" ; do
    echo "--[ subdir: ${subdir} ]--"
    find ${dir}/${subdir} -type d -exec chmod 0770 {} \+
    find ${dir}/${subdir} -type f -exec chmod 0660 {} \+
done 
```
```bash
chmod +x ./fixperm.sh
./fixperm.sh
```

[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/SbrhqPoAg3o0raPt-image-1781000483899.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/SbrhqPoAg3o0raPt-image-1781000483899.png)



# Application is ready

[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/ghMyzY16E1VioVfn-image-1781021462002.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/ghMyzY16E1VioVfn-image-1781021462002.png)




## Used space:

Minimal openSUSE16.1 + webserver + PHP-FPM + MariaDB


```bash
df -hT | sort
```

[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/JCi9x4xeJNkIOicR-image-1781021509313.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/JCi9x4xeJNkIOicR-image-1781021509313.png)




```bash
date
export dir="/var/www/${app}"
df -hT ${dir}
du -h ${dir} --max-depth=1
```


[![](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/scaled-1680-/FCi74qTEwTJr3ct4-image-1781021592681.png)](https://storage.googleapis.com/iau-data-dox/uploads/images/gallery/2026-06/FCi74qTEwTJr3ct4-image-1781021592681.png)