1424 words
7 minutes
Cool Self Hosting Projects with my VPS

Image Credit: vectorpouch @ freepik

Intro#

If you don’t know, I’ve recently started self-hosting a Hollo fediverse instance on a Virtual Private Server (VPS), which is a whole can of worms.

This VPS has 8GB RAM and 80GB NVME storage, which is more than enough to run Hollo, so I’ve been looking into other tools or software that I can use with the additional capacity that I have leftover, since I’m paying monthly for it anyways. I got super into finding cool tools that make my life easier, and below is what I’ve stuck with. I thought I would share what I’ve installed and how helpful it’s been so far.

I’ve installed all of these as Docker containers with Docker Compose, so it was pretty easy to get started. It was as simple as copying the default or suggested compose.yml file for each software and creating it in the root of each project folder, then running some basic commands. Each software linked has documentation about how to get their docker container up and running, but I also used help from Gemini when troubleshooting the configuration or needed help/explanations on how to do certain steps.

​RSS Aggregator - FreshRSS#

​Technically this was the first project I got up and running on the VPS before I installed Hollo, but I learned a lot getting it working. Especially about how to navigate a server with just the terminal window and SSH.

Although I was using CapyReader (mobile) and Feedbro (desktop) to read RSS feeds from my subscribed blogs and microblogs, it was annoying having two separate sources that didn’t talk to each other. Often times I was marking things read on one device that I already read on the other. It’s fine if you only read on your phone or computer, but I realized this was going to get old quick if I ended up following a lot of feeds. Since I was already deadset on paying for hosting to run Hollo, I figured I would get this up and running as well.

FreshRSS has been really convenient, since you can sign into Capy with your FreshRSS account in order to sync feeds and reading status. I’ve even been following accounts that are only on Twitter like the Lord of Mysteries official account, through the Xcancel RSS feed.

I was using Mastodon as a feed reader with RSS Parrot but this just feels better to use. RSS Parrot doesn’t include media and attachments, which was a huge downside for me, so I’m glad I moved to my own self-hosted FreshRSS instance.

File Manager - Tiny File Manager#

​The next problem I wanted to solve was how to upload blog assets easily to my server, so I could stop using imgur for image hosting. It is technically against imgur TOS to use it as an asset host/CDN for a blog/website, but I wasn’t getting much traffic on my blog to warrant being noticed for this usage. I could have uploaded the images manually to Github where the blog source files are stored (before it’s pushed to Netlify and built using Astro), but that increases the size of your Github repo and can cause build problems down the road. There’s a max size for repos since it’s not meant to be used for file storage.

I ended up with Tiny File Manager because it has an “upload from URL” function, which makes it SUPER easy to copy from official sources for book covers, game banners, etc. It’s also quite user friendly to navigate, and manage files of any type. It also isn’t very resource intensive, so it was a great addition to my server, and made my blogging workflow way easier.

Note Taking / Task Lists - Jotty#

​Since I work between a desktop and laptop computer, I need a place to write my blog post drafts. Since my blog and medialog are both Astro projects hosted on Github, I was actually using Github Projects for this to manage my drafts, since Github issues have a relatively user friendly rich text interface and it outputs markdown which I could copy into the blog post’s markdown file.

Janky, but it worked. But since I have a server, I figure I could use a more purpose built software for it that allowed greater organization.

In comes Jotty! This post was actually drafted in Jotty. I tried the demo on the official website and really liked it, and got to installing. Since by this point, I was fairly used to setting up the dockerfiles and compose files, it only took about 30 minutes, most of which was figuring out my Caddy/internet configuration.

I also appreciate that all the files you create and work with in Jotty are saved as regular markdown and json files, so it’s super easy to backup. I even installed it a web app on my phone, and the interface is quite nice on mobile too!

I’m using Jotty for my blog post drafting workflow, but it can also replace my google keep usage, so I’m looking forward to finding more uses for this tool.

AO3 (Email) to Instapaper - custom#

​This is the project I’m both simultaneously proud of and not proud of. 😂 It’s giving luxury and laziness.

Basically, I read fanfics from AO3 on my Kobo by adding chapter links as articles to my Instapaper account (I really appreciate the formatting and how friendly on the eyes it is, it’s godsend for longfics). I was manually doing this for every new chapter of every fic that I was following. You can imagine how annoying this gets when you have 30+ stories to follow.

