iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
😩

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?

https://about.gitea.com/products/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?

https://litestream.io/

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.

https://docs.gitea.com/installation/install-with-docker-rootless

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.

image1.png
For now, select "Install Gitea" with the default settings.

Then create an account and log in. The following screen will be displayed.

image2.png

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.

https://min.io/

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".

image3.png

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

image4.png

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.

image5.png

Let's create a repository just for testing.

image6.png

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.

image7.png

Keep everything else as default and click "Install Gitea".

It says "Installing, please wait..." and doesn't proceed!?

image8.png

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.

image9.png


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.

https://cloud.google.com/storage/docs/gcs-fuse?hl=ja#expandable-1

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