How to Deploy tt-rss to a Kubernetes Cluster

15 min read | Posted: 2025-03-23 | tags: Kubernetes , K3s , TTRSS , RSS , Self-hosting

Tiny Tiny RSS (tt-rss) is an open-source RSS reader that can be run on your home server. It offers many excellent features including synchronization across multiple devices, a customizable interface, and extensibility through plugins.

I mainly use an RSS reader for two reasons:

  1. Selective information intake: In today’s information-overloaded environment, receiving information only from sources I’ve chosen prevents mental fatigue.
  2. Avoiding temptations: For example, subscribing to YouTube channels via RSS allows me to avoid recommended videos. This eliminates the need to fight against temptations.

I chose tt-rss because there’s no risk of service termination and it can be run on a home server. While FreshRSS is another self-hosted RSS reader option, tt-rss excels in terms of extensibility.

While information about deploying tt-rss using Helm charts (Kubernetes package management tool) can be found, there’s still limited information about writing Kubernetes manifests from scratch. Therefore, I’ll explain how to deploy tt-rss to a K8s cluster in the following order:

  1. System configuration and environment explanation
  2. MySQL database deployment
  3. tt-rss application deployment
  4. Nginx reverse proxy deployment

This article uses container images compatible with the ARM64 architecture of Raspberry Pi, but you can apply it to other architectures simply by changing the images.

Configuration files can be created separately or combined into one. If you want to set everything up at once, refer to the Appendix section at the end of the article. This article introduces the minimum configuration required, so feel free to customize it to suit your environment.

Prerequisite Knowledge: Required Kubernetes Resources

The main Kubernetes resources used for deployment are as follows:

  • Namespace: Logically groups resources, separating tt-rss and Nginx management
  • Deployment: Manages and scales application pods
  • Service: Provides access points to pods, ensuring stable communication
  • PersistentVolumeClaim: Persists database data
  • Secret: Securely manages sensitive information such as passwords

System Configuration

The system we’re building consists of the following components:

tt-rss configuration diagram
Figure 1: Kubernetes deployment configuration for tt-rss

  1. Nginx reverse proxy: Accepts external access and forwards to tt-rss
  2. tt-rss application: The RSS reader itself
  3. MySQL database: Stores tt-rss data

External access reaches tt-rss through Nginx, and tt-rss stores data in MySQL. Each component runs as a separate pod and connects via Services, allowing for individual updates and scaling.

MySQL Deployment

Creating a Namespace

First, create a namespace to logically separate tt-rss-related resources. This makes resource management easier and avoids conflicts with other applications.

# ttrss-namespace.yaml
# Namespace for logically grouping tt-rss-related resources
apiVersion: v1
kind: Namespace
metadata:
  name: ttrss  # tt-rss and MySQL resources will be placed in this namespace

Database Connection Secret

Manage database passwords as Secrets. This improves security by avoiding plaintext passwords in manifest files.

# Create Secret for MySQL root password and tt-rss user password
kubectl create secret generic mysql-secret \
--from-literal=mysql_root_pass=rootpassword \
--from-literal=mysql_ttrss_pass=ttrsspassword -n ttrss

MySQL Database PVC

Create a PersistentVolumeClaim to persist database data. This ensures data isn’t lost when pods restart.

# mysql-pvc.yaml
# PersistentVolumeClaim for persisting MySQL data
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  namespace: ttrss  # placed in ttrss namespace
  name: mysql-pvc  # this PVC will be referenced by the MySQL deployment
spec:
  accessModes:
    - ReadWriteOnce  # allows read-write access from a single node
  resources:
    requests:
      storage: 1Gi  # requests 1GB of storage (can be adjusted as needed)

MySQL Deployment

Deploy the MySQL database to the Kubernetes cluster. This deployment uses an ARM64-compatible MySQL image and configures the necessary environment variables and volume mounts.

# mysql-deployment.yaml
# Manifest for deploying MySQL database
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: ttrss  # placed in ttrss namespace
  name: mysql  # deployment name
spec:
  replicas: 1  # MySQL runs as a single instance (not a replication configuration)
  selector:
    matchLabels:
      app: mysql  # manages pods with this label
  template:
    metadata:
      labels:
        app: mysql  # applies label to pod
    spec:
      containers:
        - name: mysql
          image: arm64v8/mysql:8.0  # MySQL image for ARM64 architecture
          env:
            # MySQL configuration via environment variables
            - name: MYSQL_ROOT_PASSWORD  # root password (retrieved from Secret)
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: mysql_root_pass
            - name: MYSQL_DATABASE
              value: ttrss  # database name for tt-rss
            - name: MYSQL_USER
              value: ttrss  # database user for tt-rss
            - name: MYSQL_PASSWORD  # user password (retrieved from Secret)
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: mysql_ttrss_pass
          ports:
            - containerPort: 3306  # standard MySQL port
          volumeMounts:
            - name: mysql-storage  # volume for data persistence
              mountPath: /var/lib/mysql  # MySQL data directory
      volumes:
        - name: mysql-storage
          persistentVolumeClaim:
            claimName: mysql-pvc  # uses the PVC created earlier

