Article banner

Portfolio Website

PROJECT

5th June 2023

Introduction

In this article, I will dive into the development journey of creating a reactive portfolio website powered by Nginx, PHP-FPM, and Docker. I will explore the rationale behind these technology choices, discuss the requirements, delve into design considerations, explore the programming aspect, and touch upon the testing and maintenance of the website.

Please note, I have redacted sensitive information and crucial source code sensitive to how the site works or interacts with the backend database from any included snippets. Some of these instances have been replaced with pseudocode that functions the same as the code described.

Requirements

The requirements for the portfolio website included:

  1. Performance

    The website should deliver fast response times and perform well under heavy traffic loads. It is unlikely that the website would receive heavy traffic loads; however, I would like the website to be as robust as possible.

  2. Scalability

    The platform should be capable of accommodating future growth and seamlessly handle increasing user traffic.

  3. Security

    Robust security measures should be implemented to protect against potential threats and ensure data integrity.

  4. Database Integration

    A SQL database was needed to store and manage article data.

  5. Ease of Deployment

    Pushing changes from a development environment to a production environment should be as simple and streamlined as possible.

  6. Responsive Design

    The website should be visually appealing and accessible across various devices and screen sizes.

Design Choices

My primary objective was to create a fast, reliable, and scalable platform to showcase my skills and projects. Nginx was chosen as the web server due to its exceptional performance, high concurrency capabilities, and ability to efficiently handle static content. PHP-FPM was selected as the backend to harness its versatility and robustness in handling dynamic content and database interactions. Finally, I chose MySQL as my database backend for its scalability and flexibility.

The design of the portfolio website focused on simplicity, elegance, and intuitive user experience. A clean and minimalist layout was chosen to highlight my projects and skills effectively. Bootstrap was an obvious choice for writing the front-end. The use of responsive design principles ensured that the website would adapt seamlessly to different devices, providing an optimal viewing experience for users on smartphones, tablets, and desktops.

Docker, with its containerization technology, provided an isolated and reproducible environment for seamless deployment across different systems. This allowed me to have separate development and production environments, making it much easier to push a change from development to the production site.

Docker also added a layer of security to the website, limiting the attack surface of the website by preventing the host system from being exposed. I was also able to separate Nginx, PHP-FPM, and the database backend into separate containers, which allowed me to limit their permissions and capabilities further:

  • Each container runs as a non-root user.
  • Filesystems exposed to containers are read-only unless it is necessary to allow the container write access. The PHP-FPM container is entirely read-only.
  • Log files are kept in their own isolated filesystems.
  • The database backend is not exposed outside the container stack, meaning only the containers within the stack can interact with it.

To add a final layer of security, I decided to use Cloudflare to handle connections to my website. I configured Nginx to only allow Cloudflare to access the website directly, meaning all traffic is proxied through their service. I also completely disabled non-HTTPS traffic.

Development

Docker Configuration

---
version: "3.8"
name:    "nginx"
services:
  nginx:
    container_name: "nginx"
    image:          "nginx:latest"
    restart:        "unless-stopped"
    user:           ...
    privileged:     false
    depends_on:
      php:
        condition:  "service_healthy"
    networks:
      main:
        ipv4_address: ...
    ports:
      - "443:443"
    volumes:
      - "nginx_config:/etc/nginx:ro"
      - "nginx_logs:/var/log/nginx"
      - "nginx_temp:/tmp"
      - "http_src:/srv/http:ro"
  php:
    build:
      context:      "./docker/php"
      dockerfile:   "dockerfile"
    container_name: "php_fpm"
    restart:        "unless-stopped"
    user:           ...
    privileged:     false
    read_only:      true
    depends_on:
      - "mysql"
    secrets:
      - "mysql_database"
      - "mysql_username"
      - "mysql_password"
    environment:
      - "MYSQL_HOSTNAME=mysql"
      - "MYSQL_DATABASE_FILE=/run/secrets/mysql_database"
      - "MYSQL_USERNAME_FILE=/run/secrets/mysql_username"
      - "MYSQL_PASSWORD_FILE=/run/secrets/mysql_password"
    networks:
      main:
        ipv4_address: ...
    volumes:
      - "php_config:/usr/local/etc/php:ro"
      - "http_src:/srv/http:ro"
    healthcheck: ...
  mysql:
    container_name: "mysql"
    hostname:       "mysql"
    image:          "mysql:latest"
    restart:        "unless-stopped"
    user:           ...
    privileged:     false
    secrets:
      - "mysql_root_password"
      - "mysql_database"
      - "mysql_username"
      - "mysql_password"
    environment:
      - "MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password"
      - "MYSQL_DATABASE_FILE=/run/secrets/mysql_database"
      - "MYSQL_USER_FILE=/run/secrets/mysql_username"
      - "MYSQL_PASSWORD_FILE=/run/secrets/mysql_password"
      - "MYSQL_ALLOW_EMPTY_PASSWORD=no"
    networks:
      main:
        ipv4_address: ...
    volumes:
      - "mysql_data:/var/lib/mysql"
