Install Bolt CMS with Nginx, MySQL, and PHP on Ubuntu 20.04

Updated on October 15, 2021
Install Bolt CMS with Nginx, MySQL, and PHP on Ubuntu 20.04 header image

Introduction

Bolt is an open-source content management system based on the Symfony PHP framework. It is suitable for medium-sized websites and aims at both content editors and software developers. This guide explains how to install Bolt with Nginx, MySQL, and PHP on Ubuntu 20.04 and set up HTTPS with a free Let's Encrypt TLS certificate.

Prerequisites

Before you begin, you should:

Make sure to replace bolt.example.com in the code examples with your server's fully qualified domain name.

1. Install System Packages

Bolt requires the following software:

  • A web server such as Nginx.
  • A SQL database engine such as MySQL.
  • PHP 7.2.9 or higher with a minimum of 32MB of memory allocated to PHP.
  • The following PHP modules:
    • curl, exif, fileinfo, gd, json, mysqlnd (to use MySQL as a database), openssl, pdo, posix, xml, zip.
    • (optional but recommended) intl, mbstring, opcache.
  1. Install Nginx.

     $ sudo apt -y install nginx
  2. Follow the Install MySQL section of this Vultr guide to install and secure MySQL 8.0.

  3. Add the ppa:ondrej/php repository, a long-time and community-trusted repository that offers PHP 8.0.

     $ sudo LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php
  4. Install PHP 8.0 and the required modules.

     $ sudo apt -y install php8.0-cli php8.0-curl php8.0-fpm php8.0-gd php8.0-intl php8.0-mbstring php8.0-mysql php8.0-xml php8.0-zip
  5. Install unzip to extract zip files.

     $ sudo apt -y install unzip

2. Configure PHP

  1. List all the time zones that your Ubuntu system supports. Use the Up / Down / Pgup / Pgdn keys to move through the list, and press Q to exit.

     $ timedatectl list-timezones
  2. Copy an appropriate time zone from the list, for example, America/New_York. Then update the operating system with that time zone.

     $ sudo timedatectl set-timezone America/New_York
  3. Edit the main PHP configuration file to tell PHP to use your time zone.

     $ sudo nano /etc/php/8.0/fpm/php.ini
  4. Find the line ;date.timezone =. Remove the semicolon and add your time zone. For example:

     date.timezone = America/New_York
  5. Here are the common settings that you can change if needed:

     max_execution_time
     memory_limit
     post_max_size
     upload_max_filesize
  6. Save the configuration file and exit.

  7. Create a dedicated user named bolt to manage your website's source code.

     $ sudo adduser bolt

    Switch to this user each time you change the source code.

  8. Create the PHP-FPM configuration file from the default one.

     $ sudo cp /etc/php/8.0/fpm/pool.d/www.conf /etc/php/8.0/fpm/pool.d/bolt.conf
  9. Rename the default file to disable it and keep it as a backup.

     $ sudo mv /etc/php/8.0/fpm/pool.d/www.conf /etc/php/8.0/fpm/pool.d/www.conf.default
  10. Edit the PHP-FPM configuration file.

    $ sudo nano /etc/php/8.0/fpm/pool.d/bolt.conf
  11. Search for the following settings, then:

    • Replace [www] with [bolt]
    • Replace user = www-data with user = bolt
    • Replace group = www-data with group = bolt (do not change the listen.group = www-data setting)

    Make sure the listen = /run/php/php8.0-fpm.sock setting does not start with ;. This setting makes PHP-FPM listen on a Unix socket specified by the /run/php/php8.0-fpm.sock file.

  12. Copy and paste the following settings to the end of the file.

    catch_workers_output = yes
    php_flag[display_errors] = off
    php_admin_flag[log_errors] = on
    php_admin_value[error_log] = /var/log/php-fpm/bolt/error.log
    
    php_admin_value[session.save_path] = /var/lib/php/sessions/bolt

    Those settings make PHP-FPM log error messages to the /var/log/php-fpm/bolt/error.log file instead of displaying them to website users and store session data in the /var/lib/php/sessions/bolt directory.

  13. Save the configuration file and exit.

  14. Create two directories to store PHP logs and session data.

    $ sudo mkdir -p /var/log/php-fpm/bolt
    $ sudo mkdir -p /var/lib/php/sessions/bolt
  15. Update the ownership and permissions of the two directories so that only the PHP-FPM processes can access them.

    $ sudo chown bolt:bolt /var/log/php-fpm/bolt
    $ sudo chmod 700 /var/log/php-fpm/bolt
    $ sudo chown bolt:bolt /var/lib/php/sessions/bolt
    $ sudo chmod 700 /var/lib/php/sessions/bolt
  16. Check the new configuration.

    $ sudo php-fpm8.0 -t
  17. Restart the PHP-FPM service for the changes to take effect.

    $ sudo systemctl restart php8.0-fpm.service

