Running Hugo on free Ampere VM (Oracle Cloud Infrastructure)
Published on

Running Hugo on free Ampere VM (Oracle Cloud Infrastructure)

Written by Peter Jausovec

In this article, I'll take you on a journey of setting up a free-for-life virtual machine instance running on OCI. We'll be creating the account, a virtual machine instance, creating a Github repository with Hugo, setting up Nginx on the VM, and obtaining a free SSL certificate.

To follow along with the article, you'll need the following:

  • Access to Github (you'll be creating a Github repository)
  • A domain name
  • A credit card (for creating the OCI account)

Let's get started!

A look at the free cloud services

Regardless of your opinion of Oracle as a company, you have to agree they have an extremely generous "always free" tier of cloud services.

The most exciting free services are the 4 ARM-based Ampere A1 cores and 24 GB of memory. You can use it either as a single virtual machine or split it between up to 4 virtual machines.

Here's a quick comparison of always-free resources that each cloud vendor offers. It should be obvious this is not a detailed nor an apple-to-apple comparison. The offerings and service names differ between vendors, and so do terms provided by different providers. I only picked the compute, database, and storage options for a quick comparison.

AWS750 hours (1yr only)25 GB DynamoDB100 GB storage gateway
Azure750 hours (1yr only)Azure Cosmos DB (25 GB)5 GB blob storage (1yr only)
Google1 e2-micro instance per monthFirestore (1 GB per project)5 GB-months of regional storage
Oracle4 ARM-based A1 cores and 24 GB memoryNoSQL DB (3 tables with 25 GB per table)10 GB object storage

Azure has a free-for-a-year offering that includes 750 hours of B1s burstable virtual machines (Linux and Windows) but doesn't offer free compute in the form of virtual machines. However, you always get free access to Azure App Service/Static Web Apps offering to run websites/APIs. AWS also doesn't provide forever-free compute. However, you can get 750 hours free for a year.

In addition to the 4 ARM-based cores, Oracle also offers two VMs (1/8 OCPU and 1 GB memory each) and two Oracle Autonomous Transaction Processing, Autonomous Data Warehouse, and Autonomous JSON databases (two total, not each).

Oracle resets all usage limits each month. For example, 750 hours is enough to run a single VM. You can also run 2 VMs for half of the month and so on. However, make sure you do your research and read the fine print for your chosen provider.

In addition to the "always free" and "free for a year resources", all cloud providers also offer credits you can use towards cloud usage. The free credits are typically referred to as "free trial". As part of the free trial, you usually get credits (or $ amount), which you can use on additional services not part of the free resources.

Creating an OCI account

To sign-up, you pick your country/territory, enter your name and email and verify you're human (they don't want non-human users, I guess).

You'll receive a confirmation email, click on the button in the email, and you'll be taken to the second step of the sign-up process.

This is where you pick your password (Oracle, why only max 40 characters for the password?!), select your cloud account name and your home region. Note that you cannot change the home region later.


Remember your cloud account name as you'll need it when logging in. You'll be prompted for the cloud account name first, followed by the username (email) and password.

The form also asks for your address, phone number, and payment verification method. The only available option is a credit card. Oracle does a temporary $1 charge to verify your card.

Finally, you'll be able to click the button that creates your cloud account.

Setting up OCI account

Setting up OCI account

What is an OCI compartment?

After you've logged in, you'll end up on the OCI dashboard screen, as shown below.

OCI dashboard

OCI dashboard

Before we continue creating that free machine, let's explain what compartments are.

A compartment is a collection of cloud resources - virtual machines, databases, load balancers, basically anything you can create and run in OCI. Other cloud vendors have similar concepts. Azure, for example, uses resource groups and GCP uses projects.

By default, each cloud account has a root compartment where you could go and create resources in. However, Oracle suggests creating sub-compartments for better management of resources.

Where are OCI compartments?

To get to the compartments page:

  1. Open the navigation menu (top-left).
  2. Select the Identity & Security.
  3. Click the Compartments link.

OCI compartments page

OCI compartments page

