iTranslated by AI
Deploying Gitea with Cloud Run and Litestream: A Failure Story
Overview
While not many people may attempt this, this article explores whether it's possible to deploy Gitea, a lightweight self-hosted Github/Bitbucket/GitLab alternative, on Cloud Run, and then replicate Gitea's built-in SQLite database to Cloud Storage using Litestream.
In the end, it didn't work out, but hopefully this can be of some use to someone...
What is Gitea?
Gitea is an MIT-licensed open-source DevOps platform that brings together everything from Git repository hosting to code review, issue management, package registry, and CI/CD in one package.
Written in Go, it is lightweight and resource-efficient enough to run even on a Raspberry Pi, and can be easily self-hosted on major operating systems like Linux, macOS, and Windows.
The project was forked from Gogs in 2016 and has continued community-driven feature expansion, with native support for modern development workflows such as "Gitea Actions," which is compatible with GitHub Actions.
"Git with a cup of tea" – its philosophy of aiming for a Git service as easy and casual as tea has garnered support from individual developers to large organizations.
What is Litestream?
Litestream is a Go-based streaming replication and backup tool that transfers SQLite WAL (Write-Ahead Log) to destinations like S3 in real-time, achieving second-level RPO (Recovery Point Objective) even on a single server.
It can be set up with just a YAML configuration file and a single command, and recovery is as simple as running
litestream restore, which automatically reconstructs the snapshot and increments.Released as MIT-licensed open-source in 2021, community development continues with BoltDB author Ben Johnson at the center.
It supports the "server-side SQLite" use case on platforms like Fly.io and is favored for its ability to scale lightly from Raspberry Pi to the cloud.
Running Gitea Locally
First, let's run Gitea locally. I'll proceed by following the guide below.
First, create a directory and then create the necessary subdirectories.
$ mkdir selfhost-gitea
$ cd selfhost-gitea
Next, create a compose.yml file with the following content.
volumes:
gitea-data:
gitea-config:
services:
gitea:
image: docker.gitea.com/gitea:1.24.0-rootless
restart: always
working_dir: /usr/src
volumes:
- .:/usr/src
- gitea-data:/var/lib/gitea
- gitea-config:/etc/gitea
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:2222"
Let's start it up and check.
$ docker compose up -d
After startup, access http://localhost:3000/. If the following screen appears, it's working.

For now, select "Install Gitea" with the default settings.
Then create an account and log in. The following screen will be displayed.

We've successfully run Gitea locally. Simple, isn't it? ✨
Looking at the previous configuration items, the default file path for the SQLite database is /var/lib/gitea/data/gitea.db.
Running Litestream Locally with MinIO
Next, I want to configure MinIO, which can be tested locally, as the replication destination for Litestream and replicate the SQLite database file we created earlier.
Add the MinIO environment to the previously created compose.yml.
volumes:
gitea-data:
gitea-config:
minio_data: # Added
services:
gitea:
# ...
# 👇 Added depends_on
depends_on:
- minio
# 👇 Added service
minio:
image: minio/minio:RELEASE.2025-02-18T16-25-55Z
volumes:
- minio_data:/minio/data
command: server --console-address ':9001' /minio/data
ports:
- 9000:9000
- 9001:9001
Also, since I want to use Litestream and SQLite commands (for debugging) within the services:gitea section, I'll prepare the following Dockerfile.local and configure the service to use it.
build:
context: .
dockerfile: Dockerfile.local
FROM keinos/sqlite3:3.50.1 AS sqlite3-builder
FROM alpine AS litestream-builer
ARG LITESTREAM_VERSION=0.3.13
ENV LITESTREAM_VERSION=${LITESTREAM_VERSION}
ADD "https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-v${LITESTREAM_VERSION}-linux-amd64.tar.gz" /tmp/litestream.tar.gz
RUN tar -C /usr/local/bin -xzf /tmp/litestream.tar.gz
FROM docker.gitea.com/gitea:1.24.0-rootless
COPY --from=sqlite3-builder /usr/bin/sqlite3 /usr/bin/sqlite3
COPY --from=litestream-builer /usr/local/bin/litestream /usr/local/bin/litestream
compose.yml
services:
gitea:
# 👇 Changed to this
build:
context: .
dockerfile: Dockerfile.local
image: selfhost-gitea
container_name: "selfhost-gitea"
# ....
Once this is done, start it with docker compose up -d and access the MinIO console at http://localhost:9001.
By default, both the username and password are minioadmin, so log in.
First, create a bucket for replication. Click "Create Bucket" and create a bucket named "gitea-bucket".