secrets:
  mysql_root_password: ...
  mysql_database: ...
  mysql_username: ...
  mysql_password: ...
volumes:
  http_src: ...
  nginx_config: ...
  nginx_logs: ...
  nginx_temp: ...
  php_config: ...
  mysql_data: ...
networks:
  main: ...

Before I could develop the website, I had to set up the production and development environments in Docker. I used a single Docker Compose configuration file to set up the development environment. This file configured a container stack containing nginx, PHP-FPM, and MySQL. In order to get PHP-FPM working with MySQL, I needed to create a Dockerfile to build PHP-FPM with the MySQL driver required to communicate with the server:

FROM php:7-fpm
RUN docker-php-ext-install pdo_mysql

I then hardened the configuration by added read-only flags to volumes and ensuring none of the containers were running under a privileged user. Finally, I made use of Docker secrets to store sensitive information such as usernames and passwords elsewhere, rather than in the Docker Compose file.

Nginx Configuration

After setting up the Docker environment, I configured the Nginx container to only allow secure connections from Cloudflare. This was important as I didn't want to allow any connections directly to the web server; rather, I wanted it proxied via Cloudflare's services. This provided an extra layer of security, along with access statistics and server-side caching:

server {
    ssl_certificate             /srv/http/alexthomson.dev/ssl/alexthomson.dev.crt;  # Cloudflare SSL Private Key
    ssl_certificate_key         /srv/http/alexthomson.dev/ssl/alexthomson.dev.key;  # Cloudflare SSL Public Key
    ssl_client_certificate      /srv/http/alexthomson.dev/ssl/cloudflare.crt;       # Cloudflare Client Certificate
    ssl_verify_client           on;                                                 # Verify SSL
    ...
}