MySQL Service

Create a service to access MySQL. This service enables the tt-rss application to access the database reliably.

# mysql-service.yaml
# Service definition for accessing MySQL
apiVersion: v1
kind: Service
metadata:
  namespace: ttrss  # placed in ttrss namespace
  name: mysql  # service name (accessible within the cluster via this DNS name)
spec:
  selector:
    app: mysql  # targets mysql pods
  ports:
    - port: 3306  # standard MySQL port
  type: ClusterIP  # accessible only within the cluster

tt-rss Deployment

tt-rss Deployment

Deploy the tt-rss application itself. This deployment configures the tt-rss container and the environment variables needed for database connection.

# ttrss-deployment.yaml
# Manifest for deploying tt-rss application
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: ttrss  # placed in ttrss namespace
  name: ttrss  # deployment name
spec:
  replicas: 1  # deployed as a single instance
  selector:
    matchLabels:
      app: ttrss  # manages pods with this label
  template:
    metadata:
      labels:
        app: ttrss  # applies label to pod
    spec:
      containers:
        - name: ttrss
          image: nventiveux/ttrss  # tt-rss container image
          env:
            # Database connection information set via environment variables
            - name: TTRSS_DB_HOST
              value: mysql  # MySQL service name
            - name: TTRSS_DB_PORT
              value: "3306"  # MySQL port
            - name: TTRSS_DB_NAME
              value: ttrss  # database name
            - name: TTRSS_DB_USER
              value: ttrss  # database user
            - name: TTRSS_DB_PASS  # database password (retrieved from Secret)
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: mysql_ttrss_pass
            - name: TTRSS_DB_TYPE
              value: mysql  # database type
            - name: TTRSS_SELF_URL_PATH
              value: "http://IP_Address:30080/"  # external access URL for tt-rss (change to match your environment)
          ports:
            - containerPort: 80  # tt-rss web server port

tt-rss Service

Create a service to access tt-rss. This service enables communication from Nginx to tt-rss.

# ttrss-service.yaml
# Service definition for accessing tt-rss application
apiVersion: v1
kind: Service
metadata:
  namespace: ttrss  # placed in ttrss namespace
  name: ttrss  # service name (accessible within the cluster via this DNS name)
spec:
  selector:
    app: ttrss  # targets ttrss pods
  ports:
    - name: http
      port: 80  # service port
      targetPort: 80  # container port
      protocol: TCP
  type: ClusterIP  # accessible only within the cluster

Nginx Deployment

Creating a Namespace

Create a dedicated namespace for separate management of Nginx resources. This allows management separate from tt-rss.

# nginx-namespace.yaml
# Namespace for separate management of Nginx resources
apiVersion: v1
kind: Namespace
metadata:
  name: nginx  # namespace for Nginx-related resources
  labels:
    app: nginx
    role: reverse-proxy  # indicates role as reverse proxy

Nginx Configuration ConfigMap

Manage Nginx configuration as a ConfigMap. This allows flexible updates to Nginx settings and separates deployment from configuration.

# nginx-configmap.yaml
# Manage Nginx configuration as ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: nginx  # placed in nginx namespace
  name: nginx-config  # ConfigMap name
data:
  default.conf: |
    server {
      listen 80;
      server_name localhost;
      
      # Reverse proxy settings for tt-rss
      location / {
        # Forward requests to tt-rss service using internal DNS
        proxy_pass http://ttrss.ttrss.svc.cluster.local:80/;
        
        # Header settings to properly convey client information to tt-rss
        proxy_set_header Host $host:30080;
        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;
      }
    }        

Nginx Deployment

Deploy Nginx. This deployment runs the Nginx container and mounts the ConfigMap created earlier as a configuration file.

# nginx-deployment.yaml
# Manifest for deploying Nginx reverse proxy
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: nginx  # placed in nginx namespace
  name: nginx  # deployment name
  labels:
    app: nginx
spec:
  replicas: 1  # deployed as a single instance (can be increased if high availability is needed)
  selector:
    matchLabels:
      app: nginx  # manages pods with this label
  template:
    metadata:
      labels:
        app: nginx  # applies label to pod
    spec:
      volumes:
        - name: nginx-config  # volume for configuration files
          configMap:
            name: nginx-config  # references the ConfigMap created earlier
      containers:
        - name: nginx
          image: nginx:latest  # uses official Nginx image
          ports:
            - containerPort: 80  # standard HTTP port for Nginx
          volumeMounts:
            - name: nginx-config  # mount ConfigMap
              mountPath: /etc/nginx/conf.d/  # Nginx configuration directory