Next, go to "Access Keys" in the side menu > "Create access key +" and create a new Access Key.

Next, create a litestream.yml file with the following content. Set the access-key-id and secret-access-key to the ones you just created.
dbs:
- path: /var/lib/gitea/data/gitea.db
replicas:
- type: s3
bucket: gitea-bucket
path: gitea.db
endpoint: http://minio:9000
region: us-east-1
access-key-id: xxxxxxxx
secret-access-key: xxxxxxx
force-path-style: true
Let's run Litestream right away.
litestream replicate -config ./litestream.yml
Objects have been created in the bucket you created in MinIO.

Let's create a repository just for testing.

Stop litestream with Ctrl + C, then run the following to check the gitea.db file.
$ sqlite3 /var/lib/gitea/data/gitea.db
SQLite version 3.50.1 2025-06-06 14:52:32
Enter ".help" for usage hints.
sqlite> select * from repository;
2|1|slowhand|test|test|||0||main|main|...
The repository we just created is registered. Let's delete these files and then try restoring from the MinIO bucket.
$ rm /var/lib/gitea/data/gitea.db
$ rm /var/lib/gitea/data/gitea.db-*
$ litestream restore -if-replica-exists -o /var/lib/gitea/data/gitea.db -config ./litestream.yml /var/lib/gitea/data/gitea.db
$ sqlite3 /var/lib/gitea/data/gitea.db
SQLite version 3.50.1 2025-06-06 14:52:32
Enter ".help" for usage hints.
sqlite> select * from repository;
2|1|slowhand|test|test|||0||main|main|...
It looks like it was restored successfully. In this state, restart the container, access http://localhost:3000, and if it displays correctly as before, it's working.
In the local environment, we used volumes to persist the necessary data entirely, so restarting the container works fine. This situation changes when using Cloud Run. Next, we'll attempt to deploy to Cloud Run, which ultimately didn't work.
Deployment Preparation
To deploy to Cloud Run, we need to create a Cloud Storage bucket and a repository in Artifact Registry.
Creating a Cloud Storage Bucket
We'll create a bucket named selfhost-gitea. Adjust the bucket name as needed.
gcloud storage buckets create selfhost-gitea \
--uniform-bucket-level-access \
--location=asia-northeast1 \
--project={PROJECT_ID} # Specify your project ID
Also, let's create a bucket for volume-mounting the Gitea configuration file app.ini to Cloud Storage. The reason will be explained later. The bucket is named selfhost-gitea-config.
--add-volume=name=gitea-config,type=cloud-storage,bucket=selfhost-gitea-config \
--add-volume-mount=volume=gitea-config,mount-path=/etc/gitea
gcloud storage buckets create selfhost-gitea-config \
--uniform-bucket-level-access \
--location=asia-northeast1 \
--project={PROJECT_ID} # Specify your project ID
Creating an Artifact Registry Repository
We'll create a repository named selfhost-gitea-repository.
gcloud artifacts repositories create selfhost-gitea-repository \
--repository-format=docker \
--location=asia-northeast1 \
--description=selfhost-gitea \
--async \
--disable-vulnerability-scanning \
--project={PROJECT_ID} # Specify your project ID
Creating a Dockerfile for Deployment
Next, create a Dockerfile for the Docker image to push to selfhost-gitea-repository. The final Dockerfile is as follows.
FROM alpine AS litestream-builer
ARG LITESTREAM_VERSION=0.3.13
ENV LITESTREAM_VERSION=${LITESTREAM_VERSION}
ADD "https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-v${LITESTREAM_VERSION}-linux-amd64.tar.gz" /tmp/litestream.tar.gz
RUN tar -C /usr/local/bin -xzf /tmp/litestream.tar.gz
FROM docker.gitea.com/gitea:1.24.0-rootless
ENV GCS_BUCKET_URL=gcs://selfhost-gitea/gitea.db
COPY --from=litestream-builer /usr/local/bin/litestream /usr/local/bin/litestream
COPY docker-entrypoint.sh /
COPY litestream.yml /etc/litestream.yml
ENTRYPOINT ["/docker-entrypoint.sh"]
What this does is copy the Litestream binary prepared in the litestream-builder phase into the Gitea Docker image so it can be used.
The docker-entrypoint.sh is as follows.
#!/bin/bash
set -e
echo "starting restore"
echo "gcs url >> ${GCS_BUCKET_URL}"
if [ -f /var/lib/gitea/data/gitea.db ]; then
echo "Database already exists"
else
echo "Database does not exist. Creating..."
litestream restore -if-replica-exists -o /var/lib/gitea/data/gitea.db "${GCS_BUCKET_URL}"
fi
echo "done with restore. starting litestream"
exec litestream replicate \
-exec "/usr/local/bin/gitea -c ${GITEA_APP_INI:-/etc/gitea/app.ini} web --port ${PORT}"
The final command directly executes the gitea web command, specifying the port at that time.
The litestream.yml configuration for Cloud Storage is as follows.
dbs:
- path: /var/lib/gitea/data/gitea.db
replicas:
- url: ${GCS_BUCKET_URL}
Push the Docker Image
Now that we are ready, let's build the Docker image and push it to the Artifact Registry repository. Replace {PROJECT_ID} as needed.
# Build
$ docker build -t asia-northeast1-docker.pkg.dev/{PROJECT_ID}/selfhost-gitea-repository/gitea --platform amd64 .
# Authenticate and push
$ gcloud auth configure-docker asia-northeast1-docker.pkg.dev --project={PROJECT_ID}
$ docker push asia-northeast1-docker.pkg.dev/{PROJECT_ID}/selfhost-gitea-repository/gitea
Deploy to Cloud Run
Finally, deploy to Cloud Run.
gcloud run deploy selfhost-gitea \
--image=asia-northeast1-docker.pkg.dev/{PROJECT_ID}/selfhost-gitea-repository/gitea \
--region=asia-northeast1 \
--allow-unauthenticated \
--project={PROJECT_ID} \
--port=8080 \
--add-volume=name=gitea-config,type=cloud-storage,bucket=selfhost-gitea-config \
--add-volume-mount=volume=gitea-config,mount-path=/etc/gitea
--max-instances=1 \
--min-instances=1
Once the deployment is successful, access the generated URL. You should see the initial setup screen as you did locally. Set the database type to SQLite3, and change the site title if needed.