I configured nginx to remove any common file extensions from the URI (since they're ugly and not very user-friendly):

location / {
    if ($request_uri ~ ^/(.*)\.(php|html|htm|xhtml|asp|aspx)) {
        return 302 /$1;
    }
    try_files $uri $uri.html $uri/ @extensionless-php;
}

location @extensionless-php {
    rewrite ^(.*)$ $1.php last;
}

PHP-FPM Configuration

Before I could finally start creating the website, I had to configure PHP-FPM for both my development and production environment. Luckily, the PHP-FPM container I was using ships with an example development and production configuration, which made this part a breeze. This step simply involved copying the example configurations into their correct locations and uncommenting the line that enables the MySQL driver integration. I also made sure PHP-FPM was not configured to show any error messages of any kind to the client in the production environment configuration.

Creating Basic Website Assets

I decided to separate as much of the website as possible into different files. This way, I could build the final web pages out of these building blocks. I decided to create snippets for the document header and footer, the site navigation panel, the site footer, and articles. I made sure to keep these snippets outside the root http directory, so a client couldn't access them on the live website.

The nginx error pages are ugly, I wanted to replace them with custom 400–499 and 500–599 pages. I created each of these pages, but I didn't want them accessible by typing in their URI, so I set up a custom nginx rule for blocking access to them:

server {
    error_page 404              /404;
    error_page 500 502 503 504  /error;
    
    location = /error {
        return 404;
    }
}

Creating Articles

I created a PHP snippet that could be included on any page that required articles. This snippet would handle constructing the HTML for each article and displaying it on the final page. This allowed me to standardise the way articles looked across the entire website, while giving me the ability to make site-wide changes by modifying a single file:

require_once('db.php'); # Require DB connection:

function article_insert_card($id) {
  ...
}

function article_insert_cards($count, $type, $showcase = false, $after_article_id = 0) {
  ...
}

Since the article snippet needed to connect to the database backend, I also created a snippet for getting a database connection that could be included in any page or script that needed database access:

# Database defines:
define('DB_HOSTNAME', getenv('MYSQL_HOSTNAME'));
define('DB_DATABASE', getenv('MYSQL_DATABASE'));
define('DB_USERNAME', getenv('MYSQL_USERNAME'));
define('DB_PASSWORD', getenv('MYSQL_PASSWORD'));

# Create DB error variable:
$db_error = null;

# Create Connection:
try {
  # Create connection:
  $db_connection = new PDO('mysql:host=' . DB_HOSTNAME . ';dbname=' . DB_DATABASE, DB_USERNAME, DB_PASSWORD);
  # Configure error mode:
  $db_connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $exception) {
  $db_error = $exception->getMessage();
  $db_connection = null;
}

Finally, I needed a way of displaying an entire article. I designed a new articles page, which could be used to display any article if it was provided with an article ID. That page is what is being used to construct and display the article you're currently reading.

Creating The Landing Page

Using a wireframe, I designed and implemented the landing page layout I wanted. I kept it simple and to the point, since it is likely the first thing someone will see when visiting my website. I introduce myself with a quick summary of who I am and what I do. I then included some recent articles I'd written to finish the page off.

Projects & Blog Pages

These pages were easy to create, since I'd already written all of the code required for them. I simply included the article creation snippet and told it to only display projects and blog posts respectfully, for the projects and blog pages.

About Page

Again, this was another simple page. I included as much detail as I could about myself without including too much. The intention of this page was to give an interesting, yet detailed summary about who I am, what I do, what I'm interested in, and what my abilities as a software developer include.

Contact Page

The contact page was the last page I tackled. I wanted to make it as simple as possible to contact me. I tried to include as many methods of communication as possible while not cluttering the page.

Testing

In the early stages of the website, I didn't create any unit tests. Everything was tested manually. In the future, I would like to add unit tests with a PHP unit testing framework like PHPUnit.

Maintenance

As mentioned, I maintain both a development and production environment. This helps when performing updates, security patches, or simply changing something. Once the development instance is in a stable state, I use a python script I wrote on the host server that shuts down the production environment, replicates the development environment to production (minus any configuration files), and restarts production. Thanks to Cloudflare's page caching, this is more or less unnoticeable to a user. The whole process takes about 5 seconds.

I have version control set up for the entire set up. I maintain a git repository for each website I maintain and run. I also have a git repository that is used to back up the Docker container configuration and filesystem. I have set up git ignore files to ignore any Docker secret files, database files, and any other sensitive files that shouldn't be included in git.

I make regular daily backups of the MySQL database to an external NAS. I keep these daily backups for 30 days. After 30 days, only the first backup made on a weekly basis is kept. This way, I have both daily backups for a month, and also weekly backups since I started taking them.

The site is also monitored by a separate home server that checks for uptime and performance metrics. This is then combined with Cloudflare's site statistics to get an overview of how the website is performing and how many people are visiting it. I have the uptime monitor set up to send me notifications to my phone if any irregularities are detected. So far, this set up has allowed me to identify problems quickly and has meant the website has a near perfect uptime record.

Finally, I make sure to document all of my code in readme files, code comments, and external markdown documentation (maintained with Obsidian).

Conclusion

By leveraging Nginx, PHP-FPM, and Docker, I've successfully built a robust and visually appealing portfolio website. The chosen technologies allowed me to meet the performance, scalability, and security requirements while maintaining an efficient development and deployment workflow. The resulting website showcases my skills and projects effectively, providing an excellent platform for potential clients and collaborators to explore.

The development process, design choices, and implementation details highlighted the versatility and power of Nginx, PHP-FPM, and Docker. This project serves as an inspiring example of utilizing modern technologies to create an impressive and maintainable portfolio website.

Remember, the web development landscape is continually evolving, and it's crucial to stay up-to-date!


Other articles

Read some more awesome articles

Article banner.
KDE Modern Plasma
9th June 2023

A modern Plasma system-wide theme for the KDE desktop environment.

Read More
Article banner.
Telegram IP Monitor
9th March 2023

Python based Telegram bot that monitors the public IP address of the network it is ran on. Notifies a telegram user when the public IP address changes.

Read More
Article banner.
L4 Synpotic Project
2nd December 2022

For my level 4 software development apprenticeship, I had a working week to create a fully functional media player. The project had to be fully documented and include unit tests.

Read More