diff --git a/docs/content/doc/packages/helm.en-us.md b/docs/content/doc/packages/helm.en-us.md
new file mode 100644
index 0000000000..9c43b08bf4
--- /dev/null
+++ b/docs/content/doc/packages/helm.en-us.md
@@ -0,0 +1,67 @@
+---
+date: "2022-04-14T00:00:00+00:00"
+title: "Helm Chart Registry"
+slug: "packages/helm"
+draft: false
+toc: false
+menu:
+  sidebar:
+    parent: "packages"
+    name: "Helm"
+    weight: 50
+    identifier: "helm"
+---
+
+# Helm Chart Registry
+
+Publish [Helm](https://helm.sh/) charts for your user or organization.
+
+**Table of Contents**
+
+{{< toc >}}
+
+## Requirements
+
+To work with the Helm Chart registry use a simple HTTP client like `curl` or the [`helm cm-push`](https://github.com/chartmuseum/helm-push/) plugin.
+
+## Publish a package
+
+Publish a package by running the following command:
+
+```shell
+curl --user {username}:{password} -X POST --upload-file ./{chart_file}.tgz https://gitea.example.com/api/packages/{owner}/helm/api/charts
+```
+
+or with the `helm cm-push` plugin:
+
+```shell
+helm repo add  --username {username} --password {password} {repo} https://gitea.example.com/api/packages/{owner}/helm
+helm cm-push ./{chart_file}.tgz {repo}
+```
+
+| Parameter    | Description |
+| ------------ | ----------- |
+| `username`   | Your Gitea username. |
+| `password`   | Your Gitea password or a personal access token. |
+| `repo`       | The name for the repository. |
+| `chart_file` | The Helm Chart archive. |
+| `owner`      | The owner of the package. |
+
+## Install a package
+
+To install a Helm char from the registry, execute the following command:
+
+```shell
+helm repo add  --username {username} --password {password} {repo} https://gitea.example.com/api/packages/{owner}/helm
+helm repo update
+helm install {name} {repo}/{chart}
+```
+
+| Parameter  | Description |
+| ---------- | ----------- |
+| `username` | Your Gitea username. |
+| `password` | Your Gitea password or a personal access token. |
+| `repo`     | The name for the repository. |
+| `owner`    | The owner of the package. |
+| `name`     | The local name. |
+| `chart`    | The name Helm Chart. |
diff --git a/docs/content/doc/packages/maven.en-us.md b/docs/content/doc/packages/maven.en-us.md
index 78288a9e42..837c8434ae 100644
--- a/docs/content/doc/packages/maven.en-us.md
+++ b/docs/content/doc/packages/maven.en-us.md
@@ -8,7 +8,7 @@ menu:
   sidebar:
     parent: "packages"
     name: "Maven"
-    weight: 50
+    weight: 60
     identifier: "maven"
 ---
 
diff --git a/docs/content/doc/packages/npm.en-us.md b/docs/content/doc/packages/npm.en-us.md
index 28b7cb8827..9ab4ac900c 100644
--- a/docs/content/doc/packages/npm.en-us.md
+++ b/docs/content/doc/packages/npm.en-us.md
@@ -8,7 +8,7 @@ menu:
   sidebar:
     parent: "packages"
     name: "npm"
-    weight: 60
+    weight: 70
     identifier: "npm"
 ---
 
diff --git a/docs/content/doc/packages/nuget.en-us.md b/docs/content/doc/packages/nuget.en-us.md
index 5565bf5b89..0b92d85a3d 100644
--- a/docs/content/doc/packages/nuget.en-us.md
+++ b/docs/content/doc/packages/nuget.en-us.md
@@ -8,7 +8,7 @@ menu:
   sidebar:
     parent: "packages"
     name: "NuGet"
-    weight: 70
+    weight: 80
     identifier: "nuget"
 ---
 
diff --git a/docs/content/doc/packages/overview.en-us.md b/docs/content/doc/packages/overview.en-us.md
index 1e4209930f..10f2184bc9 100644
--- a/docs/content/doc/packages/overview.en-us.md
+++ b/docs/content/doc/packages/overview.en-us.md
@@ -30,6 +30,7 @@ The following package managers are currently supported:
 | [Conan]({{< relref "doc/packages/conan.en-us.md" >}}) | C++ | `conan` |
 | [Container]({{< relref "doc/packages/container.en-us.md" >}}) | - | any OCI compliant client |
 | [Generic]({{< relref "doc/packages/generic.en-us.md" >}}) | - | any HTTP client |
+| [Helm]({{< relref "doc/packages/helm.en-us.md" >}}) | - | any HTTP client, `cm-push` |
 | [Maven]({{< relref "doc/packages/maven.en-us.md" >}}) | Java | `mvn`, `gradle` |
 | [npm]({{< relref "doc/packages/npm.en-us.md" >}}) | JavaScript | `npm`, `yarn` |
 | [NuGet]({{< relref "doc/packages/nuget.en-us.md" >}}) | .NET | `nuget` |
diff --git a/docs/content/doc/packages/pypi.en-us.md b/docs/content/doc/packages/pypi.en-us.md
index 1d7a8f22e8..d9f4872dca 100644
--- a/docs/content/doc/packages/pypi.en-us.md
+++ b/docs/content/doc/packages/pypi.en-us.md
@@ -8,7 +8,7 @@ menu:
   sidebar:
     parent: "packages"
     name: "PyPI"
-    weight: 80
+    weight: 90
     identifier: "pypi"
 ---
 
diff --git a/docs/content/doc/packages/rubygems.en-us.md b/docs/content/doc/packages/rubygems.en-us.md
index 603e925e32..9d9ce09b1c 100644
--- a/docs/content/doc/packages/rubygems.en-us.md
+++ b/docs/content/doc/packages/rubygems.en-us.md
@@ -8,7 +8,7 @@ menu:
   sidebar:
     parent: "packages"
     name: "RubyGems"
-    weight: 90
+    weight: 100
     identifier: "rubygems"
 ---
 
diff --git a/integrations/api_packages_helm_test.go b/integrations/api_packages_helm_test.go
new file mode 100644
index 0000000000..fcf5d2f762
--- /dev/null
+++ b/integrations/api_packages_helm_test.go
@@ -0,0 +1,166 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+	"archive/tar"
+	"bytes"
+	"compress/gzip"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/packages"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	helm_module "code.gitea.io/gitea/modules/packages/helm"
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/stretchr/testify/assert"
+	"gopkg.in/yaml.v2"
+)
+
+func TestPackageHelm(t *testing.T) {
+	defer prepareTestEnv(t)()
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+
+	packageName := "test-chart"
+	packageVersion := "1.0.3"
+	packageAuthor := "KN4CK3R"
+	packageDescription := "Gitea Test Package"
+
+	filename := fmt.Sprintf("%s-%s.tgz", packageName, packageVersion)
+
+	chartContent := `apiVersion: v2
+description: ` + packageDescription + `
+name: ` + packageName + `
+type: application
+version: ` + packageVersion + `
+maintainers:
+- name: ` + packageAuthor + `
+dependencies:
+- name: dep1
+  repository: https://example.com/
+  version: 1.0.0`
+
+	var buf bytes.Buffer
+	zw := gzip.NewWriter(&buf)
+	archive := tar.NewWriter(zw)
+	archive.WriteHeader(&tar.Header{
+		Name: fmt.Sprintf("%s/Chart.yaml", packageName),
+		Mode: 0o600,
+		Size: int64(len(chartContent)),
+	})
+	archive.Write([]byte(chartContent))
+	archive.Close()
+	zw.Close()
+	content := buf.Bytes()
+
+	url := fmt.Sprintf("/api/packages/%s/helm", user.Name)
+
+	t.Run("Upload", func(t *testing.T) {
+		defer PrintCurrentTest(t)()
+
+		uploadURL := url + "/api/charts"
+
+		req := NewRequestWithBody(t, "POST", uploadURL, bytes.NewReader(content))
+		req = AddBasicAuthHeader(req, user.Name)
+		MakeRequest(t, req, http.StatusCreated)
+
+		pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeHelm)
+		assert.NoError(t, err)
+		assert.Len(t, pvs, 1)
+
+		pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+		assert.NoError(t, err)
+		assert.NotNil(t, pd.SemVer)
+		assert.IsType(t, &helm_module.Metadata{}, pd.Metadata)
+		assert.Equal(t, packageName, pd.Package.Name)
+		assert.Equal(t, packageVersion, pd.Version.Version)
+
+		pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+		assert.NoError(t, err)
+		assert.Len(t, pfs, 1)
+		assert.Equal(t, filename, pfs[0].Name)
+		assert.True(t, pfs[0].IsLead)
+
+		pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+		assert.NoError(t, err)
+		assert.Equal(t, int64(len(content)), pb.Size)
+
+		req = NewRequestWithBody(t, "POST", uploadURL, bytes.NewReader(content))
+		req = AddBasicAuthHeader(req, user.Name)
+		MakeRequest(t, req, http.StatusCreated)
+	})
+
+	t.Run("Download", func(t *testing.T) {
+		defer PrintCurrentTest(t)()
+
+		checkDownloadCount := func(count int64) {
+			pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeHelm)
+			assert.NoError(t, err)
+			assert.Len(t, pvs, 1)
+			assert.Equal(t, count, pvs[0].DownloadCount)
+		}
+
+		checkDownloadCount(0)
+
+		req := NewRequest(t, "GET", fmt.Sprintf("%s/%s", url, filename))
+		req = AddBasicAuthHeader(req, user.Name)
+		resp := MakeRequest(t, req, http.StatusOK)
+
+		assert.Equal(t, content, resp.Body.Bytes())
+
+		checkDownloadCount(1)
+	})
+
+	t.Run("Index", func(t *testing.T) {
+		defer PrintCurrentTest(t)()
+
+		req := NewRequest(t, "GET", fmt.Sprintf("%s/index.yaml", url))
+		req = AddBasicAuthHeader(req, user.Name)
+		resp := MakeRequest(t, req, http.StatusOK)
+
+		type ChartVersion struct {
+			helm_module.Metadata `yaml:",inline"`
+			URLs                 []string  `yaml:"urls"`
+			Created              time.Time `yaml:"created,omitempty"`
+			Removed              bool      `yaml:"removed,omitempty"`
+			Digest               string    `yaml:"digest,omitempty"`
+		}
+
+		type ServerInfo struct {
+			ContextPath string `yaml:"contextPath,omitempty"`
+		}
+
+		type Index struct {
+			APIVersion string                     `yaml:"apiVersion"`
+			Entries    map[string][]*ChartVersion `yaml:"entries"`
+			Generated  time.Time                  `yaml:"generated,omitempty"`
+			ServerInfo *ServerInfo                `yaml:"serverInfo,omitempty"`
+		}
+
+		var result Index
+		assert.NoError(t, yaml.NewDecoder(resp.Body).Decode(&result))
+		assert.NotEmpty(t, result.Entries)
+		assert.Contains(t, result.Entries, packageName)
+
+		cvs := result.Entries[packageName]
+		assert.Len(t, cvs, 1)
+
+		cv := cvs[0]
+		assert.Equal(t, packageName, cv.Name)
+		assert.Equal(t, packageVersion, cv.Version)
+		assert.Equal(t, packageDescription, cv.Description)
+		assert.Len(t, cv.Maintainers, 1)
+		assert.Equal(t, packageAuthor, cv.Maintainers[0].Name)
+		assert.Len(t, cv.Dependencies, 1)
+		assert.ElementsMatch(t, []string{fmt.Sprintf("%s%s/%s", setting.AppURL, url[1:], filename)}, cv.URLs)
+
+		assert.Equal(t, url, result.ServerInfo.ContextPath)
+	})
+}
diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go
index 3249260f80..fbdc40f37f 100644
--- a/models/packages/descriptor.go
+++ b/models/packages/descriptor.go
@@ -15,6 +15,7 @@ import (
 	"code.gitea.io/gitea/modules/packages/composer"
 	"code.gitea.io/gitea/modules/packages/conan"
 	"code.gitea.io/gitea/modules/packages/container"
+	"code.gitea.io/gitea/modules/packages/helm"
 	"code.gitea.io/gitea/modules/packages/maven"
 	"code.gitea.io/gitea/modules/packages/npm"
 	"code.gitea.io/gitea/modules/packages/nuget"
@@ -129,6 +130,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
 		metadata = &container.Metadata{}
 	case TypeGeneric:
 		// generic packages have no metadata
+	case TypeHelm:
+		metadata = &helm.Metadata{}
 	case TypeNuGet:
 		metadata = &nuget.Metadata{}
 	case TypeNpm:
diff --git a/models/packages/package.go b/models/packages/package.go
index 373bd86d9f..bdb535492b 100644
--- a/models/packages/package.go
+++ b/models/packages/package.go
@@ -35,9 +35,10 @@ const (
 	TypeConan     Type = "conan"
 	TypeContainer Type = "container"
 	TypeGeneric   Type = "generic"
-	TypeNuGet     Type = "nuget"
-	TypeNpm       Type = "npm"
+	TypeHelm      Type = "helm"
 	TypeMaven     Type = "maven"
+	TypeNpm       Type = "npm"
+	TypeNuGet     Type = "nuget"
 	TypePyPI      Type = "pypi"
 	TypeRubyGems  Type = "rubygems"
 )
@@ -53,12 +54,14 @@ func (pt Type) Name() string {
 		return "Container"
 	case TypeGeneric:
 		return "Generic"
-	case TypeNuGet:
-		return "NuGet"
-	case TypeNpm:
-		return "npm"
+	case TypeHelm:
+		return "Helm"
 	case TypeMaven:
 		return "Maven"
+	case TypeNpm:
+		return "npm"
+	case TypeNuGet:
+		return "NuGet"
 	case TypePyPI:
 		return "PyPI"
 	case TypeRubyGems:
@@ -78,12 +81,14 @@ func (pt Type) SVGName() string {
 		return "octicon-container"
 	case TypeGeneric:
 		return "octicon-package"
-	case TypeNuGet:
-		return "gitea-nuget"
-	case TypeNpm:
-		return "gitea-npm"
+	case TypeHelm:
+		return "gitea-helm"
 	case TypeMaven:
 		return "gitea-maven"
+	case TypeNpm:
+		return "gitea-npm"
+	case TypeNuGet:
+		return "gitea-nuget"
 	case TypePyPI:
 		return "gitea-python"
 	case TypeRubyGems:
diff --git a/modules/packages/helm/metadata.go b/modules/packages/helm/metadata.go
new file mode 100644
index 0000000000..9517448ca6
--- /dev/null
+++ b/modules/packages/helm/metadata.go
@@ -0,0 +1,131 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package helm
+
+import (
+	"archive/tar"
+	"compress/gzip"
+	"errors"
+	"io"
+	"strings"
+
+	"code.gitea.io/gitea/modules/validation"
+
+	"github.com/hashicorp/go-version"
+	"gopkg.in/yaml.v2"
+)
+
+var (
+	// ErrMissingChartFile indicates a missing Chart.yaml file
+	ErrMissingChartFile = errors.New("Chart.yaml file is missing")
+	// ErrInvalidName indicates an invalid package name
+	ErrInvalidName = errors.New("package name is invalid")
+	// ErrInvalidVersion indicates an invalid package version
+	ErrInvalidVersion = errors.New("package version is invalid")
+	// ErrInvalidChart indicates an invalid chart
+	ErrInvalidChart = errors.New("chart is invalid")
+)
+
+// Metadata for a Chart file. This models the structure of a Chart.yaml file.
+type Metadata struct {
+	APIVersion   string            `json:"api_version" yaml:"apiVersion"`
+	Type         string            `json:"type,omitempty" yaml:"type,omitempty"`
+	Name         string            `json:"name" yaml:"name"`
+	Version      string            `json:"version" yaml:"version"`
+	AppVersion   string            `json:"app_version,omitempty" yaml:"appVersion,omitempty"`
+	Home         string            `json:"home,omitempty" yaml:"home,omitempty"`
+	Sources      []string          `json:"sources,omitempty" yaml:"sources,omitempty"`
+	Description  string            `json:"description,omitempty" yaml:"description,omitempty"`
+	Keywords     []string          `json:"keywords,omitempty" yaml:"keywords,omitempty"`
+	Maintainers  []*Maintainer     `json:"maintainers,omitempty" yaml:"maintainers,omitempty"`
+	Icon         string            `json:"icon,omitempty" yaml:"icon,omitempty"`
+	Condition    string            `json:"condition,omitempty" yaml:"condition,omitempty"`
+	Tags         string            `json:"tags,omitempty" yaml:"tags,omitempty"`
+	Deprecated   bool              `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
+	Annotations  map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
+	KubeVersion  string            `json:"kube_version,omitempty" yaml:"kubeVersion,omitempty"`
+	Dependencies []*Dependency     `json:"dependencies,omitempty" yaml:"dependencies,omitempty"`
+}
+
+type Maintainer struct {
+	Name  string `json:"name,omitempty" yaml:"name,omitempty"`
+	Email string `json:"email,omitempty" yaml:"email,omitempty"`
+	URL   string `json:"url,omitempty" yaml:"url,omitempty"`
+}
+
+type Dependency struct {
+	Name         string        `json:"name" yaml:"name"`
+	Version      string        `json:"version,omitempty" yaml:"version,omitempty"`
+	Repository   string        `json:"repository" yaml:"repository"`
+	Condition    string        `json:"condition,omitempty" yaml:"condition,omitempty"`
+	Tags         []string      `json:"tags,omitempty" yaml:"tags,omitempty"`
+	Enabled      bool          `json:"enabled,omitempty" yaml:"enabled,omitempty"`
+	ImportValues []interface{} `json:"import_values,omitempty" yaml:"import-values,omitempty"`
+	Alias        string        `json:"alias,omitempty" yaml:"alias,omitempty"`
+}
+
+// ParseChartArchive parses the metadata of a Helm archive
+func ParseChartArchive(r io.Reader) (*Metadata, error) {
+	gzr, err := gzip.NewReader(r)
+	if err != nil {
+		return nil, err
+	}
+	defer gzr.Close()
+
+	tr := tar.NewReader(gzr)
+	for {
+		hd, err := tr.Next()
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			return nil, err
+		}
+
+		if hd.Typeflag != tar.TypeReg {
+			continue
+		}
+
+		if hd.FileInfo().Name() == "Chart.yaml" {
+			if strings.Count(hd.Name, "/") != 1 {
+				continue
+			}
+
+			return ParseChartFile(tr)
+		}
+	}
+
+	return nil, ErrMissingChartFile
+}
+
+// ParseChartFile parses a Chart.yaml file to retrieve the metadata of a Helm chart
+func ParseChartFile(r io.Reader) (*Metadata, error) {
+	var metadata *Metadata
+	if err := yaml.NewDecoder(r).Decode(&metadata); err != nil {
+		return nil, err
+	}
+
+	if metadata.APIVersion == "" {
+		return nil, ErrInvalidChart
+	}
+
+	if metadata.Type != "" && metadata.Type != "application" && metadata.Type != "library" {
+		return nil, ErrInvalidChart
+	}
+
+	if metadata.Name == "" {
+		return nil, ErrInvalidName
+	}
+
+	if _, err := version.NewSemver(metadata.Version); err != nil {
+		return nil, ErrInvalidVersion
+	}
+
+	if !validation.IsValidURL(metadata.Home) {
+		metadata.Home = ""
+	}
+
+	return metadata, nil
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 5662ed2c40..21bf0c49ea 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3051,6 +3051,9 @@ container.labels.key = Key
 container.labels.value = Value
 generic.download = Download package from the command line:
 generic.documentation = For more information on the generic registry, see <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/generic">the documentation</a>.
+helm.registry = Setup this registry from the command line:
+helm.install = To install the package, run the following command:
+helm.documentation = For more information on the Helm registry, see <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/helm/">the documentation</a>.
 maven.registry = Setup this registry in your project <code>pom.xml</code> file:
 maven.install = To use the package include the following in the <code>dependencies</code> block in the <code>pom.xml</code> file:
 maven.install2 = Run via command line:
diff --git a/public/img/svg/gitea-helm.svg b/public/img/svg/gitea-helm.svg
new file mode 100644
index 0000000000..5ab50dd29e
--- /dev/null
+++ b/public/img/svg/gitea-helm.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 62 65" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" class="svg gitea-helm" width="16" height="16" aria-hidden="true"><path fill="#3f7a9c" d="m41.868 16.659.248.19a19.17 19.17 0 0 1 3.919 4.128l.9 1.414 2.774-1.601-1.05-1.65a22.373 22.373 0 0 0-3.304-3.748 17.143 17.143 0 0 0 2.215-2.403c2.158-2.813 3.141-5.657 2.204-6.376s-3.43.966-5.589 3.779a17.048 17.048 0 0 0-1.748 2.762 22.297 22.297 0 0 0-10.308-3.48c.189-.957.298-2.076.298-3.273 0-3.546-.951-6.4-2.133-6.4S28.16 2.854 28.16 6.4c0 1.198.109 2.317.298 3.273a22.293 22.293 0 0 0-10.308 3.48 17.117 17.117 0 0 0-1.748-2.762c-2.158-2.813-4.651-4.498-5.589-3.779s.045 3.563 2.204 6.376a17.203 17.203 0 0 0 2.215 2.403 22.373 22.373 0 0 0-3.304 3.748 22.33 22.33 0 0 0-1.05 1.65l2.774 1.601a19.13 19.13 0 0 1 .9-1.414 19.21 19.21 0 0 1 3.919-4.128l.248-.19c3.214-2.424 7.221-3.859 11.574-3.859s8.36 1.435 11.574 3.859zM14.551 43.023A19.15 19.15 0 0 0 30.293 51.2a19.15 19.15 0 0 0 15.742-8.177l2.624 1.837a22.373 22.373 0 0 1-3.304 3.748 17.143 17.143 0 0 1 2.215 2.403c2.158 2.813 3.141 5.657 2.204 6.376s-3.43-.966-5.589-3.779a17.048 17.048 0 0 1-1.748-2.762 22.297 22.297 0 0 1-10.308 3.48c.189.957.298 2.076.298 3.273 0 3.546-.951 6.4-2.133 6.4s-2.133-2.854-2.133-6.4c0-1.198.109-2.317.298-3.273a22.293 22.293 0 0 1-10.308-3.48 17.117 17.117 0 0 1-1.748 2.762c-2.158 2.813-4.651 4.498-5.589 3.779s.045-3.563 2.204-6.376a17.203 17.203 0 0 1 2.215-2.403 22.373 22.373 0 0 1-3.304-3.748zm30.249-2.49V24.32h4.693l3.413 9.813 3.413-9.813h4.267v16.213h-3.84v-5.12l.853-5.973-3.84 9.813h-2.133l-3.84-9.813.853 5.973v5.12zM31.147 24.32v16.213h10.667V37.12h-6.4v-12.8zm-14.08 0v16.213H28.16V37.12h-6.827v-2.987h5.547V30.72h-5.547v-2.987h6.4V24.32zm-12.8 16.213v-6.4h5.12v6.4h4.267V24.32H9.387v5.973h-5.12V24.32H0v16.213z" stroke="none"/></svg>
\ No newline at end of file
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index f0251b95eb..b5fdc739d7 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -17,6 +17,7 @@ import (
 	"code.gitea.io/gitea/routers/api/packages/conan"
 	"code.gitea.io/gitea/routers/api/packages/container"
 	"code.gitea.io/gitea/routers/api/packages/generic"
+	"code.gitea.io/gitea/routers/api/packages/helm"
 	"code.gitea.io/gitea/routers/api/packages/maven"
 	"code.gitea.io/gitea/routers/api/packages/npm"
 	"code.gitea.io/gitea/routers/api/packages/nuget"
@@ -162,6 +163,11 @@ func Routes() *web.Route {
 				}, reqPackageAccess(perm.AccessModeWrite))
 			})
 		})
+		r.Group("/helm", func() {
+			r.Get("/index.yaml", helm.Index)
+			r.Get("/{filename}", helm.DownloadPackageFile)
+			r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), helm.UploadPackage)
+		})
 		r.Group("/maven", func() {
 			r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile)
 			r.Get("/*", maven.DownloadPackageFile)
diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go
new file mode 100644
index 0000000000..ae0643a35a
--- /dev/null
+++ b/routers/api/packages/helm/helm.go
@@ -0,0 +1,205 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package helm
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	packages_model "code.gitea.io/gitea/models/packages"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/log"
+	packages_module "code.gitea.io/gitea/modules/packages"
+	helm_module "code.gitea.io/gitea/modules/packages/helm"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/routers/api/packages/helper"
+	packages_service "code.gitea.io/gitea/services/packages"
+
+	"gopkg.in/yaml.v2"
+)
+
+func apiError(ctx *context.Context, status int, obj interface{}) {
+	helper.LogAndProcessError(ctx, status, obj, func(message string) {
+		type Error struct {
+			Error string `json:"error"`
+		}
+		ctx.JSON(status, Error{
+			Error: message,
+		})
+	})
+}
+
+// Index generates the Helm charts index
+func Index(ctx *context.Context) {
+	pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
+		OwnerID: ctx.Package.Owner.ID,
+		Type:    packages_model.TypeHelm,
+	})
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+
+	baseURL := setting.AppURL + "api/packages/" + url.PathEscape(ctx.Package.Owner.Name) + "/helm"
+
+	type ChartVersion struct {
+		helm_module.Metadata `yaml:",inline"`
+		URLs                 []string  `yaml:"urls"`
+		Created              time.Time `yaml:"created,omitempty"`
+		Removed              bool      `yaml:"removed,omitempty"`
+		Digest               string    `yaml:"digest,omitempty"`
+	}
+
+	type ServerInfo struct {
+		ContextPath string `yaml:"contextPath,omitempty"`
+	}
+
+	type Index struct {
+		APIVersion string                     `yaml:"apiVersion"`
+		Entries    map[string][]*ChartVersion `yaml:"entries"`
+		Generated  time.Time                  `yaml:"generated,omitempty"`
+		ServerInfo *ServerInfo                `yaml:"serverInfo,omitempty"`
+	}
+
+	entries := make(map[string][]*ChartVersion)
+	for _, pv := range pvs {
+		metadata := &helm_module.Metadata{}
+		if err := json.Unmarshal([]byte(pv.MetadataJSON), &metadata); err != nil {
+			apiError(ctx, http.StatusInternalServerError, err)
+			return
+		}
+
+		entries[metadata.Name] = append(entries[metadata.Name], &ChartVersion{
+			Metadata: *metadata,
+			Created:  pv.CreatedUnix.AsTime(),
+			URLs:     []string{fmt.Sprintf("%s/%s", baseURL, url.PathEscape(createFilename(metadata)))},
+		})
+	}
+
+	ctx.Resp.WriteHeader(http.StatusOK)
+	if err := yaml.NewEncoder(ctx.Resp).Encode(&Index{
+		APIVersion: "v1",
+		Entries:    entries,
+		Generated:  time.Now(),
+		ServerInfo: &ServerInfo{
+			ContextPath: setting.AppSubURL + "/api/packages/" + url.PathEscape(ctx.Package.Owner.Name) + "/helm",
+		},
+	}); err != nil {
+		log.Error("YAML encode failed: %v", err)
+	}
+}
+
+// DownloadPackageFile serves the content of a package
+func DownloadPackageFile(ctx *context.Context) {
+	filename := ctx.Params("filename")
+
+	pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
+		OwnerID: ctx.Package.Owner.ID,
+		Type:    packages_model.TypeHelm,
+		Name: packages_model.SearchValue{
+			ExactMatch: true,
+			Value:      ctx.Params("package"),
+		},
+		HasFileWithName: filename,
+	})
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+	if len(pvs) != 1 {
+		apiError(ctx, http.StatusNotFound, nil)
+		return
+	}
+
+	s, pf, err := packages_service.GetFileStreamByPackageVersion(
+		ctx,
+		pvs[0],
+		&packages_service.PackageFileInfo{
+			Filename: filename,
+		},
+	)
+	if err != nil {
+		if err == packages_model.ErrPackageFileNotExist {
+			apiError(ctx, http.StatusNotFound, err)
+			return
+		}
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+	defer s.Close()
+
+	ctx.ServeStream(s, pf.Name)
+}
+
+// UploadPackage creates a new package
+func UploadPackage(ctx *context.Context) {
+	upload, needToClose, err := ctx.UploadStream()
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+	if needToClose {
+		defer upload.Close()
+	}
+
+	buf, err := packages_module.CreateHashedBufferFromReader(upload, 32*1024*1024)
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+	defer buf.Close()
+
+	metadata, err := helm_module.ParseChartArchive(buf)
+	if err != nil {
+		apiError(ctx, http.StatusBadRequest, err)
+		return
+	}
+
+	if _, err := buf.Seek(0, io.SeekStart); err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+
+	_, _, err = packages_service.CreatePackageOrAddFileToExisting(
+		&packages_service.PackageCreationInfo{
+			PackageInfo: packages_service.PackageInfo{
+				Owner:       ctx.Package.Owner,
+				PackageType: packages_model.TypeHelm,
+				Name:        metadata.Name,
+				Version:     metadata.Version,
+			},
+			SemverCompatible: true,
+			Creator:          ctx.Doer,
+			Metadata:         metadata,
+		},
+		&packages_service.PackageFileCreationInfo{
+			PackageFileInfo: packages_service.PackageFileInfo{
+				Filename: createFilename(metadata),
+			},
+			Data:              buf,
+			IsLead:            true,
+			OverwriteExisting: true,
+		},
+	)
+	if err != nil {
+		if err == packages_model.ErrDuplicatePackageVersion {
+			apiError(ctx, http.StatusConflict, err)
+			return
+		}
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+
+	ctx.Status(http.StatusCreated)
+}
+
+func createFilename(metadata *helm_module.Metadata) string {
+	return strings.ToLower(fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version))
+}
diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go
index b445e8e2f8..f3aa19c319 100644
--- a/routers/api/v1/packages/package.go
+++ b/routers/api/v1/packages/package.go
@@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) {
 	//   in: query
 	//   description: package type filter
 	//   type: string
-	//   enum: [composer, conan, generic, maven, npm, nuget, pypi, rubygems]
+	//   enum: [composer, conan, container, generic, helm, maven, npm, nuget, pypi, rubygems]
 	// - name: q
 	//   in: query
 	//   description: name filter
diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl
index 114a108feb..373a97407b 100644
--- a/templates/admin/packages/list.tmpl
+++ b/templates/admin/packages/list.tmpl
@@ -17,6 +17,7 @@
 						<option value="conan" {{if eq .PackageType "conan"}}selected="selected"{{end}}>Conan</option>
 						<option value="container" {{if eq .PackageType "container"}}selected="selected"{{end}}>Container</option>
 						<option value="generic" {{if eq .PackageType "generic"}}selected="selected"{{end}}>Generic</option>
+						<option value="helm" {{if eq .PackageType "helm"}}selected="selected"{{end}}>Helm</option>
 						<option value="maven" {{if eq .PackageType "maven"}}selected="selected"{{end}}>Maven</option>
 						<option value="npm" {{if eq .PackageType "npm"}}selected="selected"{{end}}>npm</option>
 						<option value="nuget" {{if eq .PackageType "nuget"}}selected="selected"{{end}}>NuGet</option>
diff --git a/templates/package/content/helm.tmpl b/templates/package/content/helm.tmpl
new file mode 100644
index 0000000000..a85f7c4850
--- /dev/null
+++ b/templates/package/content/helm.tmpl
@@ -0,0 +1,57 @@
+{{if eq .PackageDescriptor.Package.Type "helm"}}
+	<h4 class="ui top attached header">{{.i18n.Tr "packages.installation"}}</h4>
+	<div class="ui attached segment">
+		<div class="ui form">
+			<div class="field">
+				<label>{{svg "octicon-terminal"}} {{.i18n.Tr "packages.helm.registry"}}</label>
+				<div class="markup"><pre class="code-block"><code>helm repo add gitea {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/helm
+helm repo update</code></pre></div>
+			</div>
+			<div class="field">
+				<label>{{svg "octicon-terminal"}} {{.i18n.Tr "packages.helm.install"}}</label>
+				<div class="markup"><pre class="code-block"><code>helm install {{.PackageDescriptor.Package.Name}} gitea/{{.PackageDescriptor.Package.Name}}</code></pre></div>
+			</div>
+			<div class="field">
+				<label>{{.i18n.Tr "packages.helm.documentation" | Safe}}</label>
+			</div>
+		</div>
+	</div>
+
+	{{if .PackageDescriptor.Metadata.Description}}
+		<h4 class="ui top attached header">{{.i18n.Tr "packages.about"}}</h4>
+		<div class="ui attached segment">
+			{{.PackageDescriptor.Metadata.Description}}
+		</div>
+	{{end}}
+
+	{{if .PackageDescriptor.Metadata.Dependencies}}
+		<h4 class="ui top attached header">{{.i18n.Tr "packages.dependencies"}}</h4>
+		<div class="ui attached segment">
+			<table class="ui single line very basic table">
+				<thead>
+					<tr>
+						<th class="ten wide">{{.i18n.Tr "packages.dependency.id"}}</th>
+						<th class="six wide">{{.i18n.Tr "packages.dependency.version"}}</th>
+					</tr>
+				</thead>
+				<tbody>
+					{{range .PackageDescriptor.Metadata.Dependencies}}
+						<tr>
+							<td>{{.Name}}</td>
+							<td>{{.Version}}</td>
+						</tr>
+					{{end}}
+				</tbody>
+			</table>
+		</div>
+	{{end}}
+
+	{{if .PackageDescriptor.Metadata.Keywords}}
+		<h4 class="ui top attached header">{{.i18n.Tr "packages.keywords"}}</h4>
+		<div class="ui attached segment">
+			{{range .PackageDescriptor.Metadata.Keywords}}
+				{{.}}
+			{{end}}
+		</div>
+	{{end}}
+{{end}}
diff --git a/templates/package/content/npm.tmpl b/templates/package/content/npm.tmpl
index 16347d1b6e..bc714e5c97 100644
--- a/templates/package/content/npm.tmpl
+++ b/templates/package/content/npm.tmpl
@@ -45,7 +45,7 @@
 		</div>
 	{{end}}
 
-	{{if or .PackageDescriptor.Metadata.Keywords}}
+	{{if .PackageDescriptor.Metadata.Keywords}}
 		<h4 class="ui top attached header">{{.i18n.Tr "packages.keywords"}}</h4>
 		<div class="ui attached segment">
 			{{range .PackageDescriptor.Metadata.Keywords}}
diff --git a/templates/package/metadata/helm.tmpl b/templates/package/metadata/helm.tmpl
new file mode 100644
index 0000000000..7c97c6358e
--- /dev/null
+++ b/templates/package/metadata/helm.tmpl
@@ -0,0 +1,4 @@
+{{if eq .PackageDescriptor.Package.Type "helm"}}
+	{{range .PackageDescriptor.Metadata.Maintainers}}<div class="item" title="{{$.i18n.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.Name}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Home}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.Home}}" target="_blank" rel="noopener noreferrer me">{{.i18n.Tr "packages.details.project_site"}}</a></div>{{end}}
+{{end}}
diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl
index 9216e6b9de..0b0f71283b 100644
--- a/templates/package/shared/list.tmpl
+++ b/templates/package/shared/list.tmpl
@@ -10,6 +10,7 @@
 				<option value="conan" {{if eq .PackageType "conan"}}selected="selected"{{end}}>Conan</option>
 				<option value="container" {{if eq .PackageType "container"}}selected="selected"{{end}}>Container</option>
 				<option value="generic" {{if eq .PackageType "generic"}}selected="selected"{{end}}>Generic</option>
+				<option value="helm" {{if eq .PackageType "helm"}}selected="selected"{{end}}>Helm</option>
 				<option value="maven" {{if eq .PackageType "maven"}}selected="selected"{{end}}>Maven</option>
 				<option value="npm" {{if eq .PackageType "npm"}}selected="selected"{{end}}>npm</option>
 				<option value="nuget" {{if eq .PackageType "nuget"}}selected="selected"{{end}}>NuGet</option>
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl
index 1b1c5d50c6..bb96da3410 100644
--- a/templates/package/view.tmpl
+++ b/templates/package/view.tmpl
@@ -23,9 +23,10 @@
 					{{template "package/content/conan" .}}
 					{{template "package/content/container" .}}
 					{{template "package/content/generic" .}}
-					{{template "package/content/nuget" .}}
-					{{template "package/content/npm" .}}
+					{{template "package/content/helm" .}}
 					{{template "package/content/maven" .}}
+					{{template "package/content/npm" .}}
+					{{template "package/content/nuget" .}}
 					{{template "package/content/pypi" .}}
 					{{template "package/content/rubygems" .}}
 				</div>
@@ -43,9 +44,10 @@
 							{{template "package/metadata/conan" .}}
 							{{template "package/metadata/container" .}}
 							{{template "package/metadata/generic" .}}
-							{{template "package/metadata/nuget" .}}
-							{{template "package/metadata/npm" .}}
+							{{template "package/metadata/helm" .}}
 							{{template "package/metadata/maven" .}}
+							{{template "package/metadata/npm" .}}
+							{{template "package/metadata/nuget" .}}
 							{{template "package/metadata/pypi" .}}
 							{{template "package/metadata/rubygems" .}}
 						</div>
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 4bf3874bae..e35ba98909 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -1904,7 +1904,9 @@
             "enum": [
               "composer",
               "conan",
+              "container",
               "generic",
+              "helm",
               "maven",
               "npm",
               "nuget",
diff --git a/web_src/svg/gitea-helm.svg b/web_src/svg/gitea-helm.svg
new file mode 100644
index 0000000000..1209965592
--- /dev/null
+++ b/web_src/svg/gitea-helm.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62 65" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round">
+<g><path fill="#3f7a9c" d="M41.868 16.659l.248.19c1.503 1.172 2.825 2.564 3.919 4.128l.9 1.414 2.774-1.601-1.05-1.65c-.959-1.37-2.068-2.628-3.304-3.748.728-.642 1.491-1.459 2.215-2.403 2.158-2.813 3.141-5.657 2.204-6.376s-3.43.966-5.589 3.779c-.725.944-1.317 1.892-1.748 2.762-3.013-1.94-6.525-3.176-10.308-3.48.189-.957.298-2.076.298-3.273 0-3.546-.951-6.4-2.133-6.4S28.16 2.854 28.16 6.4c0 1.198.109 2.317.298 3.273-3.783.304-7.296 1.54-10.308 3.48-.431-.87-1.024-1.818-1.748-2.762-2.158-2.813-4.651-4.498-5.589-3.779s.045 3.563 2.204 6.376c.725.944 1.487 1.761 2.215 2.403-1.236 1.12-2.345 2.378-3.304 3.748a22.33 22.33 0 0 0-1.05 1.65l2.774 1.601a19.13 19.13 0 0 1 .9-1.414 19.21 19.21 0 0 1 3.919-4.128l.248-.19c3.214-2.424 7.221-3.859 11.574-3.859s8.36 1.435 11.574 3.859zM14.551 43.023A19.15 19.15 0 0 0 30.293 51.2a19.15 19.15 0 0 0 15.742-8.177l2.624 1.837c-.959 1.37-2.068 2.628-3.304 3.748.728.642 1.491 1.459 2.215 2.403 2.158 2.813 3.141 5.657 2.204 6.376s-3.43-.966-5.589-3.779c-.725-.944-1.317-1.892-1.748-2.762-3.013 1.94-6.525 3.176-10.308 3.48.189.957.298 2.076.298 3.273 0 3.546-.951 6.4-2.133 6.4s-2.133-2.854-2.133-6.4c0-1.198.109-2.317.298-3.273-3.783-.304-7.296-1.54-10.308-3.48-.431.87-1.024 1.818-1.748 2.762-2.158 2.813-4.651 4.498-5.589 3.779s.045-3.563 2.204-6.376c.725-.944 1.487-1.761 2.215-2.403-1.236-1.12-2.345-2.378-3.304-3.748zM44.8 40.533V24.32h4.693l3.413 9.813 3.413-9.813h4.267v16.213h-3.84v-5.12l.853-5.973-3.84 9.813h-2.133l-3.84-9.813.853 5.973v5.12zM31.147 24.32v16.213h10.667V37.12h-6.4v-12.8zm-14.08 0v16.213H28.16V37.12h-6.827v-2.987h5.547V30.72h-5.547v-2.987h6.4V24.32zm-12.8 16.213v-6.4h5.12v6.4h4.267V24.32H9.387v5.973h-5.12V24.32H0v16.213z" stroke="none"/></g>
+</svg>
\ No newline at end of file