I was initially trying to solve this problem by using a combination of “AO3-RSS” which scrapes stories for chapter updates and creates an RSS feed, FreshRSS to combine all story feeds together, and “feeds-to-instapaper” would would submit new feed items from my AO3 subscription feed to instapaper.

I (my server) got blocked by AO3 for my AO3-RSS bot, which… fair enough. AO3 doesn’t need the extra traffic and scraping is a waste of resources anyways. This setback made me realize that all I really needed was a tool that submits chapter links to my instapaper. Since I already receive AO3 subscription emails, maybe something could be done with that…?

Below is a product of my prototyping with Gemini over this, and it requires a Gmail account since Gmail allows separate app-passwords. Don’t worry, I created a separate email account for this (and suggest you do too if you’re going to use this).

I’m calling my little tool “Email to Instapaper” since it checks your inbox for any links that match the configured sources. I’m only using it for AO3 right now, but I imagine this would work for any newsletter that one could sign up for with email that contains a permalink.

Email to Instapaper code#

Dockerfile

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
USER 1000
EXPOSE 5000
CMD ["python", "app.py"]

compose.yml

services:
ao3-bridge:
build: .
container_name: ao3-bridge
restart: always
networks:
- web_traffic
environment:
- PYTHONUNBUFFERED=1
networks:
web_traffic:
external: true

requirements.txt

flask
requests

app.py

import imaplib
import email
import re
import requests
import time
# --- CONFIGURATION ---
GMAIL_USER = "test@gmail.com"
GMAIL_APP_PASS = "test password"
INSTAPAPER_USER = "instapaper_username"
INSTAPAPER_PASS = "instapaper_password"
# --- MODULAR SOURCES ---
# Add new sources here by defining their sender and the regex for the link you want.
SOURCES = {
"AO3": {
"sender": "do-not-reply@archiveofourown.org",
"regex": r'https://archiveofourown\.org/works/\d+/chapters/\d+',
},
# Example for future use:
# "Substack": {
# "sender": "no-reply@substack.com",
# "regex": r'https://[\w-]+\.substack\.com/p/[\w-]+',
# }
}
def send_to_instapaper(url):
"""Handles the actual API call to Instapaper."""
try:
resp = requests.post("https://www.instapaper.com/api/add", data={
'username': INSTAPAPER_USER,
'password': INSTAPAPER_PASS,
'url': url
})
if resp.status_code in [200, 201]:
print(f"Successfully synced: {url}")
return True
else:
print(f"Instapaper API Error ({resp.status_code}): {resp.text}")
return False
except Exception as e:
print(f"Instapaper Connection Error: {e}")
return False
def check_and_sync():
try:
mail = imaplib.IMAP4_SSL("imap.gmail.com")
mail.login(GMAIL_USER, GMAIL_APP_PASS)
mail.select("inbox")
for name, config in SOURCES.items():
# Search specifically for UNSEEN emails from this source's sender
search_query = f'(UNSEEN FROM "{config["sender"]}")'
status, messages = mail.search(None, search_query)
if status == 'OK':
ids = messages[0].split()
if not ids:
continue # Move to next source if nothing found
print(f"Checking {name} ({len(ids)} new emails)...")
for num in ids:
_, data = mail.fetch(num, '(RFC822)')
raw_email = data[0][1].decode('utf-8', errors='ignore')
# Find the link using the specific regex for this source
match = re.search(config["regex"], raw_email)
if match:
url = match.group(0)
print(f"[{name}] Found link: {url}")
send_to_instapaper(url)
# Mark as read regardless of whether a link was found
# (to prevent reprocessing failed regex matches)
mail.store(num, '+FLAGS', '\\Seen')
mail.logout()
except Exception as e:
print(f"Polling Error: {e}")
if __name__ == "__main__":
print("Multi-Source Link Router Started...")
while True:
check_and_sync()
time.sleep(600)

​​

​​

Comments

Comments are fetched from the Fediverse. You can join the conversation by replying to this post on Hollo. New replies will appear here after the next site rebuild. If you don't have a fedi account, you can send me your comment below.

0 Replies 5 Boosts 7 Likes
No comments yet.

Post a Guest Comment