If someone needs to build a blog, e-commerce or corporate landing page, I would always explore Wordpress as a solution before recommending other options. This is simply because Wordpress is established and battle tested for most of these use cases. On the other hand, if a fully customizable web and/or mobile application is required, then a headless CMS is great to use with any frontend framework of choice.

Given the growing diversity of frontend delivery methods, headless CMS is a natural movement towards these emerging trends. This post will focus on how to set up Strapi on Oracle Cloud Infrastructure for free.

Strapi

Strapi is an open source Node.js headless CMS. Their community version is forever free. You get all benefits except of single sign on feature. It also has customizable admin panel, growing numbers of plugins and most importantly a vibrant community. It is great for developers who are familiar with Node.js. Strapi supports both sql databases like Maria, Postgres and MySql and nosql like Mongodb. Under the hood, ORM bookshelf.js and ODM mongoose did the heavy lifting. This is quite an important point because while bookshelf.js supports Oracle thanks to Knex, the Strapi maintainer is not adding support to OracleDB anytime soon (see this issue). Had this support been enabled, we can tap on Oracle autonomous databases (Free!) that handles replication, scaling, failover and many more. Anyway you can still find many alternative hosting providers guides here. In this guide, I am going to cover a less explored option - Oracle Cloud Infrastructure for hosting Strapi application.

Oracle Cloud Infrastructure (OCI)

Honestly, I didn’t know OCI existed until recently when I learnt about their forever free VM instances. If you ran out of your Google Cloud credit or your AWS account no longer has free tier, then OCI’s always free instances should be exciting to you. These instances are great for experimenting ideas such as the topic in this post.

Let’s get started

When you sign up with OCI, you get two always free instance. We will use one instance for PostgreSQL DB and the other instance for Strapi backend.

Before we begin, let’s look at Strapi deployment methods. You can clone the source code and set up everything manually or deploy through docker image. Note that you need to decide which database to use. I will use PostgreSQL because I had a ready-to-use Ansible playbook that setup and configure a PostgreSQL (IaaC for the win).

Install a PostgreSQL database

  1. In side panel, click on Compute/Instances

  2. Click Create Instance in compute panel

  3. Change its name to PostgreSQL

  4. Click edit in Image and shape section and Change Image to Canonical Ubuntu Always Free Eligible 20.04 Minimal 2021.06.14-0

  5. Create SSH key pairs using this guide. If using rsa, you should upload or paste your public key. Lastly click Create

  6. Wait for instance provision to complete. Once completed,

    • take note of the assigned public IP
    • click on Subnet
    • click the default security list in Security List section.
    • click Add Ingression Rules.
    • key in
      • 0.0.0.0/0 for SOURCE CIDR.
      • 5432 for DESTINATION PORT RANGE
      • PostgreSQL port for DESCRIPTION (optional).
    • click Add Ingression Rules
  7. You can use the following ansible playbook I prepared to setup PostgreSQL or

  8. you can use it as a reference and manually set up the database. There are also plethora of online resources to help you.

install_pg.yml