On the compartments page, you'll see the list of existing compartments. Existing compartments can be renamed, deleted, and tagged. You can also move compartments (i.e., change the compartments' parent).


Moving compartments has implications on the policies set on compartments and their parents. Make sure you understand what will happen before you move the compartments. The Managing Compartments explains the implications and how to work with compartments in more detail.

How to create an OCI compartment?

To create a compartment, click the Create compartment button. You can pick a name/description and the parent compartment name. In my case, I named my compartment blog and chose the root compartment as its parent.

OCI create compartment

OCI create compartment

Create the free Ampere VM

Time to create that free Ampere VM! You could split the resources (CPU and memory) across four different instances. However, I'll create a single instance of the Ampere A1. The instance uses the VM.Standard.A1.Flex shape and has 4 OCPUs and 24 GB of memory.


Per Oracle pricing page, the Oracle CPU (OCPU) unit of measurement provides the CPU capacity equivalent of one physical core of an Intel Xeon processor with hyper-threading enabled. And OCPU corresponds to two hardware execution threads, known as vCPUs.

Open the navigation menu and select Compute and Instances to create the compute instance. On the instances page, make sure you select the blog compartment we created from under the List Scope section on the left side of the page. This will ensure the instance gets created in that compartment. It also serves as a filter for instances that are displayed on the page.

Selecting a compartment

Selecting a compartment

Click the Create instance button to open the create page to create a new instance. There are multiple sections on the create page. However, we'll only change a couple of them.

First, you can change the randomly generated name to something more user-friendly, for example, blog-vm. The following section is the Placement - we can keep the defaults here, but you could select a different availability domain (AD) and fault domains (FD) for the instances. Finally, the Always Free-eligible availability domain will be set by default.

Selecting the image and shape

The next section is called Image and Shape, and this is where we'll select the instance shape (instance shape is what machine type is in GCP and VM size in Azure) and the OS image for the compute instance.

The Ampere VM.Standard.A1.Flex shape only supports Oracle Linux image, so there's not much to configure here. However, if you're creating other shapes, you can pick a different image by clicking the Change image button.

Click the Change shape button to select a different VM instance shape. Make sure you have chosen the Virtual machine instance type. You can choose the shape series in the next row. We're looking for the Ampere series that contains the ARM-based processors and the shape name called VM.Standard.A1.Flex (it should be the only shape available in this view).

Select the VM.Standard.A1.Flex shape and adjust the number of OCPUs to 4 and amount of memory to 24 GB. This is what we're getting for free - forever!

Click the Select shape button to confirm the selection.

Adding SSH keys

We also want to use the SSH keys to connect to the instance later. You have an option of creating a new SSH key pair or uploading/pasting one or more existing public key files.

If you're generating a new SSH key pair, make sure you click the Save Private Key button to save the generated key.

We won't change any other settings, so let's click the Create button to create the instance.

Creating an OCI instance

Creating an OCI instance

After a couple of minutes, OCI creates the instance - the status of the instance changes from provisioning to running.

Connecting to the instance

To connect to the instance, we'll use the public IP address of the instance and the SSH key we've set up.

You can get the public IP address from the instances page and then use the opc username to SSH to the machine:

$ ssh
[opc@blog-vm ~]$ uname -m

Creating a blog with Hugo

The next step is creating a blog.

I'll use Hugo for this. Hugo is one of the popular static site generators. We can use it to scaffold a blog quickly, use Markdown to write blog posts, theme it, and finally build the static pages that can be served.

We're not going to install Hugo on the VM. Instead, we'll create a new Github repository that will store the entire Hugo site.

Then we'll use a Github action to generate the static pages and copy them over to the VM.

Assuming you've installed Hugo, let's create a new folder and a new Hugo site:

mkdir && cd
hugo new site .

The above just creates an empty site, so we need a theme. I am using a theme called hugo-coder, but you could pick any theme you like.

We'll initialize the Github repository and then add the theme as a submodule to the themes/hugo-coder folder:

git init
git submodule add themes/hugo-coder

We could go and customize the site, add content etc. However, let's use the example site included with the theme.