3. Create a Database

  1. Connect to MySQL as the MySQL root user.

     $ sudo mysql
  2. Create a new db_name database for Bolt.

     mysql> CREATE DATABASE db_name;
  3. Create a MySQL user named db_user and grant it all privileges on the db_name database. Replace db_password with a strong password.

     mysql> CREATE USER 'db_user'@'localhost' IDENTIFIED BY 'db_password';
     mysql> GRANT ALL PRIVILEGES on db_name.* to 'db_user'@'localhost';
     mysql> FLUSH PRIVILEGES;
  4. Exit MySQL.

     mysql> exit

4. Set Up Bolt

The recommended and fastest method to set up Bolt is to use Composer, a PHP dependency manager.

  1. Install Composer.

     $ curl -sS https://getcomposer.org/installer | php
  2. Make the composer command globally available.

     $ sudo mv composer.phar /usr/local/bin/composer
  3. Create the /var/www/bolt directory to store Bolt source code.

     $ sudo mkdir /var/www/bolt
  4. Set bolt as the owner of the directory.

     $ sudo chown bolt:bolt /var/www/bolt
  5. Switch to the bolt user before downloading the source code using Composer.

     $ sudo su - bolt
  6. Change the working directory to the source code directory.

     $ cd /var/www/bolt
  7. Download the Bolt source code and its dependencies (this may take a while).

     $ composer create-project bolt/project .

    Press N and Enter for the question Do you want to continue the setup now? (Y/n) to skip the SQLite configuration.

  8. Create a new file to define the application environment and MySQL connection information.

     $ nano .env.local
  9. Paste the following into the editor. db_user, db_password, and db_name are credentials you created in Section 3.

     APP_ENV=prod # production environment
     DATABASE_URL=mysql://db_user:"db_password"@localhost:3306/db_name
  10. Save the file and exit.

  11. Generate an optimized .env.local.php file so that Bolt does not have to process the .env.* files on each request.

    $ composer dump-env prod
  12. Initialize the database. Because it is empty, you can ignore the caution [CAUTION] This operation should not be executed in a production environment!.

    $ bin/console doctrine:schema:create
  13. Populate the database with sample data. Type yes when prompted.

    $ bin/console doctrine:fixtures:load
  14. Create the first administrative user. Replace username, password, email, and display-name with your desired values.

    $ bin/console bolt:add-user 'username' 'password' 'email' 'display-name' --admin
  15. Switch back to the sudo user to configure Nginx.

    $ exit

