---
title: 'Tutorial: Deploy Access Server on Kubernetes with Helm'
canonical: https://openvpn.net/as-docs/tutorials/tutorial--deploy-on-kubernetes-with-helm/
product: Access Server Docs
description: Deploy OpenVPN Access Server on a Kubernetes cluster using the official Helm Chart — covers installation, custom values.yaml configuration, and cloud provider options, including GKE, EKS, and AKS.
---

# Tutorial: Deploy Access Server on Kubernetes with Helm

## Overview

This tutorial guides you through deploying Access Server using a Helm Chart on a Kubernetes cluster. Kubernetes (K8s) is an open-source container orchestration platform that automates deployment, scaling, and management of containerized applications.

### Use cases

| **Audience** | **Use case** |
| --- | --- |
| Developers | Run and test Access Server in containers. |
| IT admins and DevOps | Manage production VPN environments at scale. |
| Organizations | Build reliable, scalable cloud-native platforms. |

### Where Kubernetes can run

- **On a laptop:** Minikube, Kind, or Docker Desktop.
- **On-premises:** Your own servers.
- **Cloud:** Google Kubernetes Engine (GKE), Amazon EKS, Azure AKS, DigitalOcean Kubernetes, IBM Cloud Kubernetes Service, and others.

> **Tip**
>
> Check the [system requirements](https://openvpn.net/as-docs/system-requirements.md) to ensure your host is compatible.

## Prerequisites

Before proceeding, ensure your environment meets the following requirements:

- [Kubernetes](https://kubernetes.io/releases/download/) installed.
- [kubectl](https://kubernetes.io/docs/tasks/tools/) installed.
- [Helm](https://helm.sh/docs/intro/install/) installed.

## Step 1: Add the Access Server Helm Chart repository

1. Connect to the console and get root privileges.
2. Add the repository to your system:

   ```
   helm repo add as-helm-chart https://openvpn.github.io/as-helm-chart
   ```
3. Verify the Access Server Helm Chart is available:

   ```
   helm search repo as-helm-chart
   ```

   - Example output:

     ```
     # helm search repo as-helm-chart
     NAME                            CHART VERSION   APP VERSION     DESCRIPTION
     as-helm-chart/openvpn-as        0.1.1           latest          A Helm chart for OpenVPN Access Server
     ```

## Step 2: Install the Access Server Helm Chart

### Option A: Install with default values

1. Install the Access Server Helm Chart with default settings:

   ```
   helm install my-openvpn-as as-helm-chart/openvpn-as
   ```

   - The Access Server Helm Chart installs on one of your pods in your Kubernetes cluster.
2. Check the pod status:

   ```
   kubectl get pods
   ```

   - Example output

     ```
     # kubectl get pods
     NAME                                        READY   STATUS    RESTARTS   AGE
     my-openvpn-as-openvpn-as-6b474c9757-jmjlc   1/1     Running   0          2m36s
     ```
3. Verify the installation:

   ```
   helm status my-openvpn-as --show-resources
   ```

   - Example output:

     ```
     # helm status my-openvpn-as --show-resources
     NAME: my-openvpn-as
     LAST DEPLOYED: Wed Sep  3 17:06:26 2025
     NAMESPACE: default
     STATUS: deployed
     REVISION: 1
     RESOURCES:
     ==> v1/Secret
     NAME                                 TYPE     DATA   AGE
     my-openvpn-as-openvpn-as-poststart   Opaque   1      2d23h

     ==> v1/PersistentVolumeClaim
     NAME                           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS       VOLUMEATTRIBUTESCLASS   AGE
     my-openvpn-as-openvpn-as-pvc   Bound    pvc-256c79ba-97e9-474b-8b7c-8008187bb2fb   5Gi        RWO            do-block-storage   <unset>                 2d23h

     ==> v1/Service
     NAME                       TYPE           CLUSTER-IP      EXTERNAL-IP       PORT(S)                                      AGE
     my-openvpn-as-openvpn-as   LoadBalancer   10.108.92.231   203.0.113.5   943:30479/TCP,443:31010/TCP,1194:31020/UDP   2d23h

     ==> v1/Deployment
     NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
     my-openvpn-as-openvpn-as   1/1     1            1           2d23h

     ==> v1/Pod(related)
     NAME                                        READY   STATUS    RESTARTS   AGE
     my-openvpn-as-openvpn-as-6b474c9757-jmjlc   1/1     Running   0          2d23h
     ```
4. Use the `Pod Name` and the `sacli` tool to check Access Server:

   ```
   kubectl exec -it <Pod Name> -- /bin/bash sacli version
   ```

   Replace `<Pod Name>` with your pod name. From our example: `my-openvpn-as-openvpn-as-6b474c9757-jmjlc`.

   - Example output:

     ```
     # kubectl exec -it my-openvpn-as-openvpn-as-6b474c9757-jmjlc -- /bin/bash sacli version
     3.2.1 (abc12345)
     ```

### Option B: Install with custom values

Use this option if you need a custom image version, service type, or storage size.

1. Go to [ArtifactHub](https://artifacthub.io/packages/helm/as-helm-chart/openvpn-as) and select **Default Values**.
2. Copy the contents of the `values.yaml` file.
3. Create a new custom values file:

   ```
   nano my-values.yaml
   ```
4. Paste the copied content and edit as needed. Key settings:

   ```
   image:
     repository: openvpn/openvpn-as
     tag: latest
     pullPolicy: IfNotPresent

   service:
     type: LoadBalancer
     ports:
       admin: 943
       tcp: 443
       udp: 1194

   persistence:
     enabled: true
     size: 5Gi
     storageClass:
     accessMode: ReadWriteOnce
   ```

   - Common customizations:

     | **Setting** | **Example** | **Use case** |
     | --- | --- | --- |
     | `image.tag` | `3.2.1-abc12345-Ubuntu26` | Pin to a specific AS version. |
     | `service.type` | `NodePort` | Bare-metal environments without a load balancer. |
     | `persistence.size` | `20Gi` | Larger storage for high-traffic deployments. |
5. Save and exit (`Ctrl+X`, then `Y`).
6. Install with your custom values file:

   ```
   helm install my-openvpn-as as-helm-chart/openvpn-as -f my-values.yaml
   ```
7. Verify the installation:

   ```
   helm status my-openvpn-as --show-resources
   ```

## Step 3: Find the Admin Web UI URL

> **Important**
>
> This step demonstrates how to find the Admin Web UI IP via the `LoadBalancer` service type. The process may differ for other service types.

You've installed Access Server, and the container is running. You can now sign in to the Admin Web UI, a web-based GUI for managing your VPN server, with or without Linux knowledge.

The Admin Web UI is available at `https://EXTERNAL-IP:943/admin`.

1. Retrieve the `EXTERNAL-IP`:

   ```
   kubectl get svc
   ```

   - Example output:

     ```
     # kubectl get svc
     NAME                       TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)                                      AGE
     kubernetes                 ClusterIP      10.108.64.1     <none>         443/TCP                                      24h
     my-openvpn-as-openvpn-as   LoadBalancer   10.108.79.160   203.0.113.5   943:31851/TCP,443:31305/TCP,1194:30865/UDP   4m1s
     ```

     The `EXTERNAL-IP` of this Access Server is `203.0.113.5`.

## Step 4: Find the temporary password

The default admin username is **openvpn**. The temporary password is in the container logs.

1. Get the pod name:

   ```
   kubectl get pods
   ```

   - Example output:

     ```
     # kubectl get pods
     NAME                                        READY   STATUS    RESTARTS   AGE
     my-openvpn-as-openvpn-as-6b474c9757-jmjlc   1/1     Running   0          5m53s
     ```
2. Retrieve the logs using the pod name:

   ```
   kubectl logs <Pod Name>
   ```

   Replace `<Pod Name>` with your name from the previous command. Example `my-openvpn-as-openvpn-as-6b474c9757-jmjlc`.

   - The Access Server Initial Configuration Tool output displays.
3. Scroll through the output to find:

   ```
   Auto-generated pass = "<password>". Setting in db...
   ```
4. Use the password with the **openvpn** username to sign in.

## Step 5: Sign in to the Admin Web UI

1. Open a web browser.
2. Navigate to `https://EXTERNAL-IP:943/admin`.

   > **Important**
   >
   > Ensure you use **https** in the URL.
3. Select through the security warning. Access Server uses a self-signed SSL certificate by default. We recommend replacing it with a signed certificate. See [SSL Certificates](https://openvpn.net/as-docs/ssl-certificates.md).
4. Enter **openvpn** as the username with the temporary password from Step 4.
5. Review and accept the EULA to proceed to the Admin Web UI.

## Step 6: Update the hostname or IP address

For end-user devices to connect to your VPN server, update the hostname or public IP address in Access Server:

1. Sign in to the Admin Web UI.
2. Select **Configuration → Network Settings**.
3. Enter the `EXTERNAL-IP` or your domain name in the **Hostname or IP Address** field.

   > **Note**
   >
   > Access Server likely has a private IP address populated in this field. Clients connecting from outside the network need a public IP or a domain name with an A record pointing to it. We recommend using a [domain name](https://openvpn.net/as-docs/hostname.md).
4. Select **Save Settings** and **Update Running Server**.

## (Optional) Step 7: Apply custom post-start configuration

When deploying Access Server via Helm Chart in your Kubernetes Cluster, you can run custom scripts to perform custom configuration, such as creating users, configuring rules, installing Web SSL certificates, etc

Here's an example of running `sacli` commands to create a new user with a password and grant them admin privileges. By following the procedure for our example below, you would push the following Bash Script:

```
#!/bin/sh
echo "Custom setup"
echo "[INFO] Running default post-start script for OpenVPN Access Server..."
# Waiting for AS service initialization
until /usr/local/openvpn_as/scripts/sacli status 2>/dev/null |grep -q '"api": "on"'
do
    sleep 2
done
sacli --user "admin" --key "type" --value "user_connect" UserPropPut
sacli --user "admin" --new_pass "secure123" SetLocalPassword
sacli --user "admin" --key "prop_superuser" --value "true" UserPropPut
sacli start
```

> **Important**
>
> If you plan to use `sacli` in the `postStart` script, ensure that the Access Server service is fully up and running beforehand. You can find an example of how to wait for the service in the Inline Script above with the `sleep 2` line.

1. Add the Helm Chart Repository to your system:

   ```
   helm repo add as-helm-chart https://openvpn.github.io/as-helm-chart
   ```
2. Go to the ArtifactHub where the [Access Server Helm Chart](https://artifacthub.io/packages/helm/as-helm-chart/openvpn-as) is allocated and click on the `DEFAULT VALUES` option on the right panel.
3. Copy the content of the `values.yaml` file.
4. Create a new file (example: `my-values.yaml`) where you'll be deploying your Access Server Helm Chart:

   ```
   nano my-values.yaml
   ```
5. Paste the copied contents from the `values.yaml` file.
6. Edit your new file and the `postStart` script section from this:

   ```
   postStart:
     enabled: false

     # Optional: inline override
     customScript: ""
   ```

   To this:

   ```
   postStart:
     enabled: true

     # Optional: inline override
     customScript: |
       #!/bin/sh
       echo "Custom setup"
       echo "[INFO] Running default post-start script for OpenVPN Access Server..."
       # Waiting for AS service initialization
       until /usr/local/openvpn_as/scripts/sacli status 2>/dev/null |grep -q '"api": "on"'
       do
           sleep 2
       done
       sacli --user "admin" --key "type" --value "user_connect" UserPropPut
       sacli --user "admin" --new_pass "secure123" SetLocalPassword
       sacli --user "admin" --key "prop_superuser" --value "true" UserPropPut
       sacli start
   ```
7. Save and exit (Ctrl+x, then y).
8. Install the Access Server Helm Chart with your new custom file. (Example: `my-values.yaml`.)

   ```
   helm install my-openvpn-as as-helm-chart/openvpn-as -f my-values.yaml
   ```

   - Access Server will be successfully deployed with the custom commands pushed by the Bash script.
9. Verify that Access Server was installed correctly by identifying the pod name:

   ```
   helm status my-openvpn-as --show-resources
   ```

   - Example output:

     ```
     # helm status my-openvpn-as --show-resources
     NAME: my-openvpn-as
     LAST DEPLOYED: Wed Sep  3 17:06:26 2025
     NAMESPACE: default
     STATUS: deployed
     REVISION: 1
     RESOURCES:
     ==> v1/Secret
     NAME                                 TYPE     DATA   AGE
     my-openvpn-as-openvpn-as-poststart   Opaque   1      2d23h

     ==> v1/PersistentVolumeClaim
     NAME                           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS       VOLUMEATTRIBUTESCLASS   AGE
     my-openvpn-as-openvpn-as-pvc   Bound    pvc-256c79ba-97e9-474b-8b7c-8008187bb2fb   5Gi        RWO            do-block-storage   <unset>                 2d23h

     ==> v1/Service
     NAME                       TYPE           CLUSTER-IP      EXTERNAL-IP       PORT(S)                                      AGE
     my-openvpn-as-openvpn-as   LoadBalancer   10.108.92.231   203.0.113.5   943:30479/TCP,443:31010/TCP,1194:31020/UDP   2d23h

     ==> v1/Deployment
     NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
     my-openvpn-as-openvpn-as   1/1     1            1           2d23h

     ==> v1/Pod(related)
     NAME                                        READY   STATUS    RESTARTS   AGE
     my-openvpn-as-openvpn-as-6b474c9757-jmjlc   1/1     Running   0          2d23h
     ```
10. And using the `Pod Name` and the `sacli` tool to check Access Server:

    ```
    kubectl exec -it <Pod Name> -- /bin/bash sacli UserPropGet
    ```

    Replace `<Pod Name>` with your pod name. From our example: `my-openvpn-as-openvpn-as-6b474c9757-jmjlc`.

    - Example output:

      ```
      # kubectl exec -it my-openvpn-as-openvpn-as-6b474c9757-jmjlc -- /bin/bash sacli UserPropGet
      {
        "__DEFAULT__": {
          "prop_autogenerate": "true",
          "type": "user_default"
        },
        "admin": {
          "prop_superuser": "true",
          "pvt_password_digest": "$P$zIggWugxu9qv/LZid+np+Q==$0+RZiz4UK6y4c1+A3aG8gBBS08sFAmX3x23U+vxqP1c=",
          "type": "user_compile"
        },
        "openvpn": {
          "prop_superuser": "true",
          "pvt_password_digest": "$P$zYL2GZ3tRzUDw0/rWj6EPA==$QNQmZLQqkR0T1o4drQVkCGLDopr4tY0TIerl2yMmRo0=",
          "type": "user_compile",
          "user_auth_type": "local"
        }
      }
      ```

## Uninstall Access Server

- Run the command below to **uninstall the Access Server Helm Chart**:

  ```
  helm delete my-openvpn-as
  ```
- Run the command below to **remove the Access Server Helm Chart completely**:

  ```
  helm repo remove as-helm-chart
  ```

> **Tip**
>
> Uninstalling the Helm chart removes the deployment, but the Persistent Volume Claim (PVC) may remain. Delete it manually if you want to remove all data:
>
> ```
> kubectl delete pvc my-openvpn-as-openvpn-as-pvc
> ```

## Limitations and known issues

The following are known issues or limitations if you deploy Access Server from a Docker image:

- **Failover mode:** This feature isn't supported.
- **Layer 2 (bridging):** This mode isn't supported.
- **Fixed license keys:** This license key model isn't supported. Using fixed keys can cause license invalidation because the hardware specification fingerprint isn't persistent.
- **DCO:** DCO support depends on the Linux kernel version running on your Docker host:
  - **Linux kernel 6.16 or newer:** The latest Access Server Docker image ships with Access Server 3.2.2, which includes DCO support built into the kernel. No additional installation is needed on the Docker host as Access Server uses DCO automatically.
  - **Linux kernel older than 6.16:** To use DCO, you need to install it manually on the Docker host before running the container. See [Step 1: Install DCO](https://openvpn.net/as-docs/v3/tutorials/tutorial--turn-on-openvpn-dco/#step-1--install-dco--if-needed-) for instructions.
- **Clustering:** You can use Access Servers, deployed from Docker images, to build cluster functionality with the following limitations:
  - You can only run one cluster node per Host system at a time.
  - Hosts must be available directly from the internet, not via a load balancer, proxy, or ingress controller.
- **Performance:** The additional abstraction layer can cause performance degradation and increase latency. We don't recommend deploying highly loaded installations using Docker.
- **PAM authentication method:** We recommend avoiding PAM as your authentication method because user credentials stored inside the container aren't persistent.
- **Logs:** Access Server forwards logs to Docker, so it can't handle logging in the Access Server configuration. See the Docker logging documentation to set up rotation, forwarding, etc.
- **IPv6:** We don't recommend Access Server inside a Docker container if you plan to use IPv6 for VPN clients because IPv6 support in the Docker network toolset is limited/experimental.

## Troubleshooting

[Submit a support ticket](https://support.openvpn.com/) if you need assistance.

- Check pod status:

  ```
  kubectl get pods
  ```
- If a pod fails to start:

  ```
  kubectl describe pod <Pod Name>
  kubectl logs <Pod Name>
  ```

  This displays events, errors, and useful debugging information.

### Common issues:

- `ErrImagePull` — Docker image not found or network issue.
- `CrashLoopBackOff` — Service not starting correctly; check `postStart` scripts.

---

Prev: [Tutorial: How to Use the Access Server AWS Launcher](https://openvpn.net/as-docs/tutorials/tutorial--how-to-use-aws-launcher.md)
Next: [Tutorial: Change the Database Backend to MySQL or Amazon RDS with the Admin Web UI](https://openvpn.net/as-docs/tutorials/tutorial--change-db-backend-gui.md)
Index: https://openvpn.net/as-docs/llms.txt
