Wallabag login screen

Deploying Wallabag to Kubernetes

Wallabag is an open-source, self-hostable read it later application similar to Mozilla Pocket that I discovered recently thanks to the r/selfhosted Reddit community. I used Pocket for a couple of years and I was happy with it, but now, I’m replacing as many online services as I can with open source alternatives to regain control over my data which is where Wallabag comes in. Wallabag runs in a server, has a mobile app, browser extension and makes it easy to import my data from Pocket. Perfect combination. In this post, I’ll show you how I deployed it to my Kubernetes cluster.

Credential Set Up

Wallabag stores its data in a SQL database that requires a username and password. To avoid hard-coding database credentials in the code, I created credentials in AWS Parameter Store using the AWS CLI so I could reference them from the code. If you’re interested in AWS security, checkout my post on it.

aws ssm put-parameter 
--name "/K8s/Wallabag/wallabag-credentials" 
--type "SecureString" 
--value '{"SYMFONY__ENV__DATABASE_PASSWORD": "your_password", "SYMFONY__ENV__DATABASE_USER": "your_db_user"}'

The command above creates an encrypted secret in the AWS Parameter Store. Next, to sync the secret to Kubernetes, I created an ExternalSecret object that references the AWS Secret and syncs it to a local secret called wallabag-container-env:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: wallabag-external-secret
  namespace: wallabag
spec:
  refreshInterval: 1h
  kind: SecretStore
target:
  name: wallabag-container-env
  creationPolicy: Owner
data:
  - secretKey: SYMFONY__ENV__DATABASE_PASSWORD
    remoteRef:
      key: /K8s/Wallabag/wallabag-credentials
      property: SYMFONY__ENV__DATABASE_PASSWORD

  - secretKey: SYMFONY__ENV__DATABASE_USER
    remoteRef:
      key: /K8s/Wallabag/wallabag-credentials
      property: SYMFONY__ENV__DATABASE_USER

External Secrets is a third-party Kubernetes operator that retrieves secrets from secure vaults and syncs them to Kubernetes secrets automatically.

Create Kubernetes Objects

After defining the credentials, I created all the other resources needed to deploy the application using this manifests I’ll share below. If you’d like to see the full YAML manifest, check it out in GitHub.

Namespace and ConfigMap

The first two resources define a namespace and configmap. A namespace in Kubernetes is a logical division in the cluster that helps group and isolate resources that must be deployed together. The ConfigMap is a way of injecting configuration data like environment variables and settings into containers. Here, I create the Wallabag namespace to containe all Wallabag resources and pass environment variables for connecting to the postgres database.

---
apiVersion: v1
kind: Namespace
metadata:
  name: wallabag
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: wallabag-configmap
  namespace: wallabag
data:
  SYMFONY__ENV__DATABASE_PORT: "5432"
  SYMFONY__ENV__DATABASE_DRIVER: pdo_pgsql
  SYMFONY__ENV__DATABASE_NAME: wallabag
  SYMFONY__ENV__DATABASE_HOST: wallabag-db
  SYMFONY__ENV__DOMAIN_NAME: "http://wallabag.ndlovucloud.co.zw"
---

Deployment and Networking

Next, I set up an ingress, a service and a deployment. An ingress acts as a gateway that routes traffic based on hostname, the service allows external traffic into the cluster and the deployment manages running containers. I reference the database credentials I created above as a secret in the deployment:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
    name: wallabag-ingress
    namespace: wallabag
    annotations:
      gethomepage.dev/description: Wallabag
      gethomepage.dev/enabled: "true"
      gethomepage.dev/group: Cluster Management
      gethomepage.dev/icon: wallabag.png
      gethomepage.dev/name: Wallabag
      nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  ingressClassName: nginx
  rules:
    - host: wallabag.ndlovucloud.co.zw
      http:
        paths:
          - path: /?(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: wallabag
                port:
                  number: 80
---
apiVersion: v1
kind: Service
metadata:
  name: wallabag
  namespace: wallabag
spec:
  ports:
    - protocol: TCP
      port: 8083
      targetPort: 80
      name: http
  selector:
    app: wallabag
  type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wallabag
  namespace: wallabag
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wallabag
  template:
    metadata:
      labels:
        app: wallabag
    spec:
      containers:
      - name: wallabag-web
        image: wallabag/wallabag:2.6.10
        resources:
            requests:
              memory: 128Mi
              cpu: 100m
            limits:
              memory: 256Mi
              cpu: 200m
        ports:
          - containerPort: 80
            protocol: TCP
        envFrom:
          - configMapRef:
              name: wallabag-configmap
          - secretRef:
              name: wallabag-container-env

I didn’t set up TLS certificates directly in Kubernetes for this project because I’ll use Cloudflare Tunnels to expose it and Cloudflare sets up automatic HTTPS for free.

Database

Next, I configured the database using a StatefulSet and a headless service. Statefulsets are similar to deployments but are suited for stateful objects like databases. They ensure that data isn’t lost when pods or containers are stopped or restarted. Headless services allow applications to communicate directly with the database instance.

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: wallabag-db
  namespace: wallabag
spec:
  selector:
    matchLabels:
      app: wallabag-db
  serviceName: wallabag-db
  replicas: 1
  template:
    metadata:
      labels:
        app: wallabag-db
    spec:
      containers:
        - name: wallabag-db
          image: postgres:13
          ports:
            - containerPort: 5432
          volumeMounts:
          - name: postgres-storage
            mountPath: /var/lib/postgresql/data
          env:
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: wallabag-container-env
                  key: SYMFONY__ENV__DATABASE_USER
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: wallabag-container-env
                  key: SYMFONY__ENV__DATABASE_PASSWORD
            - name: POSTGRES_DB
              value: wallabag
  volumeClaimTemplates:
    - metadata:
        name: postgres-storage
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: local-path
        resources:
          requests:
            storage: 1Gi
  ordinals:
    start: 1
---
apiVersion: v1
kind: Service
metadata:
  name: wallabag-db
  namespace: wallabag
spec:
  ports:
    - port: 5432
      targetPort: 5432
      protocol: TCP
  selector:
    app: wallabag-db
  clusterIP: None

Outcome And Problems

After writing all the manifests, I committed them to git and pushed them to the repo and Flux CD deployed everything to the cluster. It took a bit of tinkering and wrestling with the YAML to get everything to deploy correctly, but after it did, I was met by this cryptic HTTP 500 error when I tried to open the application:

Wallabag post installation error

The logs showed that there were missing tables and relations in the database, database migrations didn’t get applied. After a quick Google search, I learned that the fix was to drop into the wallabag container shell and run the installation manually and fix file permissions afterwards:

kubectl exec -it wallabag-dcd545998-d9h47 -n wallabag sh

php bin/console wallabag:install --env=prod -n

# Fix file permissions, takes a few minutes to complete
chown -R nobody:nobody /var/www/wallabag

The two commands took a couple of minutes to complete but after running them, I was able to login to Wallabag

Conclusion

I like Wallabag for its comfortable reader mode, mobile app integration and the ease of importing data into it. On the downside, some aspects of it feel a little unrefined compared to Pocket,but that’s a small price to pay to be in full control over my own data.