Files
docs/docs/usage/packages/terraform-module.md
T
878c885050 docs(packages): document terraform module registry (#433)
Ref: https://github.com/go-gitea/gitea/pull/38040

Adds docs/usage/packages/terraform-module.md describing the v1 scope,
naming rules, authentication, service discovery, HTTP API surface and
[packages] settings for the new private Terraform Module Registry.

---------

Co-authored-by: silverwind <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-authored-by: Lunny Xiao <[email protected]>
Reviewed-on: https://gitea.com/gitea/docs/pulls/433
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Michael Lopez <[email protected]>
2026-11-07 11:43:22 +00:00

7.4 KiB

date, slug, sidebar_position
date slug sidebar_position
2026-06-09T00:00:00+00:00 terraform-module 119

Terraform Module Registry

Publish Terraform modules for your user or organization and use them from Terraform or Terragrunt. The registry implements the HashiCorp module registry protocol, so modules are referenced with the usual HOST/NAMESPACE/NAME/PROVIDER address.

Requirements

  • The Terraform CLI or Terragrunt.
  • Your Gitea instance must be served over HTTPS with a valid certificate. Terraform and Terragrunt only perform module registry discovery over HTTPS and reject plain HTTP — including http://localhost. This is a requirement of the protocol, not of Gitea.

A module is addressed by three segments:

Segment Meaning
namespace The owner of the module: a Gitea user or organization name. It must follow the same rules as name, so owners with a . in their name cannot publish modules.
name The module name. Letters, digits, - and _, starting and ending with a letter or digit, up to 64 characters.
provider The main provider the module targets, e.g. aws. Lowercase letters and digits, up to 64 characters.

Versions follow semantic versioning and are stored in canonical form: a leading v is dropped and missing segments are padded, so v1.2.3 and 1.2.3 address the same version, and 1.2 becomes 1.2.0.

Packaging a module

A module is published as a single .tar.gz archive, stored and served exactly as uploaded. The module's .tf files must therefore be at the archive root. A source tarball downloaded from a Git host wraps everything in a repository-version/ directory and is rejected — repackage the module directory's contents instead:

tar -czf vpc-1.0.0.tar.gz -C path/to/module .

The root module's inputs, outputs, providers and README are shown on the package page. Each submodule found under modules/<name> is listed with its source address and links to its own page, which shows the install snippet for that submodule along with its inputs, outputs, providers and README. Only one level is indexed, so modules/<name>/<nested> is not listed. Anything else, including examples/, is kept in the archive but is not indexed. A module that is only a collection of submodules, with no root module, is also accepted. Files in JSON syntax (.tf.json) are accepted too, but only .tf files are indexed.

Authentication

Create a personal access token with the read:package scope to download private modules, and write:package to publish or delete them. Add it to your Terraform CLI configuration file (~/.terraformrc or %APPDATA%/terraform.rc):

credentials "gitea.example.com" {
  token = "<your token>"
}

Modules owned by a public user or organization can be downloaded without a token.

Publishing a module

Upload an archive with a PUT request, sending the file as the raw request body. Use curl --upload-file rather than --data-binary: the latter sends a form content type that the registry will not accept as an archive.

curl --user "your_username:your_token" \
  --upload-file vpc-1.0.0.tar.gz \
  https://gitea.example.com/api/packages/-/terraform/modules/{namespace}/{name}/{provider}/{version}
Parameter Description
namespace The owner (user or organization).
name The module name.
provider The provider the module targets.
version The module version (semver).

For example, to publish version 1.0.0 of the vpc module for the aws provider under the acme organization:

curl --user "your_username:your_token" \
  --upload-file vpc-1.0.0.tar.gz \
  https://gitea.example.com/api/packages/-/terraform/modules/acme/vpc/aws/1.0.0

A successful upload returns 201 Created. Published versions are immutable: uploading a version that already exists returns 409 Conflict. To replace a version, delete it first.

Linking a module to a repository

A module always lives in a user or organization's package space — its namespace is that user or organization, never a repository, because the registry address (HOST/NAMESPACE/NAME/PROVIDER) has no repository segment. You cannot publish "into" a repository. Instead, a repository link is a display association that surfaces the module on a repository's Packages tab. The workflow is:

  1. Publish the module to the user or organization namespace, exactly as shown in Publishing a module.
  2. Link the package to a repository owned by that same user or organization: open the package, go to its Settings, and select the repository (or use the POST /api/v1/packages/{owner}/terraform-module/{name}%2F{provider}/-/link/{repo} API).

The link applies to the whole module — all of its versions, including ones published later — and can be changed or removed at any time. It only affects where the module is listed in the UI; the address used to consume it is unchanged (still HOST/NAMESPACE/NAME/PROVIDER, with no repository in it).

Consuming a module

Reference the module by its registry address. With Terraform:

module "vpc" {
  source  = "gitea.example.com/acme/vpc/aws"
  version = "~> 1.0"
}

To use a submodule, append its path with the standard // separator:

module "network" {
  source  = "gitea.example.com/acme/infra/google//modules/network"
  version = "~> 1.0"
}

With Terragrunt, use the tfr:// scheme and pass the version as a query parameter:

terraform {
  source = "tfr://gitea.example.com/acme/vpc/aws?version=1.0.0"
}

terraform init (or terragrunt init) resolves the address through the service discovery document at https://gitea.example.com/.well-known/terraform.json and downloads the matching version.

Deleting a version

curl --user "your_username:your_token" \
  --request DELETE \
  https://gitea.example.com/api/packages/-/terraform/modules/acme/vpc/aws/1.0.0

A successful delete returns 204 No Content. This requires the write:package scope. There is no separate deprecation flow — deleting a version removes it from the registry.

Limits

The stored archive is bounded by the instance-wide package limits (LIMIT_SIZE_TERRAFORM_MODULE and LIMIT_TOTAL_OWNER_SIZE) set by your Gitea administrator; see the configuration cheat sheet.

Independently of those settings, an archive may not exceed 32 MiB decompressed. The registry reads the whole archive to index it, and stops with 413 Request Entity Too Large past that ceiling, so raising LIMIT_SIZE_TERRAFORM_MODULE above 32 MiB does not allow larger modules. Real modules are kilobyte-scale, so this only affects archives carrying unrelated payloads.

A README larger than 1 MiB, of the root module or of a submodule, is not shown on the package page. The module is still accepted and the file stays in the archive.