Nginx Service

Create an Nginx service and expose it with NodePort. This allows access to tt-rss from outside the cluster.

# nginx-service.yaml
# Manifest for exposing Nginx service externally
apiVersion: v1
kind: Service
metadata:
  namespace: nginx  # placed in nginx namespace
  name: nginx-service  # service name
spec:
  selector:
    app: nginx  # targets nginx pods
  ports:
  - port: 80  # service port
    targetPort: 80  # container port
    nodePort: 30080  # exposed externally on node's port 30080
  type: NodePort  # published as NodePort type service

Accessing tt-rss

After completing the deployment, you can access tt-rss at the following URL:

http://<node-IP>:30080/

On first access, you can log in with the default username “admin” and password “password”.

tt-rss login screen
Figure 2: tt-rss login screen

If it doesn’t work properly, check the details using the kubectl describe pod or kubectl logs commands.

Appendix: All-in-One Configuration

Nginx manifest

# nginx-all.yaml
# Manifest defining all Nginx-related resources at once

# Namespace for Nginx
apiVersion: v1
kind: Namespace
metadata:
  name: nginx
  labels:
    app: nginx
    role: reverse-proxy
---
# ConfigMap for Nginx configuration
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: nginx 
  name: nginx-config
data:
  default.conf: |
    server {
      listen 80;
      server_name localhost;
      
      # Reverse proxy settings for tt-rss
      location / {
        # Forward requests to tt-rss service using internal DNS
        proxy_pass http://ttrss.ttrss.svc.cluster.local:80/;
        
        # Header settings to properly convey client information to tt-rss
        proxy_set_header Host $host:30080;
        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;
      }
    }        
---
# Nginx deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: nginx 
  name: nginx
  labels:
    app: nginx
spec:
  replicas: 1  # can be increased if high availability is needed
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      volumes:
        - name: nginx-config
          configMap:
            name: nginx-config
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
          volumeMounts:
            - name: nginx-config
              mountPath: /etc/nginx/conf.d/
---
# Nginx service (exposed externally via NodePort)
apiVersion: v1
kind: Service
metadata:
  namespace: nginx 
  name: nginx-service
spec:
  selector:
    app: nginx
  ports:
  - port: 80
    targetPort: 80
    nodePort: 30080
  type: NodePort

tt-rss manifest

# ttrss-all.yaml
# Manifest defining all tt-rss and MySQL-related resources at once

# Namespace for tt-rss
apiVersion: v1
kind: Namespace
metadata:
  name: ttrss
---
# PVC for MySQL data
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  namespace: ttrss
  name: mysql-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---
# MySQL deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: ttrss
  name: mysql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
        - name: mysql
          image: arm64v8/mysql:8.0
          env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: mysql_root_pass
            - name: MYSQL_DATABASE
              value: ttrss
            - name: MYSQL_USER
              value: ttrss
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: mysql_ttrss_pass
          ports:
            - containerPort: 3306
          volumeMounts:
            - name: mysql-storage
              mountPath: /var/lib/mysql
      volumes:
        - name: mysql-storage
          persistentVolumeClaim:
            claimName: mysql-pvc
---
# MySQL service
apiVersion: v1
kind: Service
metadata:
  namespace: ttrss
  name: mysql
spec:
  selector:
    app: mysql
  ports:
    - port: 3306
  type: ClusterIP
---
# tt-rss deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: ttrss
  name: ttrss
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ttrss
  template:
    metadata:
      labels:
        app: ttrss
    spec:
      containers:
        - name: ttrss
          image: nventiveux/ttrss
          env:
            - name: TTRSS_DB_HOST
              value: mysql
            - name: TTRSS_DB_PORT
              value: "3306"
            - name: TTRSS_DB_NAME
              value: ttrss
            - name: TTRSS_DB_USER
              value: ttrss
            - name: TTRSS_DB_PASS
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: mysql_ttrss_pass
            - name: TTRSS_DB_TYPE
              value: mysql
            - name: TTRSS_SELF_URL_PATH
              value: "http://IP_Address:30080/"
          ports:
            - containerPort: 80 
---
# tt-rss service
apiVersion: v1
kind: Service
metadata:
  namespace: ttrss
  name: ttrss
spec:
  selector:
    app: ttrss
  ports:
    - name: http
      port: 80
      targetPort: 80 
      protocol: TCP
  type: ClusterIP