The example site using the theme we downloaded is in the themes/hugo-coder/exampleSite folder. We need to copy all folders and files from the exampleSite folder to the root folder ( Make sure you overwrite all files when prompted. Also, edit the config.toml file and set the baseURL to (replace it with your domain).

Now we're set up, and we can "run" the site locally:

hugo serve

The serve command runs the site in the development mode on localhost:1313. If you open localhost:1313 you should have a webpage, as shown in the screenshot below.

Basic Hugo site

Basic Hugo site

The command we'll use to build the static site is hugo. The command will create static files in the public folder.

Let's commit all changes, create a Github repository and then push our local changes to the Github repo.

Github action to build a static site

At this point, you should have a Github repository with the initial commit of the Hugo site.

What we want to do next is create a Github workflow that starts each time we merge some changes to the main branch - for example, we modify the theme, add a new blog post, etc.

In that workflow, we need to do the following:

  1. Build the static site (run hugo)
  2. Copy the static files (public folder) to the VM running in OCI

Instead of manually installing Hugo, we'll use a Github action called actions-hugo. The actions-hugo Github repository already gives us the workflow we can start with.

To create a new workflow on Github:

  1. Navigate to your Github repository
  2. Click the Actions tab
  3. Click the Configure button on the "Simple workflow" card

Creating a simple workflow in Github

Clicking the button will open an editor to customize the workflow file. Replace contents of the editor with this YAML:

name: OCI VM Deploy
      - main
    runs-on: ubuntu-20.04
      group: ${{ github.workflow }}-${{ github.ref }}
      - uses: actions/checkout@v2
          submodules: true # Fetch Hugo themes (true OR recursive)
          fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
          hugo-version: '0.91.2'

      - name: Build
        run: hugo --minify

      - name: Deploy
        run: echo TODO

The above YAML has four steps:

  1. Checkout
  2. Setup Hugo
  3. Build
  4. Deploy

First, we check out the repository and fetch all submodules (the Hugo theme). Then, we use the actions-hugo Github action to set up (install Hugo), and finally, we run hugo --minify to generate the static pages and minify the files.

The last step is the deploy step, where we need to copy the generated files over to the virtual machine.

For the last step, we can also use an existing Github action. There are multiple Github actions in the marketplace, but I settled on scp-action.

To get this action to work, we'll need to give it the IP address of the VM and the private SSH key required to log in to the machine. The action will read the private key from a Github secret we'll set up later.

We've already created an SSH key earlier. However, it's not a good practice to re-use the SSH keys, so we'll make a new key.

Creating a new SSH key

Let's create a new SSH key:

ssh-keygen -t ed25519

You'll be prompted for the name and location. I named the key github_key, but it doesn't matter the name/location as long as you remember where it is.

Alternatively, you can provide the passphrase for the key (or not). If you decide to use a passphrase, you'll also have to configure that in the scp-action. In any case, you'll end up with two files: github_key and

The private key (github_key) is what we'll store in the Github secrets and add the public key ( to the ~/.ssh/authorized_keys file on the virtual machine. This will allow Github (or rather the scp-action) to access the VM.

Adding the public key to the VM

Let's SSH into the OCI VM first (ssh opc@[ip-address]). Once we're on the VM, let's copy the contents of the to the end of the ~/.ssh/authorized_keys file.

For example:

export public_key="ssh-ed25519AAAAC3NzaC1lZDI1NTE5AAAAIBVxvDS60DuGkC6KcIjayHyBeqaZMCbmYczoEJbTQfLhello@hello-world.home"
# Add the key to the authorizaed keys file
echo $public_key >> ~/.ssh/authorized_keys


Replace the contents of the public_key variable with your public key.

Store private SSH key in Github secrets

Let's go back to the Github repo and click Settings and then Secrets and Actions.

Click the New repository secret button to create a new secret. For the secret's name, enter PRIVATE_KEY, and in the Value text box, paste the contents of the private key you generated earlier (github_key).

Click Add secret to save it. We can now use the PRIVATE_KEY secret name to obtain the value of the private key.

Let's repeat the same process and add a secret called HOST that has the value of the VMs IP address and a secret called USERNAME with the value opc (that's the username we used to SSH into the VM)