Keep everything else as default and click "Install Gitea".
It says "Installing, please wait..." and doesn't proceed!?

For some reason, if you don't volume-mount the app.ini via Cloud Storage, you can't proceed, and /user/login returns a 404. I couldn't find the cause even after investigating, so if anyone knows, please share!
If you do proceed, the following screen appears, and you can use Gitea normally... However, after redeploying, the necessary persistent storage was missing, so it was not usable.

Trying Cloud Storage Volume Mounts for /var/lib/gitea and /etc/gitea
Just as we volume-mounted /var/lib/gitea and /etc/gitea locally, I tried doing the same using Cloud Storage volume mounts.
Create buckets named selfhost-gitea-config and selfhost-gitea-data, and specify the volume mounts during Cloud Run deployment.
gcloud beta run deploy selfhost-gitea \
--image=asia-northeast1-docker.pkg.dev/{PROJECT_ID}/selfhost-gitea-repository/gitea \
--region=asia-northeast1 \
--allow-unauthenticated \
--project={PROJECT_ID} \
--port=8080 \
--set-env-vars=DISABLE_SSH=true \
--set-env-vars=START_SSH_SERVER=false \
--max-instances=1 \
--min-instances=1 \
--add-volume=name=gitea-config,type=cloud-storage,bucket=selfhost-gitea-config \
--add-volume-mount=volume=gitea-config,mount-path=/etc/gitea \
--add-volume=name=gitea-data,type=cloud-storage,bucket=selfhost-gitea-data,mount-options="dir-mode=777;file-mode=666" \
--add-volume-mount=volume=gitea-data,mount-path=/var/lib/gitea
For selfhost-gitea-data, I changed the permissions using mount-options. Therefore, the command uses gcloud beta run.
When I tried deploying, it failed with an error. The log showed the following error.
code.gitea.io/gitea/modules/git.InitFull(ctx) failed: failed to set git global config gc.reflogexpire, err: exit status 4 - error: chmod on /var/lib/gitea/data/home/.gitconfig.lock failed: Operation not permitted
It seems it's trying to execute chmod but failing with Operation not permitted 👀
This appears to fail because Cloud Storage FUSE does not comply with POSIX and has the following limitation.
Cloud Storage FUSE does not support file locking or file patching. Version control systems rely on file locking and patching, so do not store version control system repositories on Cloud Storage FUSE mount points.
Thus, volume mounts cannot be used for this use case.
Summary
Perhaps it's just my approach that's wrong, and with some ingenuity, it might be possible to make Gitea work on Cloud Run. However, considering all the things that need to be done, it seems best to simply use a different method to operate Gitea. 🙂
Discussion