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:

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.