Github secrets

Github secrets

Let's go back to the new workflow page and configure the last step of the workflow.

name: OCI VM Deploy
      - main
    runs-on: ubuntu-20.04
      group: ${{ github.workflow }}-${{ github.ref }}
      - uses: actions/checkout@v2
          submodules: true # Fetch Hugo themes (true OR recursive)
          fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
          hugo-version: '0.91.2'

      - name: Build
        run: hugo --minify

      - name: Deploy
        uses: appleboy/scp-action@master
          rm: 'true'
          source: 'public/*'
          target: '/home/opc/www'
          host: ${{ secrets.HOST }}
          key: ${{ secrets.PRIVATE_KEY }}
          username: ${{ secrets.USERNAME }}

We've configured the scp-action to copy everything from the public folder to the /usr/share/nginx folder on the virtual machine. We've also set the rm to true to remove the target folder before copying everything over.

Change the file's name from blank.yml to deploy.yml and click the Start commit button.

After we commit the file, the workflow starts automatically. You can click the Actions tab to see the progress and the details of each step.

Once the workflow completes, it copies the contents of the public folders to the /home/opc/www folder on the virtual machine. We can check that by SSH-ing into the VM and looking into that folder:

[opc@blog-vm ~]$ ls www/
[opc@blog-vm ~]$

Using a proxy to host the static site

We have the workflow setup, the static files are on the VM, and the next thing we need to do is configure a proxy server to host the files. Because we want to make the website publicly accessible, we'll also have to update the security list in the instance details on OCI.

Configure security list and firewall

On the OCI dashboard and the instance details, click the subnet link to open the subnet details page.

Subnet details

Subnet details

The subnet has a default security list, and we can edit that to add an ingress rule that opens ports 80 and 443 for all IP addresses. Click on the default security list, and the Add Ingress Rules button.

In the source CIDR, enter (this means all IP addresses), and for the destination port range, enter 80,443.

Add ingress rule

Add ingress rule

Click the Add ingress rule button to add the ingress rule to the default security list. By doing this, we're allowing ingress (incoming) traffic from any IP address to ports 80 and 443.

Let's switch back to the virtual machine and run a couple of commands to open the firewall to allow HTTP and HTTPS traffic:

sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload

Serving the static pages using Nginx

We've opened port 80 (and 443) to the instance IP; however, we now need to serve some pages. We'll use Nginx to serve the pages. Let's start by installing it first:

sudo rpm -Uvh
sudo systemctl start nginx.service

To check if everything is running, you can point your browser to the instance IP address, and you should see the "Welcome to nginx!" page.

Welcome to Nginx

Welcome to Nginx

We're almost there! The last thing to set up is to tell Nginx where our Hugo static files are and serve that instead of the stock welcome page.

Let's edit the default.conf file:

sudo vim /etc/nginx/conf.d/default.conf

And replace the contents with these values:

server {
  listen   80;
  location / {
          root   /home/opc/www/public/;
          index  index.html index.htm;

We're telling Nginx to listen on port 80 and where the root location and index HTML files are with this simple server section.

Save the changes and restart Nginx using sudo systemctl restart nginx.

If you open the IP address now, you'll notice the page loads. However, none of the CSS/images are loaded. If you look at the errors in the browsers' console, you'll see how the requests look like:

Request URL: https://[IP]/css/coder.min.d9fddbffe6f27e69985dc5fe0471cdb0e57fbf4775714bc3d847accb08f4a1f6.css

Notice https? It looks like we need to configure SSL and the certs. To do this, you'll need an actual domain name. I'll use one of my many domain names -

To obtain the SSL certificate for and we'll use Let's Encrypts certbot.

Installing Certbot

I ran into a bunch of issues here.

I've used snap to install Certbot, and when trying to run it, I'd get Segmentation fault error. That led me to this issue, which says if you're using snap to install certbot on aarch64 architecture, it's not going to work. One of the mentioned workarounds was to use yum instead.

Installing it with yum (e.g., yum install certbot-nginx) fixed the segmentation fault issue but introduced another one:

[opc@blog-vm ~]$ sudo certbot --nginx -d -d
An unexpected error occurred:
ImportError: cannot import name constants

It turns out installing certbot-nginx installs python2-certbot-nginx package, which for whatever reason, doesn't seem to work either... At this point, I was too frustrated to investigate why the nginx flavor of certbot wasn't working, so I just straight up installed certbot only:

sudo yum install certbot

Finally, some progress! Before continuing here, make sure you go to your domain registrar and create an A record for your domain to point to the IP address of your virtual machine (e.g. A [IP] I've also created a CNAME record to point to (CNAME

At this point, I was ready to run sudo certbot certonly to configure the web server manually.

Certbot has a handy wizard that guides you through the process. When prompted, I picked option #2 - place files in webroot directory. We already have the Nginx server up and running, so there was no need to spin up another temporary server.

I've entered my email, agreed to the Terms of Service, and entered the comma-separated domain names,

After that, I was prompted to enter the webroot for both domains. This is the same webroot where the Hugo site is, so you can enter /home/opc/www/public.

Finally, certbot does its magic, and once it completes, you will see a message like this:


- Congratulations! Your certificate and chain have been saved at:
  Your key file has been saved at:
  Your certificate will expire on 2022-04-30. To obtain a new or
  tweaked version of this certificate in the future, simply run
  certbot again. To non-interactively renew _all_ of your
  certificates, run "certbot renew"
- If you like Certbot, please consider supporting our work by:

Remember the two paths /etc/letsencrypt/live/ to the certs as we'll use them to re-configure Nginx.

A quick note on the certificate renewal

Let's encrypt issues certificates that are valid for three months. Ideally, you should renew the certificates before they expire. Luckily for us, there's a command called certbot renew we can use to automatically (and non-interactively - i.e., you won't get prompted for anything) renew the certs.

I like to run sudo certbot renew --dry-run to make sure everything is good for the renewal, but not renew anything (just do the dry run). If that succeeds, you probably want to set up a cron job that runs the sudo certbot renew command before your certificates expire.

Configure Nginx to use an SSL certificate

With the certificate and key in /etc/letsencrypt/live/ folder, we can edit the /etc/nginx/conf.d/default.conf file and set it up for SSL:

server {
  listen 443 ssl;
  ssl_certificate /etc/letsencrypt/live/;
  ssl_certificate_key /etc/letsencrypt/live/;
  access_log /var/log/nginx/localhost.access.log;

  location / {
    root /home/opc/www/public/;
    index index.html index.htm;


Note: make sure you replace with your domain name.

The difference between the previous and this config is the server name, port, and the pointers to the SSL certificate and the SSL certificate key files.

Let's restart Nginx sudo systemctl restart nginx and go to We should see the Hugo site load correctly with all CSS and images.

Hugo site on OCI VM

Hugo site on OCI VM

We have the complete scenario working now. If you make changes to the Hugo site and the PR is merged, the workflow kicks off. Github workflow builds the static site and copies the files to the VM.

Once the files are updated, the changes are visible right away on the site (since Nginx is just hosting static files).


Thanks for sticking all the way to the end. This was a long post. If you followed everything you should now have a free VM running in OCI and a Github repository with Hugo site.

In the Github repo we've set up a workflow that automatically builds the static site and copies it over to the VM.

One the VM side, we've configured Nginx to serve the static files as well as obtained a free SSL certificate (don't forget to renew it!).

You can check out my Github repo and workflow there.

If you liked this article, give me a follow on Twitter and join over 1000 engineers reading the Learn Cloud Native newsletter.

Peter Jausovec

Peter Jausovec

Peter Jausovec is a platform advocate at He has more than 15 years of experience in the field of software development and tech, in various roles such as QA (test), software engineering and leading tech teams. He's been working in the cloud-native space, focusing on Kubernetes and service meshes, and delivering talks and workshops around the world. He authored and co-authored a couple of books, latest being Cloud Native: Using Containers, Functions, and Data to Build Next-Generation Applications.

Related posts