Host a SvelteKit Site and Blog on OCI Always Free

Cloud Provider: oci • Difficulty: intermediate Estimated Time: 90-150 minutes

What you’ll build

A production-ready SvelteKit site running on an OCI Always Free VM, fronted by Nginx, secured with a free Let’s Encrypt certificate that renews automatically.

You will also add Giscus comments so readers can comment with GitHub accounts instead of managing accounts on your site.

Why this recipe matters

This is the actual pattern you can use to host your own personal site or technical blog for effectively zero dollars per month using OCI Always Free resources.

It is also the exact pattern you can use to launch Free Tier Life itself.

What you need before you start

Accounts and access

  • An OCI tenancy with Always Free resources available
  • A domain name you control
  • A GitHub repository for your site code
  • A GitHub account that can enable Discussions

Local software

  • Node.js 20 or newer
  • Git
  • SSH key pair
  • A working local copy of your SvelteKit site

Assumptions

This recipe assumes:

  • your SvelteKit site already runs locally
  • you are using the Node adapter for production
  • you want to host on a single OCI VM
  • you are fine managing the server directly instead of containerizing on day one

High-level architecture

Internet
  |
  | DNS A records
  v
OCI Public IP
  |
  v
Nginx on ports 80 and 443
  |
  v
SvelteKit Node app on localhost port 3000
  |
  v
Giscus comments using GitHub Discussions

Step 1 - Confirm the SvelteKit site works locally

Before touching OCI, make sure the site builds and runs locally.

From your project root:

npm install
npm run build
npm run preview

Open the preview URL and confirm:

  • the homepage loads
  • the recipe listing page loads
  • the blog routes load
  • static assets resolve correctly

If the local site is broken, fix that first. Do not move a broken build into the cloud.

Step 2 - Confirm production adapter configuration

For production on a VM, use the Node adapter in svelte.config.js.

Example:

import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	preprocess: [vitePreprocess()],
	kit: {
		adapter: adapter()
	}
};

export default config;

Then rebuild:

npm run build

This should produce a build directory that can be started with Node.

Step 3 - Commit your latest working code to GitHub

If the site is working locally, commit it before deploying.

git add .
git commit -m "Add OCI hosting recipe and latest site updates"
git push

Step 4 - Create the OCI VM

In OCI Console, create an instance.

Recommended starting point:

  • Shape: VM.Standard.A1.Flex if available
  • CPU and RAM: 1 OCPU and 6 GB RAM
  • OS: Ubuntu 24.04 LTS
  • Public IP: yes
  • Boot volume: default is fine
  • SSH key: upload your public key

If Ampere is not available in your tenancy or region, use an Always Free AMD micro shape instead.

Step 5 - Open required network ports

You need these ports open:

  • 22/tcp for SSH
  • 80/tcp for HTTP
  • 443/tcp for HTTPS

In OCI

Update your Security List or Network Security Group to allow inbound traffic on:

  • 22
  • 80
  • 443

On the VM later

You will also allow Nginx traffic through Ubuntu firewall.

Step 6 - SSH into the VM

ssh -i ~/.ssh/your_private_key ubuntu@YOUR_PUBLIC_IP

If your VM uses a different default user, adjust accordingly.

Step 7 - Update the server and install dependencies

Once connected:

sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx git curl ufw
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

Confirm versions:

node -v
npm -v
nginx -v

Step 8 - Configure the firewall

Allow SSH, HTTP, and HTTPS:

sudo ufw allow OpenSSH
sudo ufw allow "Nginx Full"
sudo ufw --force enable
sudo ufw status

Step 9 - Create a deployment directory

sudo mkdir -p /var/www
sudo chown -R $USER:$USER /var/www
cd /var/www

Step 10 - Clone the site repo

git clone https://github.com/YOUR_GITHUB_USER/freetierlife.git
cd freetierlife

Install dependencies and build:

npm ci
npm run build

Step 11 - Create the production environment file

Create a file named .env in the project root with this content:

HOST=127.0.0.1
PORT=3000
ORIGIN=https://www.freetierlife.com
NODE_ENV=production

If you are launching without www, change ORIGIN accordingly.

Step 12 - Test the built app manually

Before wiring systemd, make sure the app starts.

node build

If it starts successfully, open another SSH tab and test:

curl http://127.0.0.1:3000

If that works, stop it with Ctrl+C.

Step 13 - Create the systemd service

Create this file:

/etc/systemd/system/freetierlife.service

Paste this content into it:

[Unit]
Description=Free Tier Life SvelteKit app
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/var/www/freetierlife
EnvironmentFile=/var/www/freetierlife/.env
ExecStart=/usr/bin/node build
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

If your Linux username is not ubuntu, replace it in the service file.

Reload systemd and start the app:

sudo systemctl daemon-reload
sudo systemctl enable freetierlife
sudo systemctl start freetierlife
sudo systemctl status freetierlife

If needed, inspect logs:

journalctl -u freetierlife -f

Step 14 - Configure Nginx as a reverse proxy

Create this file:

/etc/nginx/sites-available/freetierlife