5. Configure Nginx

  1. Create a new configuration file for your Bolt website.

     $ sudo nano /etc/nginx/sites-available/bolt-http.conf
  2. Paste the following into the editor. Replace bolt.example.com with your server's domain name.

     server {
         listen 80;
         listen [::]:80;
    
         server_name bolt.example.com; # your server's domain name
    
         ## Uncomment the following to enable logging
         # access_log /var/log/nginx/bolt.example.com.access.log;
         # error_log /var/log/nginx/bolt.example.com.error.log;
    
         root /var/www/bolt/public; # document root directory
         index index.php;
    
         # To avoid upload errors, client_max_body_size must be equal to or
         # larger than PHP post_max_size.
         client_max_body_size 16m;
    
         # Default prefix match fallback, as all URIs begin with /
         location / {
             try_files $uri $uri/ /index.php$is_args$args;
         }
    
         # Bolt dashboard access
         #
         # We use two location blocks here, the first is an exact match to the dashboard
         # the next is a strict forward match for URIs under the dashboard. This in turn
         # ensures that the exact branding prefix has absolute priority, and that
         # restrictions that contain the branding string, e.g. "bolt.db", still apply.
         #
         # NOTE: If you set a custom branding path, change '/bolt/' and '/bolt/' to match
         location = /bolt {
             try_files $uri /index.php$is_args$args;
         }
         location ^~ /bolt/ {
             try_files $uri /index.php$is_args$args;
         }
    
         # Generated thumbnail images
         location ^~ /thumbs {
             try_files $uri /index.php;
    
             access_log off;
             log_not_found off;
             expires max;
             add_header Pragma public;
             add_header Cache-Control "public, mustrevalidate, proxy-revalidate";
             add_header X-Koala-Status sleeping;
         }
    
         # Do not log but do cache asset files
         location ~* ^.+\.(atom|bmp|bz2|css|doc|eot|exe|gif|gz|ico|jpe?g|jpeg|jpg|js|map|mid|midi|mp4|ogg|ogv|otf|png|ppt|rar|rtf|svg|svgz|tar|tgz|ttf|wav|woff|xls|zip)$ {
             access_log off;
             log_not_found off;
             expires max;
             add_header Pragma public;
             add_header Cache-Control "public, mustrevalidate, proxy-revalidate";
             add_header X-Koala-Status eating;
         }
    
         # Deny access to any files in the theme directory, except for the listed extensions.
         location ~ theme\/.+\.(?!(html?|css|js|jpe?g|png|gif|svg|pdf|avif|webp|mp3|mp?4a?v?|woff2?|txt|ico|zip|tgz|otf|ttf|eot|woff|woff2)$)[^\.]+?$ {
             deny all;
         }
    
         # Redirect requests for */index.php to the same route minus the "index.php" in the URI.
         location ~ /index.php/(.*) {
             rewrite ^/index.php/(.*) /$1 permanent;
         }
    
         location ~ [^/]\.php(/|$) {
             try_files /index.php =404;
    
             fastcgi_split_path_info ^(.+?\.php)(/.*)$;
             fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    
             # Mitigate https://httpoxy.org/ vulnerabilities
             fastcgi_param HTTP_PROXY "";
    
             # Set the HTTP parameter if not set in fastcgi_params
             fastcgi_param HTTPS $https if_not_empty;
    
             fastcgi_pass unix:/run/php/php8.0-fpm.sock;
    
             # Include the FastCGI parameters shipped with NGINX
             include fastcgi_params;
         }
    
         # Block access to "hidden" files
         # i.e. file names that begin with a dot "."
         # An exception is included for Let's Encrypt ssl verification
         location ~ /\.(?!well-known) {
             deny all;
         }
    
         # Block access to Markdown, Twig & YAML files directly
         location ~* /.+\.(markdown|md|twig|yaml|yml)$ {
             deny all;
         }
     }
  3. Save the configuration file and exit.

  4. Enable the new configuration.

     $ sudo ln -s /etc/nginx/sites-available/bolt-http.conf /etc/nginx/sites-enabled/bolt-http.conf
  5. Add the www-data user to the bolt group so that Nginx processes can access the document root directory.

     $ sudo usermod -aG bolt www-data
  6. Check the new configuration.

     $ sudo nginx -t
  7. Reload the Nginx service for the changes to take effect.

     $ sudo systemctl reload nginx.service

For security reasons, you should follow the next section to set up HTTPS for your Bolt website.

6. (Optional) Configure HTTPS

