Part 3 of 5

    Self-Hosted Package Registries with Gitea

    Gitea ships with a built-in package registry supporting 20+ formats. No extra containers, no per-package fees.

    Prerequisites: A running Gitea instance (Part 1). You do not need the CI setup from Part 2 to use the registry, but the two work well together when your pipelines publish to the registry on every merge.

    Gitea Package Registry Overview

    The package registry is enabled by default in Gitea 1.17+. All packages are stored under the same user or organization namespace as your repositories. Permissions are inherited — a user with write access to a repo can publish packages to that org's registry.

    Registry TypeUse CaseClient ToolSupport
    Container / OCIDocker images, Helm chartsdocker, helmFull
    npmNode.js packagesnpm, yarn, pnpmFull
    PyPIPython packagespip, poetry, twineFull
    MavenJava / Kotlin packagesmvn, gradleFull
    NuGetC# / .NET packagesdotnet, nugetFull
    CargoRust cratescargoFull
    Go ModulesGo packagesgo getFull
    PubDart / Flutter packagesdart pubFull
    1

    Enabling the Package Registry

    Verify the registry is enabled in your app.ini:

    Check and enable registry
    # Check current state
    grep -i 'ENABLED' /opt/gitea/config/app.ini | head -5
    
    # If not present, add to app.ini:
    [packages]
    ENABLED = true
    
    # Restart to apply
    cd /opt/gitea && docker compose restart gitea

    Storage: Packages are stored in Gitea's data directory. Plan for roughly 2x your repository storage for packages, or mount an additional volume to /opt/gitea/data/packages.

    2

    Docker / OCI Container Registry

    Logging In

    The container registry is compatible with any OCI-compliant client (Docker, Podman, Buildah, Skopeo). Authenticate using your Gitea username and a personal access token:

    Docker login
    # Generate a PAT in Gitea: User Settings > Applications > Access Tokens
    # Scope: package:read, package:write
    
    docker login git.yourdomain.com \
      --username your_gitea_username \
      --password YOUR_ACCESS_TOKEN

    Pushing Docker Images

    Build, tag, and push
    # Build and tag
    docker build -t git.yourdomain.com/your-org/myapp:1.0.0 .
    docker tag git.yourdomain.com/your-org/myapp:1.0.0 git.yourdomain.com/your-org/myapp:latest
    
    # Push both tags
    docker push git.yourdomain.com/your-org/myapp:1.0.0
    docker push git.yourdomain.com/your-org/myapp:latest
    
    # Verify it appears in Gitea
    # Browse to: https://git.yourdomain.com/your-org/-/packages

    Pulling in Docker Compose

    docker-compose.yml — pull from Gitea
    # docker-compose.yml on your production server
    version: '3.8'
    services:
      myapp:
        image: git.yourdomain.com/your-org/myapp:latest
        # Ensure the host is logged in or use a registry auth config:
        # docker login git.yourdomain.com before running compose

    Configuring Kubernetes / k3s to Pull from Gitea

    Create a registry pull secret
    # Create a registry pull secret
    kubectl create secret docker-registry gitea-registry \
      --docker-server=git.yourdomain.com \
      --docker-username=your_gitea_user \
      --docker-password=YOUR_ACCESS_TOKEN \
      --namespace=your-namespace
    
    # Reference in a deployment
    spec:
      imagePullSecrets:
        - name: gitea-registry
      containers:
        - image: git.yourdomain.com/your-org/myapp:latest

    k3s users: You can also configure a registry mirror in /etc/rancher/k3s/registries.yaml to avoid needing pull secrets in every deployment. See Part 4 for integrating Gitea registry pulls with Dokploy and Coolify auto-deployments.

    3

    npm Package Registry

    Configuration

    Configure your npm client to use Gitea as a scoped registry. Packages under @your-scope will resolve from Gitea, while all others still come from the public npm registry:

    Configure npm registry
    # Set the registry for a scope
    npm config set @your-scope:registry https://git.yourdomain.com/api/packages/your-org/npm/
    
    # Authenticate
    npm config set //git.yourdomain.com/api/packages/your-org/npm/:_authToken YOUR_ACCESS_TOKEN
    
    # Or use .npmrc in the project root
    @your-scope:registry=https://git.yourdomain.com/api/packages/your-org/npm/
    //git.yourdomain.com/api/packages/your-org/npm/:_authToken=${GITEA_TOKEN}

    Publishing a Package

    package.json and publish
    // package.json
    {
      "name": "@your-scope/my-library",
      "version": "1.0.0",
      "publishConfig": {
        "registry": "https://git.yourdomain.com/api/packages/your-org/npm/"
      }
    }
    
    // Publish
    // npm publish
    
    // Install in another project
    // npm install @your-scope/my-library

    Publishing from CI (Gitea Actions)

    .gitea/workflows/publish.yml
    name: Publish npm Package
    on:
      push:
        tags: ['v*']
    
    jobs:
      publish:
        runs-on: ubuntu-22.04
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: '20'
          - run: npm ci
          - run: npm run build
          - run: npm publish
            env:
              NODE_AUTH_TOKEN: ${{ secrets.GITEA_PACKAGE_TOKEN }}
    4

    PyPI Package Registry

    Configuration

    pip.conf — Configure pip
    # ~/.pip/pip.conf (or /etc/pip.conf for system-wide)
    [global]
    index-url = https://git.yourdomain.com/api/packages/your-org/pypi/simple/
    trusted-host = git.yourdomain.com
    
    # For projects that also need public packages, use extra-index-url instead:
    [global]
    index-url = https://pypi.org/simple/
    extra-index-url = https://git.yourdomain.com/api/packages/your-org/pypi/simple/
    trusted-host = git.yourdomain.com

    Uploading with Twine

    Build and upload with twine
    # Install build tools
    pip install build twine
    
    # Build the distribution
    python -m build
    
    # Upload to Gitea
    twine upload \
      --repository-url https://git.yourdomain.com/api/packages/your-org/pypi/ \
      --username your_gitea_username \
      --password YOUR_ACCESS_TOKEN \
      dist/*

    Using Poetry

    Poetry integration
    # Add Gitea as a poetry source
    poetry source add gitea https://git.yourdomain.com/api/packages/your-org/pypi/simple/ --priority supplemental
    
    # Configure credentials
    poetry config http-basic.gitea your_gitea_username YOUR_ACCESS_TOKEN
    
    # Publish
    poetry publish --repository gitea
    
    # Install private packages
    poetry add --source gitea your-private-package
    5

    Helm Chart Registry

    Gitea's OCI registry also works as a Helm chart repository, which is the modern approach versus the older chart museum format:

    Helm chart publishing
    # Package your chart
    helm package ./my-chart
    
    # Login to Gitea OCI registry
    helm registry login git.yourdomain.com \
      --username your_gitea_username \
      --password YOUR_ACCESS_TOKEN
    
    # Push the chart
    helm push my-chart-1.0.0.tgz oci://git.yourdomain.com/your-org/helm-charts
    
    # Install from Gitea
    helm install my-release oci://git.yourdomain.com/your-org/helm-charts/my-chart --version 1.0.0
    6

    Storage Management & Cleanup

    Monitoring Package Storage

    Check storage usage
    # Check how much space packages are using
    du -sh /opt/gitea/data/packages/
    
    # Breakdown by type
    du -sh /opt/gitea/data/packages/*/

    Cleanup Script

    Gitea does not yet have automated cleanup policies (as of 1.22), so you'll need a cron job to remove old package versions:

    /opt/gitea/cleanup-packages.sh
    #!/bin/bash
    # Delete package versions older than 90 days via Gitea API
    
    GITEA_URL='https://git.yourdomain.com'
    GITEA_TOKEN='YOUR_ADMIN_TOKEN'
    ORG='your-org'
    
    # List all packages
    curl -s -H "Authorization: token $GITEA_TOKEN" \
      "$GITEA_URL/api/v1/packages/$ORG?limit=50" | \
      jq -r '.[] | select(.created_unix < (now - 7776000)) | .id' | \
      while read pkg_id; do
        echo "Deleting old package version: $pkg_id"
        curl -s -X DELETE -H "Authorization: token $GITEA_TOKEN" \
          "$GITEA_URL/api/v1/packages/$ORG/container/$pkg_id"
      done

    Tip: Mount a dedicated volume for /opt/gitea/data/packages if you expect large Docker image storage. A RamNode VPS can have additional SSD storage added without reinstalling the OS. Run df -h to check current disk usage before enabling Docker registry pushes from CI.

    What's Next

    Part 4 covers auto-deploying from Gitea using Dokploy and Coolify. When you push code, your CI pipeline builds and publishes a new Docker image to the Gitea registry, which then triggers an automatic redeploy on your PaaS platform.