# Technical Documentation: stringer-rss/stringer

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/stringer-rss/stringer)

### Overview (`README.md`)

# Stringer

[](https://circleci.com/gh/stringer-rss/stringer/tree/main)
[](https://codeclimate.com/github/stringer-rss/stringer/maintainability)
[](https://coveralls.io/github/stringer-rss/stringer?branch=main)
[](https://github.com/sponsors/mockdeep)

### A self-hosted, anti-social RSS reader.

Stringer has no external dependencies, no social recommendations/sharing, and no fancy machine learning algorithms.

But it does have keyboard shortcuts and was made with love!





## Installation

Stringer is a Ruby app based on Rails, PostgreSQL, Hotwire (Turbo and Stimulus) and GoodJob.

[](https://heroku.com/deploy?template=https://github.com/stringer-rss/stringer)

Stringer will run just fine on the Eco/Basic Heroku plans.

Instructions are provided for deploying to [Heroku manually](/docs/Heroku.md), to any Ruby 
compatible [Linux-based VPS](/docs/VPS.md), to [Docker](docs/Docker.md) and to [OpenShift](/docs/OpenShift.md).

## Niceties

### Keyboard Shortcuts

You can access the keyboard shortcuts when using the app by hitting `?`.



### Using your own domain with Heroku

You can run Stringer at `http://reader.yourdomain.com` using a CNAME.

If you are on Heroku:

```
heroku domains:add reader.yourdomain.com
```

Go to your registrar and add a CNAME:
```
Record: CNAME
Name: reader
Target: your-heroku-instance.herokuapp.com
```

Wait a few minutes for changes to propagate.

### Fever API

Stringer implements a clone of [Fever's API](http://www.feedafever.com/api) so it can be used with any mobile client that supports Fever.



Use the following settings:

```
Server: {path-to-stringer}/fever (e.g. http://reader.example.com/fever)

Email: stringer (case-sensitive)
Password: {your-stringer-password}
```

### Translations

Stringer has been translated to [several other languages](config/locales). Your language can be set with the `LOCALE` environment variable.

To set your locale on Heroku, run `heroku config:set LOCALE=en`.

If you would like to translate Stringer to your preferred language, please use [LocaleApp](http://www.localeapp.com/projects/4637).

### Clean up old read stories on Heroku

You can clean up old stories by running: `rake cleanup_old_stories`

By default, this removes read stories that are more than 30 days old (that
are not starred). You can either run this manually or add it as a scheduled
task.

## Development

### Getting Started

You'll need the toolchain versions pinned in `.tool-versions` (Ruby, Node,
PostgreSQL, and pnpm). With those installed, run:

```sh
bin/setup
```

This installs dependencies, prepares the database, and launches the development
server. The application will be running on port `3000`.

On subsequent runs you can start the server directly with `bin/dev`, which boots
Puma alongside the esbuild watchers for JavaScript and CSS.

You can launch an interactive console (a la `rails c`) using `rake console`.

### Tests and linting

Run the Ruby tests with `bundle exec rspec`. Run a single file or example with
`bundle exec rspec spec/path/to/file_spec.rb:LINE`.

Run the JavaScript tests with `pnpm test` (Vitest). This also type-checks and
runs ESLint first.

Other checks: `bundle exec rubocop` (Ruby lint), `pnpm eslint` (JS lint),
`pnpm stylelint` (CSS lint), `pnpm tscheck` (type check), and
`bundle exec brakeman` (security scan).

## Acknowledgments

Most of the heavy-lifting is done by [`feedjira`](https://github.com/feedjira/feedjira) and [`feedbag`](https://github.com/dwillis/feedbag).

General sexiness courtesy of [`Twitter Bootstrap`](http://twitter.github.io/bootstrap/) and [`Flat UI`](http://designmodo.github.io/Flat-UI/).

ReenieBeanie Font Copyright &copy; 2010 Typeco (james@typeco.com). Licensed under [SIL Open Font License, 1.1](http://scripts.sil.org/OFL).

Lato Font Copyright &copy; 2010-2011 by tyPoland Lukasz Dziedzic (team@latofonts.com). Licensed under [SIL Open Font License, 1.1](http://scripts.sil.org/OFL).

## Contact

If you have a question, feature idea, or are running into problems, our preferred method of contact is to open an issue on GitHub. This allows multiple people to weigh in, and we can keep everything in one place. Thanks!

## Maintainers

Robert Fletcher [boon.gl](https://boon.gl)

## Alumni

Matt Swanson (creator), [mdswanson.com](http://mdswanson.com), [@_swanson](http://twitter.com/_swanson)
Victor Koronen, [victor.koronen.se](http://victor.koronen.se/), [@victorkoronen](https://twitter.com/victorkoronen)

---

### Docker (`docs/Docker.md`)

# Stringer on Docker

## Production ready setup using docker-compose

Create a local environment file named `.env`, e.g. via `touch .env`.

Download [docker-compose.yml](../docker-compose.yml) and run:

```sh
touch .env && docker compose up -d
```

Give it a second and visit `localhost`!

## Production ready manual setup

The following steps can be used to setup Stringer on Docker, using a Postgres database also running on Docker.

1. Setup a Docker network so the two containers we're going to create can communicate:

```Sh
docker network create --driver bridge stringer-network
```

2. Setup a Postgres Docker container:

```Sh
docker run --detach \
    --name stringer-postgres \
    --restart always \
    --volume /srv/stringer/data:/var/lib/postgresql \
    --net stringer-network \
    -e POSTGRES_PASSWORD=myPassword \
    -e POSTGRES_DB=stringer \
    postgres:18-alpine
```

3. Run the Stringer Docker image:

```sh
docker run --detach \
    --name stringer \
    --net stringer-network \
    --restart always \
    -e PORT=8080 \
    -e DATABASE_URL=postgres://postgres:myPassword@stringer-postgres/stringer \
    -e SECRET_KEY_BASE=$(openssl rand -hex 64) \
    -e ENCRYPTION_PRIMARY_KEY=$(openssl rand -hex 64) \
    -e ENCRYPTION_DETERMINISTIC_KEY=$(openssl rand -hex 64) \
    -e ENCRYPTION_KEY_DERIVATION_SALT=$(openssl rand -hex 64) \
    -e FETCH_FEEDS_CRON="*/5 * * * *" \ # optional
    -e CLEANUP_CRON="0 0 * * *" \ # optional
    -p 127.0.0.1:8080:8080 \
    stringerrss/stringer:latest
```

That's it! You now have a fully working Stringer instance up and running!

## Upgrading Postgres

The setup above moved from Postgres 16 to Postgres 18. Two things changed:

- A Postgres major version cannot read data files written by an older one.
- The official 18+ image expects a single mount at `/var/lib/postgresql` (not `/var/lib/postgresql/data`) and stores the database in a version-named subdirectory inside it.

If you start the 18 image against an existing 16 data directory it refuses to start and prints an explanation, so nothing is lost. But you do need to migrate by hand. Stringer databases are small, so the simplest route is to dump and restore.

1. With the old container still running, dump everything:

```sh
docker compose exec stringer-postgres sh -c 'pg_dumpall -U "$POSTGRES_USER"' > stringer-pg16.sql
```

2. Stop the stack and move the old data directory aside:

```sh
docker compose down
sudo mv /srv/stringer/data /srv/stringer/data-pg16
```

3. Update `docker-compose.yml` (or re-download it): change the image to `postgres:18-alpine` and the volume to `/srv/stringer/data:/var/lib/postgresql`.

4. Start only the database and wait for it to accept connections:

```sh
docker compose up -d stringer-postgres
until docker compose exec stringer-postgres pg_isready -h localhost; do sleep 1; done
```

5. Restore the dump. Errors saying a role or database "already exists" are expected and harmless, because the fresh container already created them:

```sh
docker compose exec -T stringer-postgres sh -c 'psql -U "$POSTGRES_USER" -d postgres' < stringer-pg16.sql
```

6. Start the rest of the stack and check that your feeds and stories are there:

```sh
docker compose up -d
```

7. Once you are happy, delete `/srv/stringer/data-pg16` and `stringer-pg16.sql`.

If you used the manual setup instead of docker-compose, run the same commands with `docker exec stringer-postgres` in place of `docker compose exec stringer-postgres`, and recreate the Postgres container with the new image and volume path in step 3.

For production use it's recommended to put a reverse proxy in front of Stringer.

Caddy (https://caddyserver.com/):

```
stringer.example.org {
	proxy / localhost:8080 {
      transparent
	}
}
```

Nginx (https://nginx.org/):

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

### Heroku (`docs/Heroku.md`)

```sh
git clone git@github.com:stringer-rss/stringer.git
cd stringer
heroku create

heroku config:set SECRET_KEY_BASE=`openssl rand -hex 64`
heroku config:set ENCRYPTION_PRIMARY_KEY=`openssl rand -hex 64`
heroku config:set ENCRYPTION_DETERMINISTIC_KEY=`openssl rand -hex 64`
heroku config:set ENCRYPTION_KEY_DERIVATION_SALT=`openssl rand -hex 64`

git push heroku main

heroku config:set APP_URL=`heroku apps:info --shell | grep web_url | cut -d= -f2`

heroku run rake db:migrate
heroku restart

heroku addons:create scheduler
heroku addons:open scheduler
```

Add an hourly task that runs `rake lazy_fetch` (if you are not on Heroku you may want `rake fetch_feeds` instead).

Load the app and follow the instructions to import your feeds and start using the app.

See the "Niceties" section of the README for a few more tips and tricks for getting the most out of Stringer on Heroku.

## Updating the app

From the app's directory:

```sh
git pull
git push heroku main
heroku run rake db:migrate
heroku restart
```

---

### OpenShift (`docs/OpenShift.md`)

Stringer on OpenShift
========================

Deploying into OpenShift
------------------------

1. Creating new OpenShift Ruby 2.0 application with the Postgresql cartridge (command-line).

 ```sh
    rhc app create feeds ruby-2.0 postgresql-9.2
 ```

2. Pull the code from the Stringer Github repository.

 ```sh
	cd feeds
	git remote add upstream git@github.com:stringer-rss/stringer.git
	git pull -s recursive -X theirs upstream main
 ```

3. To enable migrations for the application, a new action_hook is required. Add the file, .openshift/action_hooks/deploy,  with the below 3 lines into it.

 ```
	pushd ${OPENSHIFT_REPO_DIR} > /dev/null
	bundle exec rake db:migrate RAILS_ENV="production"
	popd > /dev/null
 ```

4. Make sure that the file created above is executable on Unix-like systems.

 ```sh
    chmod +x .openshift/action_hooks/deploy
 ```

5. Set the environment variables by generating them with the commands below.

 ```sh
    rhc env set SECRET_KEY_BASE="`openssl rand -hex 64`"
    rhc env set ENCRYPTION_PRIMARY_KEY="`openssl rand -hex 64`"
    rhc env set ENCRYPTION_DETERMINISTIC_KEY="`openssl rand -hex 64`"
    rhc env set ENCRYPTION_KEY_DERIVATION_SALT="`openssl rand -hex 64`"
 ```

6. Configuration of the database server is next. Open the file config/database.yml and add in the configuration for Production as shown below. OpenShift is able to use environment variables to push the information into the application.

 ```
	production:
		adapter: postgresql
		database: <%= ENV["OPENSHIFT_APP_NAME"] %>
		host: <%= ENV["OPENSHIFT_POSTGRESQL_DB_HOST"] %>
		port: <%= ENV["OPENSHIFT_POSTGRESQL_DB_PORT"] %>
		username: <%= ENV["OPENSHIFT_POSTGRESQL_DB_USERNAME"] %>
		password: <%= ENV["OPENSHIFT_POSTGRESQL_DB_PASSWORD"] %> 
 ```

7. Due to an older version of bundler being used in OpenShift (1.3.5), some changes need to be made in the Gemfile. 

	Remove the Ruby version specification from the Gemfile below (error reporting wrong Ruby version when deploying to OpenShift). 

 ```
	ruby '2.0.0'
 ```

	Then change the two gem dependencies below to use the hash rocket syntax for the "require" option.

 ```
	gem "coveralls", "~> 0.7", require: false
	gem "rubocop", "~> 0.35.1", require: false
	gem "sinatra-assetpack", "~> 0.3.1", require: "sinatra/assetpack"
 ```
	to
 ```
	gem "coveralls", "~> 0.7", :require => false
	gem "rubocop", "~> 0.35.1", :require => false
	gem "sinatra-assetpack", "~> 0.3.1", :require => "sinatra/assetpack"
 ```

8. Finally, once completed, all changes should be committed and pushed to OpenShift. Note that it might take a while when pushing to OpenShift.

 ```sh
	git add .
	git commit -m "Deployment of Stringer"
	git push origin
 ```

9. Check that you are able to access the website at the URL given, i.e. feeds-username.rhcloud.com. Then set your password, import your feeds and all good to go!


Adding Cronjob to Fetch Feeds
-----------------------------

After importing feeds, a cron job is needed on OpenShift to fetch feeds. 

1. Add a new cron cartridge for the cron job.

 ```sh
    rhc cartridge add cron -a feeds
 ```

2. Add a new executable file, .openshift/cron/hourly/fetch_feeds and put the below 4 lines into it. 

 ```
	./usr/bin/rhcsh
	pushd ${OPENSHIFT_REPO_DIR} > /dev/null
	bundle exec rake fetch_feeds RAILS_ENV="production"
	popd > /dev/null
 ```

3. Make the file executable.

 ```sh
    chmod +x .openshift/cron/hourly/fetch_feeds
 ```

4. Push all changes to OpenShift.

 ```sh
	git add .
	git commit -m "Added Cronjob"
	git push origin
 ```

5. Done! The cron job should fetch feeds every hour.

---

### VPS (`docs/VPS.md`)

Install some dependencies
=========================

The first step is installing some essential dependencies from your VPS's package manager.

#### Ubuntu/Debian

    sudo apt-get install git libxml2-dev libxslt-dev libcurl4-openssl-dev libpq-dev build-essential postgresql libreadline-dev

#### CentOS/Fedora

    sudo yum install git libxml2-devel libxslt-devel curl-devel postgresql-devel make automake gcc gcc-c++ postgresql-server readline-devel openssl-devel

On CentOS after installing Postgres, I needed to run these commands, Fedora likely the same.

    service postgresql initdb && service postgresql start

#### Arch Linux

    pacman -S git postgresql base-devel libxml2 libxslt curl readline postgresql-libs

Here are some Arch specific instructions for setting up postgres

    systemd-tmpfiles --create postgresql.conf
    chown -c -R postgres:postgres /var/lib/postgres
    sudo su - postgres -c "initdb --locale en_US.UTF-8 -E UTF8 -D '/var/lib/postgres/data'"
    systemctl start postgresql
    systemctl enable postgresql


Set up the database
===================

Create a postgresql user to own the database Stringer will use, you will need to create a password too, make a note of it.

    sudo -u postgres createuser -D -A -P stringer

Now create the database Stringer will use

    sudo -u postgres createdb -O stringer stringer_live

Create your stringer user
=========================

We will run stringer as it's own user for security, also we will be installing a specific version of ruby to stringer user's home directory, this saves us worrying whether the version of ruby and some dependencies provided by your distro are compatible with Stringer.

    sudo useradd stringer -m -s /bin/bash
    sudo su -l stringer

Always use -l switch when you switch user to your stringer user, without it your modified .bash_profile or .profile file will be ignored.

Install Ruby for your stringer user
===================================

We are going to use Rbenv to manage the version of Ruby you use.

    cd
    git clone git://github.com/sstephenson/rbenv.git .rbenv
    echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> $HOME/.bash_profile
    echo 'eval "$(rbenv init -)"' >> $HOME/.bash_profile
    git clone git://github.com/sstephenson/ruby-build.git $HOME/.rbenv/plugins/ruby-build
    source ~/.bash_profile

    rbenv install 2.7.5
    rbenv local 2.7.5
    rbenv rehash

We also need to install bundler which will handle Stringer's dependencies

    gem install bundler
    rbenv rehash

We will also need foreman to run our app

    gem install foreman

Install Stringer and set it up
==============================

Grab Stringer from github

    git clone git@github.com:stringer-rss/stringer.git
    cd stringer

Use bundler to grab and build Stringer's dependencies

    bundle install
    rbenv rehash

Stringer uses environment variables to configure the application. Edit these values to reflect your settings.

    echo 'export DATABASE_URL="postgres://stringer:EDIT_ME@localhost/stringer_live"' >> $HOME/.bash_profile
    echo 'export RACK_ENV="production"' >> $HOME/.bash_profile
    echo 'export RAILS_ENV="production"' >> $HOME/.bash_profile
    echo "export SECRET_KEY_BASE=`openssl rand -hex 64`" >> $HOME/.bash_profile
    echo "export ENCRYPTION_PRIMARY_KEY=`openssl rand -hex 64`" >> $HOME/.bash_profile
    echo "export ENCRYPTION_DETERMINISTIC_KEY=`openssl rand -hex 64`" >> $HOME/.bash_profile
    echo "export ENCRYPTION_KEY_DERIVATION_SALT=`openssl rand -hex 64`" >> $HOME/.bash_profile
    source ~/.bash_profile

Tell stringer to run the database in production mode, using the `postgres` database you created earlier.

    cd $HOME/stringer
    rake db:migrate RAILS_ENV=production

Run the application:

    foreman start

Set up a cron job to parse the rss feeds.

    crontab -e

add the lines

    SHELL=/bin/bash
    PATH=/home/stringer/.rbenv/bin:/bin/:/usr/bin:/usr/local/bin/:/usr/local/sbin
    */10 * * * *  source $HOME/.bash_profile; cd $HOME/stringer/; bundle exec rake fetch_feeds;

Manage Stringer With Systemd
============================

You may want to manage Stringer as a systemd service on distributions come with systemd.

As stringer user, export app service files with foreman:

    cd ~/stringer
    mkdir systemd-services
    foreman export systemd systemd-services -a stringer -u stringer

Logout stringer user, install systemd services:

    sudo cp -a ~stringer/stringer/systemd-services/* /etc/systemd/system

As stringer user, close existing Stringer instance:

    exit # exit app

Start app as a systemd service and make app run at startup

    sudo systemctl start stringer.target
    sudo systemctl enable stringer.target

Reverse Proxy With Nginx
========================

You may want to use nginx as reverse proxy server to add SSL/TLS for security
reason. Here is a sample configuration:

``` nginx
server {
    listen 80;
    # listen 443 ssl;
    # ssl_certificate ssl/fullchain.pem;
    # ssl_certificate_key ssl/privatekey.pem;
    # you can try to use Mozilla SSL Configuration Generator
    # to harden your TLS configuration
    # https://mozilla.github.io/server-side-tls/ssl-config-generator/
    server_name example.com;
    location / {
        proxy_pass http://127.0.0.1:1337/;
        proxy_set_header Host $host;
    }
}
```

Deploy Stringer With Passenger And Apache
=========================================

You may want to run Stringer with the existing Apache server. We need to install
*mod_passenger* and edit few files.

The installation of *mod_passenger* depends on VPS's system distribution release.
Offical installation guide is available at [Passenger Library](https://www.phusionpassenger.com/library/install/apache/install/oss/)

After validating the *mod_passenger* install, we will fetch dependencies again
to meet Passenger's default GEM_HOME set. As stringer user:

    cd ~/stringer
    bundle install --path vendor/bundle

Edit database.yml with correct database url:

    cd ~/stringer
    sed -i "s|url: .*|url: $DATABASE_URL|" config/database.yml

Add VirtualHost to your Apache installation, here's a sample configuration:

```bash
<VirtualHost *:80>
    ServerName example.com
    DocumentRoot /home/stringer/stringer/public

    PassengerEnabled On
    PassengerAppRoot /home/stringer/stringer
    PassengerRuby /home/stringer/.rbenv/shims/ruby
    # PassengerLogFile /dev/null # don't flow logs to apache error.log

    <Directory /home/stringer/stringer/public>
        Options FollowSymLinks
        Require all granted
        AllowOverride All
    </Directory>


    # you can harden your connection with https, don't forget
    # change to <VirtualHost *:443>
    # SSLCertificateFile /etc/path/to/example.com/fullchain.pem
    # SSLCertificateKeyFile /etc/path/to/example.com/privkey.pem
    # Include /etc/path/to/options-ssl-apache.conf
</VirtualHost>
```

---

--- METRICS ---
- Files Extracted: 5
- Estimated Token Budget: ~5004 tokens
- Canonical Reference: https://codewiki.google/github.com/stringer-rss/stringer
