Deploy Readeck (read-it-later / bookmark manager) on Google Cloud Run with persistent storage using a Cloud Storage bucket.
This repository provides a simple script to deploy a personal instance with minimal setup.
This setup runs Readeck in a single Cloud Run instance and stores data in a mounted Cloud Storage bucket.
Key characteristics:
- Stateless container (Cloud Run)
- Persistent storage via mounted bucket
- SQLite database
- Single instance to avoid concurrency issues
- Least-privilege service account, secret held in Secret Manager
This deployment is designed for personal use:
- Simple to deploy (single script)
- No database to manage (SQLite)
- Very low cost
For light usage (a few requests per day):
- Cloud Run: often free (within free tier)
- Cloud Storage: a few cents per month
- Artifact Registry: negligible
- Secret Manager: free (one active secret version, free tier covers six)
👉 In practice, this setup typically costs ~0–1€/month for personal usage.
Note that Cloud Run and Cloud Storage free tiers are counted per billing account, not per project, so they are shared with anything else you host.
Trade-offs:
- Not scalable (single instance)
- Not suitable for teams or heavy traffic
Cloud Run
└─ Readeck container (runs as readeck-sa)
├─ /readeck (mounted bucket)
└─ READECK_SECRET_KEY (from Secret Manager)
Artifact Registry
└─ Docker image (mirrored from Codeberg, version-pinned)
Cloud Storage
└─ SQLite database + files
Secret Manager
└─ readeck-secret-key
- Google Cloud project, with billing enabled
- gcloud CLI installed and authenticated
craneor Docker, to mirror the image into your project
Readeck is only published on the Codeberg container registry, which Cloud Run
cannot pull from. The image therefore has to be copied into your own Artifact
Registry. crane is the
lighter option since it needs no daemon:
brew install craneThe script uses crane if present and falls back to Docker otherwise.
git clone https://github.com/arenier/readeck-run.git
cd readeck-runSet your environment variables:
export PROJECT_ID="your-project-id"
export REGION="europe-west1"
export READECK_VERSION="0.22.3" # optional, defaults to the pinned versionPROJECT_ID and REGION fall back to your gcloud configuration if unset.
chmod +x deploy.sh
./deploy.sh- Enables the required Google Cloud APIs
- Creates an Artifact Registry repository
- Mirrors the pinned Readeck image from Codeberg into your project
- Creates a Cloud Storage bucket, with uniform bucket-level access and public access prevention
- Creates a dedicated service account, granted access to that bucket only
- Generates
READECK_SECRET_KEYin Secret Manager on first run, and reuses it afterwards - Deploys the Cloud Run service
Every step is guarded, so the script is idempotent: running it again against an existing deployment updates the service without recreating the bucket, the service account or the secret.
After deployment, a URL is displayed in the terminal.
Open it in your browser and create your account. Do this promptly — the service
is deployed with --allow-unauthenticated, so until an account exists anyone
reaching the URL could create the first one.
Readeck releases roughly monthly. To upgrade, bump the pinned version and run the script again:
export READECK_VERSION="0.23.0"
./deploy.shThe script mirrors the new image and deploys it. Your data and your secret key are left untouched.
Do not replace
READECK_SECRET_KEYon an existing install. Readeck derives other keys from it, so changing it invalidates API tokens, application passwords and share links. The script creates it once and never regenerates it.
A few deliberate choices, worth knowing if you adapt this script:
- Dedicated service account. Without
--service-account, Cloud Run falls back to the default Compute service account, which holdsroles/editoron the entire project. Readeck fetches arbitrary URLs by design, so a container compromise would otherwise reach every resource you own. The service account here is grantedroles/storage.objectAdminon its own bucket and nothing else. - Secret in Secret Manager, not an environment variable. A plaintext env var is readable by anyone with
roles/run.vieweron the project, and it is not audit-logged. The secret key signs sessions and CSRF tokens, so it is worth protecting. - Bucket hardening.
--uniform-bucket-level-accessmakes IAM the only access path by disabling per-object ACLs;--public-access-preventionmakes it impossible to grant the bucket toallUsers, even by accident. - Version pinning. Deploying
:latestmeans you cannot tell which version is running, and a new upstream push would silently change your deployment. - IAM propagation. A freshly created service account is not immediately usable in IAM bindings. The script retries for up to two minutes rather than failing on a race.
The service itself is public (--allow-unauthenticated), relying on Readeck's
own authentication. If you want network-level filtering or rate limiting, that
requires putting an external HTTPS load balancer and Cloud Armor in front,
which adds a fixed monthly cost well above the rest of this setup.
- SQLite is stored on a mounted bucket (not a full POSIX filesystem)
- Only one instance is allowed (
max-instances=1), which is required for SQLite over gcsfuse - During a deployment, the new and old revisions may briefly overlap, so avoid deploying while the app is in use
- Not designed for high traffic or team usage
For a more robust setup, consider:
- Using PostgreSQL (Cloud SQL)
- Removing the single-instance limitation
- Setting a billing budget alert, since the service is publicly reachable
MIT
Inspired by https://github.com/daniefdz/actual-run