Aside

Self-hosting Miniflux on OpenBSD

Background

Miniflux is my RSS reader, and I wanted to self-host it.

I followed the Tech Independence guide to set up an OpenBSD server, then I studied the shell script + .conf files and referred to this Miniflux server setup on OpenBSD guide to install Miniflux.

I have no idea what I'm doing (this is my first time attempting something like this). Email me suggestions for improvements.

What I did

Every step that has only text styled like this means that I typed/pasted it into PowerShell and pressed enter afterwards. When I say I pasted something in, it means I copied what I needed and right-clicked in PowerShell.

Install Miniflux and PostgreSQL

  1. SSH into server
    • ssh $user@server-ip-address
  2. Download Miniflux and PostgreSQL
    • doas pkg_add postgresql-server postgresql-contrib postgresql-client miniflux
    • Saw warnings about home directories not existing, but ignored (they don't seem to matter)
  3. Enable Miniflux and PostgreSQL
    • doas rcctl enable postgresql miniflux
  4. Create a database, including superuser and password, and start the database server
    • doas su - _postgresql
    • mkdir /var/postgresql/data
    • initdb -D /var/postgresql/data -U postgres -A scram-sha-256 -E UTF8 -W
      • Enter new superuser password: Generated PostgreSQL superuser password and pasted it in
      • Enter it again: Pasted PostgreSQL superuser password again
    • pg_ctl -D /var/postgresql/data -l logfile start
    • exit
  5. Create database, including user and password, for Miniflux
    • createuser -U postgres --pwprompt --no-superuser --createdb --no-createrole miniflux
      • Enter password for new role: Generated new Miniflux database password and pasted it in
      • Enter it again: Pasted Miniflux database password again
      • Password: Prompted for PostgreSQL superuser password, so pasted that in
    • createdb -U miniflux miniflux
      • Password: Prompted for Miniflux database password, so pasted that in
  6. Create the hstore extension, which Miniflux requires
    • doas su - _postgresql
    • psql miniflux postgres
      • Password for user postgres: Prompted for PostgreSQL superuser password, so pasted that in
    • Funky miniflux=# line comes up, and I paste the following in...
      • CREATE EXTENSION hstore;
      • \q
    • exit

I get through all of that relatively unscathed. The password prompts are surprisingly unclear, so I've noted what passwords the prompts are actually asking for (because I kept entering the wrong one).

Update Miniflux config

  1. Update the miniflux.conf config file with database info, wildcard address, and port
    • doas mg /etc/miniflux.conf
      • Update DATABASE_URL password= to Miniflux database password โ€” if there are spaces in the password, wrap password in single quotes like so: password='words with spaces in between'
      • Update LISTEN_ADDR 127.0.0.1:8080 to 127.0.0.1:8081 (since :8080 is already taken)
    • ctrl + x, then ctrl + c (to exit), then y to save changes
  2. Initialize the database
    • doas miniflux -c /etc/miniflux.conf -migrate
  3. Create a Miniflux admin user and password
    • doas miniflux -c /etc/miniflux.conf -create-admin
      • Enter Username: Created a username
      • Enter Password: Generated a password and pasted it in (it wouldn't accept super long passwords, so I repeated this step a few times until I had a short enough password)
  4. Start Miniflux
    • doas rcctl -d start miniflux
  5. Since it's running on localhost (127.0.0.1:8081) and I want to access it from my local computer
    • Open a new PowerShell window
    • ssh -L 8081:127.0.0.1:8081 $user@server-ip-address
    • Open my Firefox browser
    • Go to http://127.0.0.1:8081 and see login page

It works!

Set up subdomain + security certificate

  1. Set up miniflux.example.com subdomain
    • doas mg /etc/relayd.conf
      • Add table <miniflux> { 127.0.0.1 } to the existing list
      • Add pass request quick header "Host" value "miniflux.example.com" forward to <miniflux> to the existing list
      • Add forward to <miniflux> port 8081 to the existing list
    • ctrl + x, then ctrl + c (to exit), then y to save changes
  2. Set up security certificate for miniflux.example.com subdomain
    • doas mg /etc/acme-client.conf
      • Add miniflux.example.com to the existing list
    • ctrl + x, then ctrl + c (to exit), then y to save changes
  3. Update and apply new security certificate
    • doas su
    • domain=yourdomain.com
    • acme-client -v $domain
    • rcctl restart relayd

Use Miniflux

  1. Go to https://miniflux.yourdomain.com and see login page
  2. Log in with Miniflux admin username and password set up during the Miniflux config stage

Update Miniflux config again

  1. Update the miniflux.conf config file with final URL & force cookies to use secure flag
    • doas mg /etc/miniflux.conf
    • ctrl + x, then ctrl + c (to exit), then y to save changes
  2. doas rcctl restart miniflux

Miniflux CSS

Of course, I also customized how Miniflux looks with some CSS. ๐Ÿ™ƒ I added a custom theme by going to Settings > scrolling down > and adding the following CSS into the Custom CSS field.

See how it looks on desktop (screenshot) and mobile (screenshot).

My adjustments include:

:root {  
  /* Font sizes */
  --step--1: clamp(0.8333rem, 0.8238rem + 0.0476vw, 0.9rem);
  --step-0: clamp(1rem, 0.9821rem + 0.0893vw, 1.125rem);
  
  /* Light mode */
  --body-background: #eff1f5;
  --body-color: #4c4f69;
  --subtext-color: #6c6f85;
  --link-color: #8839ef;
  --link-visited-color: #c71f9a;
  --surface: #acb0be;
  --mantle: #e6e9ef;
  --crust: #dce0e8;
  
  /* Miniflux variables */  
  --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
  --entry-content-font-family: var(--font-family);
  --entry-content-quote-font-family: var(--font-family);
  --entry-content-font-weight: normal;
  --item-title-link-font-weight: normal;
  
  --title-color: var(--body-color);
  --link-focus-color: var(--link-color);
  --link-hover-color: var(--link-color);
  --header-list-border-color: var(--subtext-color);
  --header-link-color: var(--link-color);
  --header-link-focus-color: var(--link-color);
  --header-link-hover-color: var(--link-color);
  --header-active-link-color: var(--body-color);
  --page-header-title-border-color: var(--subtext-color);
  
  --logo-color: var(--body-color);
  --logo-hover-color-span: var(--body-color);
  
  --pagination-link-color: var(--link-color);
  --pagination-border-color: var(--subtext-color);
  --hr-border-color: var(--subtext-color);
  
  --entry-header-border-color: var(--subtext-color);
  --entry-header-title-link-color: var(--link-color);
  
  --entry-content-color: var(--body-color);
  --entry-content-code-color: var(--body-color);
  --entry-content-code-background: var(--mantle);
  --entry-content-code-border-color: var(--crust);
  --entry-content-quote-color: var(--body-color);
  
  --table-border-color: var(--subtext-color);
  --table-th-background: var(--body-background);
  --table-th-color: var(--body-background);
  --table-tr-hover-background-color: var(--body-background);
  --table-tr-hover-color: var(--body-color);
  
  --button-primary-border-color: var(--link-color);
  --button-primary-background: var(--link-color);
  --button-primary-color: var(--body-background);
  --button-primary-focus-border-color: var(--link-color);
  --button-primary-focus-background: var(--link-color);
  
  --category-color: var(--body-color);
  --category-background-color: var(--body-background);
  --category-border-color: var(--link-color);
  --category-link-color: var(--link-color);
  --category-link-hover-color: var(--link-color);
  
  --item-padding: 1em;
  --item-border-color: var(--subtext-color);
  --item-status-read-title-link-color: var(--link-color);
  --item-status-read-title-focus-color: var(--link-color);
  --item-meta-focus-color: var(--body-color);
  --item-meta-li-color: var(--surface);
  --current-item-border-color: var(--subtext-color);
  
  --input-border: 1px solid var(--subtext-color);
  --input-background: var(--body-background);
  --input-color: var(--body-color);
  --input-placeholder-color: var(--body-color);
  --input-focus-color: var(--surface);
  --input-focus-border-color: var(--mantle);
  
  --alert-color: var(--body-color);
  --alert-success-color: var(--body-color);
  --alert-error-color: var(--body-color);
  --alert-info-color: var(--body-color);
  --panel-color: var(--body-color);
  --modal-color: var(--body-color);
  --parsing-error-color: var(--body-color);

  --alert-border-color: var(--crust);
  --alert-success-border-color: var(--crust);
  --alert-error-border-color: var(--crust);
  --alert-info-border-color: var(--crust);
  --panel-border-color: var(--crust);
  --entry-content-abbr-border-color: var(--crust);
  --feed-parsing-error-border-color: var(--crust);
  --feed-has-unread-border-color: var(--crust);
  --category-has-unread-border-color: var(--crust);

  --alert-background-color: var(--mantle);
  --alert-success-background-color: var(--mantle);
  --alert-error-background-color: var(--mantle);
  --alert-info-background-color: var(--mantle);
  --panel-background: var(--mantle);
  --modal-background: var(--mantle);
  --entry-enclosure-border-color: var(--mantle);
  --feed-parsing-error-background-color: var(--mantle);
  --feed-has-unread-background-color: var(--mantle);
  --category-has-unread-background-color: var(--mantle);
  
  --keyboard-shortcuts-li-color: var(--body-color);
  --counter-color: var(--subtext-color);
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
  :root {
    --body-background: #1e1e2e;
    --body-color: #cdd6f4;
    --subtext-color: #a6adc8;
    --link-color: #cba6f7;
    --link-visited-color: #f5c2e7;
    --surface: #585b70;
    --mantle: #181825;
    --crust: #11111b;
  }
  img {
    filter: brightness(0.8) contrast(1.2);
  }
}
/* Page styles */
html {
  margin-left: calc(100vw - 100%);
}
@media (max-width: 768px) {
  html {
    margin-left: auto;
  }
}
body {
  padding: 0 1em;
  max-width: 80ch;
}
/* Header */
.header li {
  list-style-type: none;
}
.header nav {
  gap: 1em;
}
@media (min-width: 620px) and (max-width: 830px) {
  .logo {
    display: initial;
  }
}
.logo {
  font-size: 0;
  padding-right: 0;
}
.logo::before {
  content: "/แ  - ห• -ใƒž โœฟหšโ‚Šยท";
  font-size: 1rem;
  margin-left: .25em;
}
section nav ul {
  display: flex;
  flex-direction: row;
  gap: 1em;
  flex-wrap: wrap;
}
.page-header li, .page-footer li {
  padding-right: 0;
}
/* Headings */
h1,
h2,
h3 {
  margin: 1em auto;
}
.entry header h1 a:hover,
.entry header h1 a:focus {
  color: var(--link-color);
}
.page-header h1 {
  border-bottom: 0;
  font-weight: bold;
}
/* Post list */
.item-title {
  font-size: var(--step-0);
}
.item-meta a,
.item-meta-icons li > :is(a, button) {
  color: var(--body-color);
}
.pagination,
.item-meta-info {
  font-size: var(--step--1);
  color: var(--subtext-color);
}
/* Post content */
.entry-content {
  line-height: 1.6;
  font-size: var(--step-0);
}
.entry-content pre {
  white-space: break-spaces;
  margin: 1em 0;
  padding: 1em;
  font-size: var(--step--1);
}
.entry-content blockquote {
  border-left: 1px dotted var(--link-color);
  padding: 0 1em;
  margin: 1.25em;
  line-height: 1.6;
}
.entry-content img {
  line-height: initial;
  margin: 1em 0;
}
.entry header {
  padding-bottom: 2em;
}
/* Post meta */
.entry-date,
.entry-meta,
.entry-tags,
.entry-meta,
.entry-website a {
  font-size: var(--step--1);
  color: var(--body-color);
}
.entry-website a {
  color: var(--link-color);
}
/* Links */
a,
.entry header h1 a,
.item-title a,
.entry-website a {
  text-decoration-line: underline;
  text-decoration-style: dotted;
  text-decoration-thickness: 1px;
}
a:hover,
.entry header h1 a:hover {
  text-decoration-line: underline;
  text-decoration-style: solid;
}
.item-meta-icons a span {
  text-decoration: none;
}
.header li a:hover {
  color: var(--header-link-hover-color);
}
/* UI stuff */
svg.icon {
  display: none;
}
:is(.page-button) {
  border: 1px dotted var(--link-color);
  padding: .5em .75em;
  font-size: var(--step--1);
}
:is(.page-button):hover {
  border-style: solid;
}
.disabled {
  opacity: 50%;
}
span.category {
  display: none;
}
.page-link {
  font-size: var(--step--1);
}
.item-meta-icons-remove {
  position: relative;
  top: .5px;
}
.button {
  font-size: .875rem;
  padding: .5em 1.25em;
}
/* Hide stuff */
.entry-actions,
.entry-website img,
.item-title img,
li.item-meta-icons-star,
li.item-meta-icons-external-url,
li.item-meta-icons-comments,
#header-menu li:nth-child(2),
#header-menu li:nth-child(6),
span.category {
  display: none;
}