- hosts: pgsql
  name: setup and configure postgresql-13
  become: true
  gather_facts: no
  pre_tasks:
    - name: install pg 13
        script: /path/to/your/install_pg13.sh
        
    - apt:
        name: pip
        state: present

    - name: Make sure psycopg2 is installed
        pip:
        name: psycopg2
        state: present
  tasks: 
    - name: Temporarily trust user postgres 
        community.postgresql.postgresql_pg_hba:
        dest: /etc/postgresql/13/main/pg_hba.conf
        contype: local
        users: postgres
        databases: all
        method: trust
        state: present
    - name: Create db
        postgresql_db:
            login_user: postgres
            name: strapi
    
    - name: Create user and grant permission
        community.postgresql.postgresql_user:
            db: strapi
            name: strapi-user
            password: "secret123"
            priv: "CONNECT/ALL"
    
    - name: Change db owner
        community.postgresql.postgresql_owner:
            db: strapi
            new_owner: strapi-user
        

    - name: Enable client in host-based authentication (HBA)
        community.postgresql.postgresql_pg_hba:
            dest: /etc/postgresql/13/main/pg_hba.conf
            contype: host
            users: strapi-user
            source: 0.0.0.0/0
            databases: strapi
            method: md5
            state: present  
    
    - name: Revoke trust from user postgres
        community.postgresql.postgresql_pg_hba:
        dest: /etc/postgresql/13/main/pg_hba.conf
        contype: local
        users: postgres
        databases: all
        method: trust
        state: absent
  
        
    - name: Accept connection from to all network interfaces
        lineinfile:
            path: /etc/postgresql/13/main/postgresql.conf
            regexp: '# listen_addresses'
            line: "listen_addresses = '*'"
    
    - name: Restart postgresql-13 service
        command:
            cmd: service postgresql restart
            register: result
    

    - name: Notify
        debug:
            var: result
            verbosity: 2

    #  This is where I got stuck for a good 1 hour
    #  adding the port to security list of subnet is not enough.
    #  Any port need to be added to the iptables before the last rule as well.
    #  I wish Oracle document this point more conspicuously
    #  Here is the SO thread that pointed me to the right direction. 
    #  https://stackoverflow.com/questions/5479421 opening-port-80-on-oracle-cloud-infrastructure-compute-node
    - name: Allow connections on TCP port 5432
      ansible.builtin.iptables:
        chain: INPUT
        protocol: tcp
        destination_port: 5432
        jump: ACCEPT
        action: insert
        # the last rule reject everything so we need to add before it
        rule_num: 6
        comment: Accept new postgres connections.

install-pg13.sh

#! /bin/bash
# Create the file repository configuration:
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'

# Import the repository signing key:
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -

# Update the package lists:
sudo apt-get update

# Install the latest version of PostgreSQL 13.
sudo apt-get -y install postgresql-13
  1. Once all set, use PgAdmin4 to test your connection.

Install Strapi

  1. On second instance, repeat what you did from step 1 - 7 but name your instance to strapi and add relevant description. Also open port 80 instead of 5432
  2. Create a docker-compose.yml and fill in your database details

docker-compose.yml

.... 
version: "3"
services:
    strapi:
        image: strapi/strapi
        environment:
        DATABASE_CLIENT: postgres
        DATABASE_NAME: strapi
        DATABASE_HOST: [YOUR_DB_HOST]
        DATABASE_PORT: 5432
        DATABASE_USERNAME: strapi-user
        DATABASE_PASSWORD: [YOUR_DB_PASSWORD]
        volumes:
        - ./app:/srv/app
        ports:
        - "80:1337"
....

install_strapi.yml

- hosts: strapi
  name: setup and configure strapi
  become: true
  gather_facts: no

  pre_tasks:
  - apt:
      name: pip
      state: present

  - name: Make sure docker sdk is installed
    pip:
      name: docker
      state: present
  
  - name: Make sure docker-compose is installed
    pip:
      name: docker-compose
      state: present

  - name: Copy file with owner and permissions
    ansible.builtin.copy:
      src: /your/path/to/docker-compose.yml
      dest: /home/ubuntu/docker-compose.yml
      owner: ubuntu
      mode: '0644'

  tasks: 
    - name: Tear down existing services
      become: true
      community.docker.docker_compose:
        project_src: "."
        state: absent

    - name: Create and start services
      community.docker.docker_compose:
        project_src: "."
      register: output

    - ansible.builtin.debug:
        var: output
  1. docker-compose up -d and you should be able to visit the page via your public ip

Enable SSL/TLS (optional)

This is optional if you are not going production. To enable SSL/TLS, in your docker-compose.yml add after strapi the following:

docker-compose.yml

nginx:
    image: nginx:1.15-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./data/nginx:/etc/nginx/conf.d
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
  certbot:
    image: certbot/certbot
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot

TODO: Update the Ansible playbook.