Paste this content into it:

server {
    listen 80;
    server_name freetierlife.com www.freetierlife.com;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Enable it:

sudo ln -s /etc/nginx/sites-available/freetierlife /etc/nginx/sites-enabled/freetierlife
sudo nginx -t
sudo systemctl reload nginx

Step 15 - Point DNS to OCI

At your DNS provider, create these records:

Type: A
Name: @
Value: YOUR_OCI_PUBLIC_IP
TTL: 300

Type: A
Name: www
Value: YOUR_OCI_PUBLIC_IP
TTL: 300

Step 16 - Install Let’s Encrypt and Certbot

Install Certbot and the Nginx plugin:

sudo apt install -y certbot python3-certbot-nginx

Request certificates:

sudo certbot --nginx -d freetierlife.com -d www.freetierlife.com

Step 17 - Verify certificate renewal

systemctl list-timers | grep certbot
sudo certbot renew --dry-run

Step 18 - Verify the live site

Validate:

  • https://freetierlife.com
  • https://www.freetierlife.com

Step 19 - Set up Giscus comments

Giscus is a comment system built on top of GitHub Discussions.

Instead of building your own comment database, moderation workflow, user accounts, password reset flows, and spam management, you can let GitHub handle identity and store comments as discussions in a GitHub repository.

That makes Giscus a strong fit for a technical site like Free Tier Life because:

  • your audience is likely comfortable with GitHub
  • you do not need to build or host a custom comment system
  • comment storage lives outside your app server
  • moderation can happen in GitHub
  • you can keep the site lightweight while still allowing discussion

How Giscus works

When a reader loads a page that includes the Giscus widget:

  • the page loads the Giscus client script from https://giscus.app/client.js
  • Giscus checks the repository and discussion category you configured
  • Giscus maps the current page to a discussion thread
  • users comment by authenticating with GitHub

The most common mapping for a blog or recipe site is:

  • pathname

That means each page path becomes its own discussion thread.

For example:

  • /recipes/oci-sveltekit-blog-ssl-giscus
  • /blog/first-post

Each path can map to a separate discussion automatically.

Step 19.1 - Choose the GitHub repository for comments

You have two main options:

Option A - Use the main site repository

Use your main freetierlife repository for comments.

This is the simplest option because:

  • everything stays in one place
  • setup is quicker
  • easier to manage when the site is still small

Option B - Use a dedicated public repo just for comments

Create a separate public repository for discussions and comments.

This can make sense if you want to:

  • separate site code from discussion traffic
  • keep comment administration isolated
  • use one comments repo for multiple sites later

For Free Tier Life, Option A is completely reasonable.

If you are undecided, use the main repo and keep moving.

Step 19.2 - Make sure the repository is public

Giscus requires a public GitHub repository.

If the repository is private, Giscus will not work the way you expect for a public site.

Double-check this first before doing anything else.

Step 19.3 - Enable GitHub Discussions

In GitHub:

  1. Open the repository you chose for comments.
  2. Go to Settings.
  3. Under the repository settings, find the Features or General section where Discussions can be enabled.
  4. Turn on Discussions.

Once enabled, GitHub will initialize discussion support for that repository.

Step 19.4 - Create or confirm a discussion category

Giscus needs a discussion category to store comment threads.

A simple approach is to create or use a category named:

  • Comments

If you already have another category that makes more sense, that is fine too. The important part is that the category exists and is available for Giscus to use.

Step 19.5 - Install the Giscus GitHub App

Go to:

  • https://giscus.app

You will be guided to install the Giscus GitHub App for the repository you selected.

Make sure the app has access to the correct repository.

If you are using the main freetierlife repository, verify that repository is included in the installation scope.

Step 19.6 - Generate the configuration values

Still at https://giscus.app, fill in the setup form.

Use values similar to these:

  • Repository: your selected public repo
  • Category: Comments (or your chosen category)
  • Mapping: pathname
  • Strict: 0
  • Reactions enabled: 1 if you want GitHub reactions
  • Emit metadata: 0
  • Input position: top
  • Theme: preferred_color_scheme
  • Language: en

As you complete the form, Giscus will generate a script configuration.

You are not going to paste that raw script directly into your markdown-driven site. Instead, you are going to copy the important values out of it and use them in the Svelte component in Step 20.

Step 19.7 - Capture the values you will need in Step 20

From the generated Giscus configuration, copy and save these values:

  • data-repo
  • data-repo-id
  • data-category
  • data-category-id
  • data-mapping
  • data-theme
  • data-lang

At minimum, you need:

  • repository name
  • repository ID
  • category name
  • category ID

You will replace the placeholders in Step 20 with those real values.

For Free Tier Life, this is a sensible starting configuration:

  • repository: your public freetierlife repo
  • category: Comments
  • mapping: pathname
  • theme: preferred_color_scheme
  • language: en

Why pathname?

Because it keeps the setup simple and maps naturally to your content routes:

  • recipes map by recipe path
  • blog posts map by blog post path

That is exactly what you want for a route-based site.

Step 19.9 - Sanity-check the GitHub side before moving on

Before you touch Step 20, confirm the following:

  • the selected repository is public
  • Discussions are enabled
  • the Giscus GitHub App is installed for that repository
  • the category exists
  • you have copied the repository and category IDs correctly

If any of those are wrong, the widget in Step 20 may render incorrectly or fail silently.

Step 19.10 - What you should have at the end of this step

By the end of Step 19, you should have:

  • a public GitHub repository for comments
  • Discussions enabled for that repository
  • a valid discussion category such as Comments
  • the Giscus GitHub App installed
  • the real repository and category values copied and ready for Step 20

Once you have those values, move on to Step 20 and wire the widget into the site.

Step 20 - Add a reusable Giscus component in SvelteKit

Create this file:

src/lib/components/Giscus.svelte

Paste this script content into a script tage in the component file:

  import { onMount } from 'svelte';

  onMount(() => {
  const existing = document.querySelector('script[data-giscus]');
  if (existing) return;

      const script = document.createElement('script');
      script.src = 'https://giscus.app/client.js';
      script.setAttribute('data-giscus', 'true');
      script.setAttribute('data-repo', 'YOUR_USER/YOUR_REPO');
      script.setAttribute('data-repo-id', 'YOUR_REPO_ID');
      script.setAttribute('data-category', 'Comments');
      script.setAttribute('data-category-id', 'YOUR_CATEGORY_ID');
      script.setAttribute('data-mapping', 'pathname');
      script.setAttribute('data-strict', '0');
      script.setAttribute('data-reactions-enabled', '1');
      script.setAttribute('data-emit-metadata', '0');
      script.setAttribute('data-input-position', 'top');
      script.setAttribute('data-theme', 'preferred_color_scheme');
      script.setAttribute('data-lang', 'en');
      script.crossOrigin = 'anonymous';
      script.async = true;

      const container = document.getElementById('giscus-container');
      if (container) {
        container.innerHTML = '';
        container.appendChild(script);
      }

  });

Also include this markup in the component, below the script tag:

Why this section is formatted this way

This recipe intentionally does not use a svelte-labeled code fence here.

Instead, the logic and markup are shown as generic code and text blocks so mdsvex does not try to treat the example like executable Svelte component content during preprocessing.

Step 21 - Add Giscus comments to your site templates

Now that the Giscus component exists and contains the correct repository and category configuration, add it to your site layouts.

The goal is to place a discussion thread at the bottom of:

  • individual recipe pages
  • individual blog post pages

Step 21.1 - Import the component

In the recipe detail page component and the blog post detail page component, import the Giscus component from:

$lib/components/Giscus.svelte

Step 21.2 - Render the component after the main content

Place the Giscus component near the bottom of the page, after the primary content area.

The intended layout is:

  • main article or recipe content first
  • comments section below it

This keeps the reading experience clean and makes it obvious that comments belong to that page.

Step 21.3 - Do not duplicate the container

The Giscus component already manages its own internal container for script injection and rendering.

Do not add a second Giscus container element directly into the page template.

Step 21.4 - Rebuild and restart the app

After wiring the component into the recipe and blog detail templates:

npm run build
sudo systemctl restart freetierlife

Step 21.5 - Validate the integration

Open a recipe page or blog post page and confirm:

  • the comments section appears below the content
  • the GitHub sign-in prompt appears
  • the browser console shows no Giscus-related errors
  • discussion threads are created and mapped correctly by pathname

Step 21.6 - Common issues

If the comments section does not appear:

  • the repository may not be public
  • Discussions may not be enabled
  • the Giscus app may not be installed correctly
  • the repository ID or category ID may be wrong
  • the component may not have been imported into the correct page template

Step 21.7 - What you should have at the end of this step

By the end of Step 21:

  • recipe pages support comments
  • blog post pages support comments
  • comments are stored in GitHub Discussions
  • no separate comment backend is required

Step 22 - Final validation checklist

  • homepage loads
  • recipe pages load
  • blog pages load
  • navigation works
  • sudo systemctl status freetierlife is healthy
  • sudo systemctl status nginx is healthy
  • browser shows valid certificate
  • sudo certbot renew --dry-run succeeds
  • Giscus iframe renders on a recipe or blog post

Troubleshooting

If the SvelteKit app fails to start

journalctl -u freetierlife -f

Common causes:

  • missing .env
  • wrong ORIGIN
  • app not built
  • wrong working directory in systemd service

If Nginx fails

sudo nginx -t

If Certbot fails

Common causes:

  • DNS not propagated yet
  • port 80 blocked
  • wrong server_name
  • Cloudflare proxy enabled too early

If Giscus does not appear

Check:

  • repo is public
  • Discussions are enabled
  • Giscus app is installed
  • repo or category IDs are correct

Cost notes

This setup can remain at zero dollars per month if you stay strictly inside OCI Always Free limits and avoid accidentally provisioning paid resources.

What to do next

  1. publish this recipe on FTL
  2. use it to host the rest of the site
  3. write Recipe 2 about migrating the same site into Podman later
  4. tell the world you built and hosted your own thought-leadership platform for free

Source links