Host a SvelteKit Site and Blog on OCI Always Free
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.Flexif 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/tcpfor SSH80/tcpfor HTTP443/tcpfor 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.comhttps://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:
- Open the repository you chose for comments.
- Go to
Settings. - Under the repository settings, find the
FeaturesorGeneralsection where Discussions can be enabled. - 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:
1if 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-repodata-repo-iddata-categorydata-category-iddata-mappingdata-themedata-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.
Step 19.8 - Recommended configuration for Free Tier Life
For Free Tier Life, this is a sensible starting configuration:
- repository: your public
freetierliferepo - 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 freetierlifeis healthysudo systemctl status nginxis healthy- browser shows valid certificate
sudo certbot renew --dry-runsucceeds- 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
- publish this recipe on FTL
- use it to host the rest of the site
- write Recipe 2 about migrating the same site into Podman later
- tell the world you built and hosted your own thought-leadership platform for free
Source links
- OCI Always Free resources: https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm
- OCI Object Storage overview: https://docs.oracle.com/en-us/iaas/Content/Object/Concepts/objectstorageoverview.htm
- Certbot and Let’s Encrypt with Nginx on Ubuntu: https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-20-04
- Certbot auto-renewal notes: https://oneuptime.com/blog/post/2026-03-02-set-up-automatic-certificate-renewal-certbot-ubuntu/view
- Giscus: https://giscus.app
- Giscus GitHub repo: https://github.com/giscus/giscus