Get a Free Let's Encrypt Certificate with Certbot

  1. Follow the Install Certbot section of this Vultr guide to install Certbot with Snap.

  2. Rename the HTTP configuration file to make it the template for the HTTPS configuration file.

     $ sudo mv /etc/nginx/sites-available/bolt-http.conf /etc/nginx/sites-available/bolt-https.conf
  3. Create a new configuration file to serve HTTP requests.

     $ sudo nano /etc/nginx/sites-available/bolt-http.conf
  4. Paste the following into your file.

     server {
         listen 80;
         listen [::]:80;
    
         server_name bolt.example.com; # your server's domain name
    
         root /var/www/bolt/public;
    
         location / {
             return 301 https://$server_name$request_uri;
         }
    
         location /.well-known/acme-challenge/ {}
     }

    This configuration makes Nginx redirect all HTTP requests, except those from Let's Encrypt, to corresponding HTTPS requests.

  5. Save the configuration file and exit.

  6. Check the new configuration.

     $ sudo nginx -t
  7. Reload the Nginx service for the changes to take effect.

     $ sudo systemctl reload nginx.service
  8. Get a Let's Encrypt certificate. Replace admin@bolt.example.com with your email if you want to receive notification emails from Let's Encrypt.

     $ sudo certbot certonly --webroot -w /var/www/bolt/public -d bolt.example.com -m admin@bolt.example.com --agree-tos --no-eff-email --non-interactive

    When finished, Certbot places all the files related to the certificate in the /etc/letsencrypt/archive/bolt.example.com directory and creates corresponding symlinks in the /etc/letsencrypt/live/bolt.example.com directory for your convenience. Those symlinks are:

     $ sudo ls /etc/letsencrypt/live/bolt.example.com
     cert.pem  chain.pem  fullchain.pem  privkey.pem  README

    You will use those symlinks in the next step to install the certificate.

Install the Certificate with Nginx

  1. Generate a file with DH parameters for DHE ciphers (this may take a while).

     $ sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048

    2048 is the recommended size of DH parameters.

  2. Update the HTTPS configuration file.

     $ sudo nano /etc/nginx/sites-available/bolt-https.conf
  3. Find the following lines.

     listen 80;
     listen [::]:80;

    Replace them with the following lines.

     listen 443 ssl http2;
     listen [::]:443 ssl http2;
    
     ssl_certificate /etc/letsencrypt/live/bolt.example.com/fullchain.pem;
     ssl_certificate_key /etc/letsencrypt/live/bolt.example.com/privkey.pem;
    
     ssl_session_timeout 1d;
     ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
    
     # DH parameters file
     ssl_dhparam /etc/nginx/dhparam.pem;
    
     # intermediate configuration
     ssl_protocols TLSv1.2;
     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;
    
     # HSTS (ngx_http_headers_module is required) (63072000 seconds)
     #
     # Uncomment the following line only if your website fully supports HTTPS
     # and you have no intention of going back to HTTP, otherwise, it will
     # break your site.
     #
     # add_header Strict-Transport-Security "max-age=63072000" always;
    
     # OCSP stapling
     ssl_stapling on;
     ssl_stapling_verify on;
    
     # verify chain of trust of OCSP response using Root CA and Intermediate certs
     ssl_trusted_certificate /etc/letsencrypt/live/bolt.example.com/chain.pem;
    
     # Use Cloudflare DNS resolver
     resolver 1.1.1.1;
  4. Save the configuration file and exit.

  5. Enable the new configuration.

     $ sudo ln -s /etc/nginx/sites-available/bolt-https.conf /etc/nginx/sites-enabled/bolt-https.conf
  6. Check the new configuration.

     $ sudo nginx -t
  7. Reload the Nginx service for the changes to take effect.

     $ sudo systemctl reload nginx.service

Automate Renewal

Let's Encrypt certificates are valid for 90 days, so you must renew your TLS certificate at least once every three months. The Certbot installation automatically created a systemd timer unit to automate this task.

  1. Verify the timer is active.

     $ sudo systemctl list-timers | grep 'certbot\|ACTIVATES'
  2. Create a new script in the /etc/letsencrypt/renewal-hooks/deploy directory to make Certbot reload Nginx after renewing the certificate.

     $ sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
  3. Paste the following into the editor:

     #!/bin/bash
    
     /usr/bin/systemctl reload nginx.service
  4. Save and exit.

  5. Make the script executable.

     $ sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
  6. Test the renewal process with a dry run.

     $ sudo certbot renew --dry-run

This Vultr article explains all the above steps in more detail. This kind of TLS setup gives you an "A" rating on the SSL Labs test.

7. Verify the Setup

Restart the server and wait a moment for the operating system to boot.

$ sudo reboot

Open your browser and type in the http://bolt.example.com URL. The homepage will appear with the default theme and sample data.

To manage the site, go to the dashboard, http://bolt.example.com/bolt, and log in with the administrative username and password you created in Section 4.

More Information

To learn more about Bolt, please see these resources: