Install and expose ArgoCD

A guide showing you how to install ArgoCD, expose its web UI, and connect private repositories and registries.

Our clusters ship without ArgoCD installed. Running your own is useful if you want to have full control over GitOps delivery into your cluster.

The guide is based on the upstream Argo CD Helm chart (argo/argo-cd).

Prerequisites

Helm needs to be provided with the correct repository:

  1. Setup helm repo

    helm repo add argo https://argoproj.github.io/argo-helm
    
  2. Make sure to update repo cache

    helm repo update
    
  3. Make sure kubectl is configured for the cluster you want to install into.

  4. To reach the ArgoCD UI over HTTPS you need something to route external traffic into the cluster and cert-manager to issue the certificate. This guide keeps the ArgoCD side of that independent of which controller you run, and leaves the controller-specific parts to the guide for the controller you picked. You can use our managed add-ons or install your own:

Install

  1. Run Helm install, pinning an explicit chart version. --create-namespace creates the argocd namespace for you:

    helm install argocd argo/argo-cd \
      --namespace argocd \
      --create-namespace \
      --version 10.1.4
    

    A full list of available Helm values is on ArgoCD’s ArtifactHub page.

    Note: If you already know you want the UI reachable via Ingress, you can supply a values.yaml up front instead of running helm upgrade afterwards, see the Expose the ArgoCD UI section below.

Verify the installation

  1. Check that all pods are running:

    kubectl get pods -n argocd
    
  2. Retrieve the initial admin password:

    kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
    

    Upstream recommends deleting this secret once you have changed the admin password.

  3. Log in with the ArgoCD CLI over a port-forward:

    kubectl port-forward svc/argocd-server -n argocd 8080:443
    argocd login localhost:8080 --username admin --insecure
    

    Note: On a fresh install the ArgoCD server presents its own self-signed certificate, so the CLI needs --insecure (or an interactive confirmation) to connect. Once you expose the server through the Ingress below, log in against that hostname with --grpc-web instead, and drop --insecure.

Expose the ArgoCD UI

  1. Interim access

    Before you have decided on a hostname, reach the UI with a port-forward:

    kubectl port-forward svc/argocd-server -n argocd 8080:443
    

    The UI is then available at https://localhost:8080.

    Note: Both service ports forward to the same container port, so the scheme follows how the server is configured, not the port you forward. Use https://localhost:8080 on a default install, and http://localhost:8080 once you have set server.insecure: true as described below.

  2. Decide where TLS terminates

    The ArgoCD server multiplexes gRPC and HTTP on a single port and terminates TLS itself by default. You therefore have to choose one of two arrangements before writing any routing configuration:

    • Terminate TLS at your ingress. Set server.insecure: true so the server serves plain HTTP behind it. This is the simpler option and what the examples below assume.
    • Keep TLS on the backend. Leave the server as it is and configure your ingress to pass TLS through untouched. How that is expressed depends entirely on which ingress you run.
    configs:
      params:
        server.insecure: true
    

    The upstream ArgoCD ingress documentation covers both arrangements in detail.

  3. Route traffic to the server

    The chart can create the routing resource for you in either model. Pick the one your controller implements: Gateway API if you run something like Envoy Gateway, Ingress if you run an ingress controller such as ingress-nginx.

    Gateway API. Point an HTTPRoute at an existing Gateway:

    server:
      httproute:
        enabled: true
        parentRefs:
          - name: <your-gateway>
            namespace: <gateway-namespace>
            sectionName: https
        hostnames:
          - argocd.example.tld
    

    Add a GRPCRoute alongside it if you want the CLI to speak plain gRPC rather than tunnelling it over HTTP:

    server:
      grpcroute:
        enabled: true
        parentRefs:
          - name: <your-gateway>
            namespace: <gateway-namespace>
            sectionName: https
        hostnames:
          - argocd.example.tld
    

    Note: With Gateway API the certificate belongs to the Gateway listener, not to the route, so there is no cert-manager annotation to set here. Configure TLS where the Gateway is defined.

    Ingress. If your controller uses Ingress resources instead:

    server:
      ingress:
        enabled: true
        ingressClassName: <your-ingress-class>
        hostname: argocd.example.tld
        annotations:
          cert-manager.io/issuer: letsencrypt-prod
        tls: true
    

    Note: Keep all annotations in one block. A second server: section further down the same values.yaml does not merge with the first one, so earlier annotations would be lost.

    Either way, apply the values with:

    helm upgrade --install argocd argo/argo-cd --namespace argocd --version 10.1.4 --values values.yaml
    
  4. Certificates on the Ingress path

    If you routed with an HTTPRoute, skip this. The certificate is attached to the Gateway listener and is configured wherever that Gateway lives.

    cert-manager.io/issuer refers to an Issuer, which is namespaced. The letsencrypt-prod Issuer from our Ingress guide has to exist in the argocd namespace, so apply it with kubectl apply -f issuer.yaml -n argocd. To keep a single issuer for the whole cluster, create it as a ClusterIssuer and use the cert-manager.io/cluster-issuer annotation instead.

    Note: Our managed cert-manager add-on installs the controller and its CRDs only. It does not create any Issuer or ClusterIssuer for you, so one of the above is always required. Without it the certificate stays False and cert-manager reports Referenced "Issuer" not found.

  5. Using the ArgoCD CLI against the exposed server

    If you enabled a GRPCRoute, the CLI can speak gRPC directly and needs no extra flag:

    argocd login argocd.example.tld
    

    On the Ingress path there is no equivalent. An Ingress carries one backend protocol and the route above serves the UI over HTTP, so the CLI has to tunnel gRPC over HTTP instead:

    argocd login argocd.example.tld --grpc-web
    

    --grpc-web works through anything that terminates HTTP, so it is the safe choice if you are unsure. Serving plain gRPC through an Ingress means a second Ingress on its own hostname with a controller-specific backend-protocol setting, which is one of the things Gateway API removes the need for.

  6. Restricting who can reach it

    ArgoCD holds credentials for every repository it syncs and is a deployment path into your cluster, so restrict access before pointing a public hostname at it.

    The control that does not depend on your ingress controller is the whitelist on the OpenStack load balancer in front of the cluster. It is part of your cluster configuration rather than something inside the cluster, so contact our support with the source ranges that should be allowed and we will apply it. If you run your own ingress controller, the equivalent is loadBalancerSourceRanges on its LoadBalancer service.

    Note: Your ingress controller may also offer a source-range restriction of its own, such as nginx.ingress.kubernetes.io/whitelist-source-range. It matches on the client address as the controller sees it, which is the real client address only when the controller is given it over PROXY protocol. Whether that holds depends on how the ingress in your cluster is set up, so check with our support before relying on the annotation. Treat it as defence in depth behind a load balancer restriction, never as your only control.

Connect a private Git repository

Add a repository with credentials via the CLI:

argocd repo add <repo-url> --username <user> --password <token>

For Azure DevOps, use a Personal Access Token scoped to Code: Read, or an SSH deploy key instead of username/password.

Alternatively, declare the repository as a Secret labelled for ArgoCD:

apiVersion: v1
kind: Secret
metadata:
  name: private-repo
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
stringData:
  url: <repo-url>
  username: <user>
  password: <token>

Note: Avoid committing credentials in plaintext to git. See Managing secrets below for options to keep this manifest encrypted at rest in your repository.

Pull images from a private registry

Create an imagePullSecret in the namespace your application is deployed to:

kubectl create secret docker-registry regcred \
  --namespace <app-namespace> \
  --docker-server=<registry-server> \
  --docker-username=<user> \
  --docker-password=<password>

This works for any registry (Docker Hub, ACR, GCR, Quay, …). Reference the secret from your workload’s ServiceAccount imagePullSecrets, or set it via your Helm chart’s values.

Managing secrets

Since ArgoCD deploys straight from git, secrets should not be committed in plaintext. Two common options, both customer-installed:

  • Sealed Secrets encrypts secrets client-side with kubeseal so only the controller in your cluster can decrypt them, safe to commit to git.
  • External Secrets syncs secrets from an external secret store (e.g. Vault, cloud provider secret managers) into Kubernetes Secret resources.

Deploy your first application

Create a file called application.yaml:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: <repo-url>
    targetRevision: HEAD
    path: <path-to-manifests>
  destination:
    server: https://kubernetes.default.svc
    namespace: <app-namespace>
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Note: ArgoCD does not create the destination namespace on its own, so a sync into a namespace that does not exist fails. CreateNamespace=true lets ArgoCD create it. Drop the option if you prefer to create the namespace yourself.

If you’re deploying a Helm chart instead of plain manifests, use a Helm source:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: <repo-url>
    targetRevision: HEAD
    path: <path-to-chart>
    helm:
      valueFiles:
        - values.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: <app-namespace>
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Then create the resource in the cluster by running:

kubectl apply -f application.yaml

Upgrade

helm upgrade --install argocd argo/argo-cd --namespace argocd --version <new-version>

Uninstall

  1. Remove your Application resources first

    kubectl delete applications --all -n argocd
    

    Note: Applications that carry the resources-finalizer.argocd.argoproj.io finalizer need the ArgoCD controller running to be cleaned up. Delete them before uninstalling the release, or the namespace gets stuck in Terminating with nothing left to clear the finalizer. Without that finalizer the workloads ArgoCD deployed are left running, which is usually what you want when you are only replacing ArgoCD itself.

  2. Remove the release

    helm uninstall argocd -n argocd
    
  3. Remove the namespace if necessary

    kubectl delete namespace argocd
    
  4. Remove the CRDs if you are not reinstalling

    kubectl delete crd applications.argoproj.io applicationsets.argoproj.io appprojects.argoproj.io
    

    Note: The chart sets crds.keep: true, so helm uninstall deliberately leaves the CRDs in place to protect your resources during an upgrade. They are cluster-scoped and outlive the namespace. Deleting a CRD deletes every resource of that type in the cluster, so only do this once you are sure ArgoCD is going away for good.