Wallabag
Self-hosted read-it-later application. Wallabag saves articles from the web by extracting their full text and storing them in your own PostgreSQL database. Read your saved articles offline across any device, organize them with tags, annotations, and reading lists, and share them with configurable access controls.
Key Features
- Full-text extraction — saves the complete article content, not just a link
- Multi-device access — web UI, mobile apps, browser extensions, and e-reader exports
- Tag and annotation system — organize articles with tags, highlights, and notes
- Working first install — a setup init container installs the schema on first start and runs Doctrine migrations on every upgrade
- Initial super-admin from a Secret — configurable username and email with a generated or supplied password
- PostgreSQL backend — bundled subchart or external database
- Optional Redis — subchart or external Redis for the asynchronous import queue
- Persistent storage — PVC-backed data directory for downloaded article images
- S3 backup — scheduled
pg_dumpof the Wallabag database to S3-compatible storage - Ingress and Gateway API — Ingress with TLS or Gateway API HTTPRoutes
- Hardened defaults — no privilege escalation, minimal capabilities,
RuntimeDefaultseccomp, no ServiceAccount token
Installation
HTTPS repository:
helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install wallabag helmforge/wallabag
OCI registry:
helm install wallabag oci://ghcr.io/helmforgedev/helm/wallabag
Getting Started
The first pod start installs the schema and creates the initial super-admin. Read its password and open the UI:
kubectl rollout status deploy/wallabag-wallabag --timeout=300s
kubectl get secret wallabag-wallabag-admin -o jsonpath='{.data.admin-password}' | base64 -d; echo
kubectl port-forward svc/wallabag-wallabag 8080:80
Log in at http://localhost:8080/ as wallabag (or wallabag.admin.username). Resource names follow
<release>-wallabag.
Deployment Examples
# values.yaml — Wallabag with bundled PostgreSQL (default)
wallabag:
domainName: 'https://wallabag.example.com'
secret: 'a-random-symfony-app-secret-here'
postgresql:
enabled: true
auth:
password: 'postgres-password'
persistence:
enabled: true
size: 5Gi
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: wallabag.example.com
paths:
- path: /
pathType: Prefix# values.yaml — Wallabag with PostgreSQL and Redis for the asynchronous import queue
wallabag:
domainName: 'https://wallabag.example.com'
secret: 'a-random-symfony-app-secret-here'
postgresql:
enabled: true
auth:
password: 'postgres-password'
redis:
enabled: true
auth:
password: 'redis-password'
persistence:
enabled: true
size: 5Gi
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: wallabag.example.com
paths:
- path: /
pathType: Prefix# values.yaml — Wallabag with external PostgreSQL and Redis
wallabag:
domainName: 'https://wallabag.example.com'
secret: 'a-random-symfony-app-secret-here'
postgresql:
enabled: false
database:
external:
host: postgresql.database.svc
port: '5432'
name: wallabag
username: wallabag
password: 'db-password'
redis:
enabled: false
externalRedis:
host: redis.cache.svc
port: '6379'
existingSecret: wallabag-redis
existingSecretPasswordKey: redis-password
persistence:
enabled: true
size: 5Gi
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: wallabag.example.com
paths:
- path: /
pathType: Prefix# values.yaml — Daily backup of the Wallabag database to S3
wallabag:
domainName: 'https://wallabag.example.com'
secret: 'a-random-symfony-app-secret-here'
postgresql:
enabled: true
auth:
password: 'postgres-password'
backup:
enabled: true
schedule: '0 3 * * *'
s3:
endpoint: https://s3.amazonaws.com
bucket: my-wallabag-backups
accessKey: '<set-me>'
secretKey: '<set-me>'
persistence:
enabled: true
size: 5Gi# values.yaml — Wallabag published through a shared Gateway
wallabag:
domainName: 'https://wallabag.example.com'
gatewayAPI:
enabled: true
httpRoutes:
- name: public
parentRefs:
- name: shared-gateway
namespace: gateway-system
sectionName: https
hostnames:
- wallabag.example.comConfiguration Reference
Core
| Parameter | Type | Default | Description |
|---|---|---|---|
global |
object | {} |
Global values shared with parents. |
nameOverride |
string | "" |
Override the chart name. |
fullnameOverride |
string | "" |
Override the full release name. |
commonLabels |
object | {} |
Extra labels added to all resources. |
Image
| Parameter | Type | Default | Description |
|---|---|---|---|
image.repository |
string | docker.io/wallabag/wallabag |
Wallabag container image. |
image.tag |
string | "2.6.14" |
Image tag. |
image.pullPolicy |
string | IfNotPresent |
Image pull policy. |
imagePullSecrets |
array | [] |
Pull secrets for private registries. |
Wallabag Configuration
| Parameter | Type | Default | Description |
|---|---|---|---|
wallabag.port |
integer | 80 |
nginx listener inside the container; the process binds this port. |
wallabag.domainName |
string | "https://wallabag.example.com" |
Public URL of the Wallabag instance. Must be changed before deploying. |
wallabag.serverName |
string | Wallabag |
Instance name shown in the UI and API. |
wallabag.secret |
string | "" |
Symfony secret; generated once and preserved across upgrades. |
wallabag.existingSecret |
string | "" |
Existing Kubernetes Secret containing the Symfony secret. |
wallabag.existingSecretKey |
string | symfony-secret |
Key inside the existing secret holding the secret value. |
wallabag.registration |
boolean | false |
Allow new user registrations. Disabled by default for security. |
wallabag.admin.username |
string | wallabag |
Initial super-admin username, applied on the first install. |
wallabag.admin.email |
string | [email protected] |
Initial super-admin email, applied on the first install. |
wallabag.admin.password |
string | "" |
Initial super-admin password; generated once when empty. |
wallabag.admin.existingSecret |
string | "" |
Existing Secret with the initial super-admin password. |
wallabag.admin.existingSecretPasswordKey |
string | admin-password |
Key inside the admin Secret. |
wallabag.extraEnv |
array | [] |
Extra environment variables injected into the Wallabag container. |
Database Setup
| Parameter | Type | Default | Description |
|---|---|---|---|
setup.enabled |
boolean | true |
Install the schema on first start and run migrations on each start. |
setup.databaseWaitTimeoutSeconds |
integer | 300 |
Seconds the setup container waits for PostgreSQL. |
setup.resources |
object | {} |
Resources for the setup init container. |
The default wallabag.domainName is https://wallabag.example.com. Wallabag uses this value to generate internal
links for article sharing, e-reader exports, and API responses. Deploy with your actual public URL or internal links
will be broken.
When wallabag.secret and wallabag.admin.password are empty, the chart generates them once and reads them back from
the cluster on helm upgrade. Tools that render without cluster access, such as Argo CD or helm template, generate
new values on every render. Set wallabag.existingSecret and wallabag.admin.existingSecret in that case.
The upstream installer always creates wallabag/wallabag. The chart replaces it on the first install with
wallabag.admin.username, wallabag.admin.email, and the password from the admin Secret. Later password changes go
through the Wallabag UI. wallabag.registration: false keeps public sign-ups disabled.
Database — Embedded Subchart
| Parameter | Type | Default | Description |
|---|---|---|---|
postgresql.enabled |
boolean | true |
Deploy a bundled PostgreSQL subchart for Wallabag. |
postgresql.architecture |
string | standalone |
PostgreSQL deployment architecture. |
postgresql.auth.database |
string | wallabag |
Database name created by the subchart. |
postgresql.auth.username |
string | wallabag |
Database username created by the subchart. |
postgresql.auth.password |
string | "" |
Database password (auto-generated if empty). |
Database — External
| Parameter | Type | Default | Description |
|---|---|---|---|
database.external.host |
string | "" |
External PostgreSQL hostname or IP. |
database.external.port |
string | "5432" |
External PostgreSQL port. |
database.external.name |
string | wallabag |
Database name on the external server. |
database.external.username |
string | wallabag |
Username for the external database. |
database.external.password |
string | "" |
Password for the external database (plain text — prefer secret). |
database.external.existingSecret |
string | "" |
Existing secret containing the database password. |
database.external.existingSecretPasswordKey |
string | password |
Key inside the existing secret for the password. |
Import Queue — Redis Subchart
| Parameter | Type | Default | Description |
|---|---|---|---|
redis.enabled |
boolean | false |
Deploy a bundled Redis subchart for the import queue. |
redis.architecture |
string | standalone |
Redis deployment architecture. |
redis.auth.password |
string | "" |
Redis password (auto-generated by the subchart if empty). |
Import Queue — External Redis
| Parameter | Type | Default | Description |
|---|---|---|---|
externalRedis.host |
string | "" |
External Redis hostname or IP. |
externalRedis.port |
string | "6379" |
External Redis port. |
externalRedis.password |
string | "" |
External Redis password (prefer an existing Secret). |
externalRedis.existingSecret |
string | "" |
Existing Secret with the external Redis password. |
externalRedis.existingSecretPasswordKey |
string | redis-password |
Key inside the Redis Secret. |
Wallabag uses Redis only as the queue for asynchronous imports. The queue is consumed by
wallabag:import:redis-worker processes, which the chart does not run. Keep the internal setting “Enable Redis to
fetch data asynchronously” disabled unless you deploy those workers.
Persistence
| Parameter | Type | Default | Description |
|---|---|---|---|
persistence.enabled |
boolean | true |
Enable a PVC for /var/www/wallabag/data. |
persistence.size |
string | 2Gi |
PVC size. |
persistence.storageClass |
string | "" |
StorageClass for the PVC. |
persistence.accessModes |
array | ["ReadWriteOnce"] |
PVC access modes. |
persistence.existingClaim |
string | "" |
Use an existing PVC instead of creating one. |
Service
| Parameter | Type | Default | Description |
|---|---|---|---|
service.type |
string | ClusterIP |
Kubernetes service type. |
service.port |
integer | 80 |
Service port exposed to the cluster. |
service.annotations |
object | {} |
Annotations for the Service. |
Ingress
| Parameter | Type | Default | Description |
|---|---|---|---|
ingress.enabled |
boolean | false |
Enable an Ingress resource. |
ingress.ingressClassName |
string | traefik |
Ingress class name. |
ingress.annotations |
object | {} |
Annotations for the Ingress (e.g. cert-manager). |
ingress.hosts |
array | [] |
Ingress host and path rules. |
ingress.tls |
array | [] |
TLS configuration (secret name and hosts). |
Gateway API
| Parameter | Type | Default | Description |
|---|---|---|---|
gatewayAPI.enabled |
boolean | false |
Render Gateway API HTTPRoutes. |
gatewayAPI.httpRoutes |
array | [] |
HTTPRoutes; each requires parentRefs and defaults to the Service. |
Probes
| Parameter | Type | Default | Description |
|---|---|---|---|
probes.path |
string | /api/info |
HTTP path used by every probe. |
probes.startup.enabled |
boolean | true |
Enable startup probe. |
probes.startup.initialDelaySeconds |
integer | 15 |
Startup probe initial delay. |
probes.startup.periodSeconds |
integer | 10 |
Startup probe period. |
probes.startup.timeoutSeconds |
integer | 5 |
Startup probe timeout. |
probes.startup.failureThreshold |
integer | 30 |
Startup probe failure threshold. |
probes.liveness.enabled |
boolean | true |
Enable liveness probe. |
probes.liveness.initialDelaySeconds |
integer | 0 |
Liveness probe initial delay. |
probes.liveness.periodSeconds |
integer | 15 |
Liveness probe period. |
probes.liveness.timeoutSeconds |
integer | 5 |
Liveness probe timeout. |
probes.liveness.failureThreshold |
integer | 3 |
Liveness probe failure threshold. |
probes.readiness.enabled |
boolean | true |
Enable readiness probe. |
probes.readiness.initialDelaySeconds |
integer | 0 |
Readiness probe initial delay. |
probes.readiness.periodSeconds |
integer | 10 |
Readiness probe period. |
probes.readiness.timeoutSeconds |
integer | 5 |
Readiness probe timeout. |
probes.readiness.failureThreshold |
integer | 3 |
Readiness probe failure threshold. |
Backup
The backup CronJob runs pg_dump against the Wallabag PostgreSQL database and uploads the archive to S3. This
protects all saved articles, tags, annotations, and user accounts.
| Parameter | Type | Default | Description |
|---|---|---|---|
backup.enabled |
boolean | false |
Enable scheduled S3 backup CronJob. |
backup.schedule |
string | "0 3 * * *" |
Cron schedule for backups. |
backup.suspend |
boolean | false |
Suspend the CronJob without deleting it. |
backup.concurrencyPolicy |
string | Forbid |
CronJob concurrency policy. |
backup.successfulJobsHistoryLimit |
integer | 3 |
Number of successful Job records to keep. |
backup.failedJobsHistoryLimit |
integer | 3 |
Number of failed Job records to keep. |
backup.backoffLimit |
integer | 1 |
Job retry limit. |
backup.archivePrefix |
string | wallabag |
Prefix for backup archive filenames. |
backup.images.postgresql |
string | docker.io/library/postgres:18-alpine |
Image used for pg_dump. |
backup.images.uploader |
string | docker.io/helmforge/mc:1.0.0 |
Image used for S3 upload. |
backup.resources |
object | {} |
Resources for backup containers. |
backup.s3.endpoint |
string | "" |
S3-compatible endpoint URL. |
backup.s3.bucket |
string | "" |
Target bucket name. |
backup.s3.prefix |
string | wallabag |
Key prefix within the bucket. |
backup.s3.createBucketIfNotExists |
boolean | true |
Create the bucket automatically if it does not exist. |
backup.s3.existingSecret |
string | "" |
Existing secret containing S3 access and secret keys. |
backup.s3.existingSecretAccessKeyKey |
string | access-key |
Key in the existing secret for the S3 access key. |
backup.s3.existingSecretSecretKeyKey |
string | secret-key |
Key in the existing secret for the S3 secret key. |
backup.s3.accessKey |
string | "" |
Inline S3 access key (ignored when existingSecret is set). |
backup.s3.secretKey |
string | "" |
Inline S3 secret key (ignored when existingSecret is set). |
backup.database.host |
string | "" |
Override database host for backup (uses app credentials if empty). |
backup.database.port |
string | "" |
Override database port for backup. |
backup.database.name |
string | "" |
Override database name for backup. |
backup.database.username |
string | "" |
Override database username for backup. |
backup.database.password |
string | "" |
Override database password for backup. |
backup.database.existingSecret |
string | "" |
Existing secret containing backup database credentials. |
backup.database.existingSecretPasswordKey |
string | password |
Key in the existing secret for the backup database password. |
backup.database.postgresDumpArgs |
string | "" |
Extra arguments passed to pg_dump. |
Resources and Security
| Parameter | Type | Default | Description |
|---|---|---|---|
resources |
object | {} |
CPU and memory requests and limits. |
podSecurityContext |
object | fsGroup: 65534, RuntimeDefault seccomp |
Pod-level security context. |
securityContext |
object | no privilege escalation, minimal capabilities | Security context for the Wallabag and setup containers. |
automountServiceAccountToken |
boolean | false |
Mount the ServiceAccount token into the pod. |
The upstream image renders its configuration and starts nginx as root, then runs PHP-FPM as nobody. runAsNonRoot
and a read-only root filesystem are therefore not supported. The defaults drop every capability except CHOWN,
DAC_OVERRIDE, FOWNER, SETGID, SETUID, and NET_BIND_SERVICE.
Service Account
| Parameter | Type | Default | Description |
|---|---|---|---|
serviceAccount.create |
boolean | false |
Create a dedicated ServiceAccount. |
serviceAccount.name |
string | "" |
Override the ServiceAccount name. |
serviceAccount.annotations |
object | {} |
Annotations for the ServiceAccount. |
Scheduling
| Parameter | Type | Default | Description |
|---|---|---|---|
nodeSelector |
object | {} |
Node selector for scheduling. |
tolerations |
array | [] |
Tolerations for scheduling. |
affinity |
object | {} |
Affinity rules. |
topologySpreadConstraints |
array | [] |
Topology spread constraints. |
priorityClassName |
string | "" |
PriorityClass for the pod. |
terminationGracePeriodSeconds |
integer | 30 |
Termination grace period. |
podLabels |
object | {} |
Extra labels for the pod. |
podAnnotations |
object | {} |
Extra annotations for the pod. |
Extra
| Parameter | Type | Default | Description |
|---|---|---|---|
extraVolumes |
array | [] |
Extra volumes to attach to the pod. |
extraVolumeMounts |
array | [] |
Extra volume mounts for the container. |
extraManifests |
array | [] |
Extra Kubernetes manifests deployed alongside the chart. |
Common Issues
If articles fail to load or share links are broken, verify that wallabag.domainName matches the exact public URL you
are using to access Wallabag, including the https:// prefix and without a trailing slash. A mismatch causes Wallabag
to generate internal links pointing to the wrong host.
Wallabag returns HTTP 500 when its schema is missing. Check kubectl logs deploy/<release>-wallabag -c setup and
confirm that setup.enabled is true and PostgreSQL is reachable with the configured credentials.
The setup container creates the schema on first start. Restore a pg_dump archive into an empty public schema while
Wallabag is scaled to zero; on the next start the setup container only applies newer migrations.