---
myst:
  html_meta:
    "description": "Deploy the production-ready Volto example: React frontend, Plone REST API backend, PostgreSQL, Varnish caching, and ingress with TLS."
    "property=og:description": "Deploy the production-ready Volto example: React frontend, Plone REST API backend, PostgreSQL, Varnish caching, and ingress with TLS."
    "property=og:title": "Deploy the Volto example"
    "keywords": "Plone, cdk8s, Kubernetes, Volto, production, PostgreSQL, Varnish, ingress, TLS"
---

# Deploy the Volto example

This guide shows you how to deploy the production-ready Volto example to your Kubernetes cluster.

## What you'll deploy

The [Volto example](https://github.com/bluedynamics/cdk8s-plone/tree/main/examples/volto) includes:

- **Plone 6.1 with Volto** (React frontend + REST API backend)
- **PostgreSQL** with RelStorage (plain PostgreSQL or CloudNativePG)
- **Varnish HTTP caching** with kube-httpcache
- **Ingress** with TLS (Traefik or Kong)
- **Three access domains** (cached, uncached, maintenance)

(volto-prerequisites)=

## Prerequisites

### Required

Ensure you have these installed on your cluster:

1. **Ingress Controller** - Either:
   - [Traefik v3](https://doc.traefik.io/traefik/getting-started/install-traefik/) with CRDs
   - [Kong Gateway](https://docs.konghq.com/gateway/latest/install/kubernetes/)

2. **cert-manager** - For TLS certificates:
   ```shell
   kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml
   ```

3. **kube-httpcache Operator** - For Varnish caching:
   ```shell
   kubectl apply -f https://github.com/mittwald/kube-httpcache/releases/latest/download/kube-httpcache.yaml
   ```

4. **PostgreSQL** - The example provisions the database itself; choose the backend with the `DATABASE` variable in Step 4. Prepare the cluster for your choice:

   **Option A: plain** (default; no operator needed) - the example deploys a single-instance PostgreSQL StatefulSet using the official `postgres` image, so there is nothing to install.

   **Option B: CloudNativePG** (recommended for production HA) - install the operator:
   ```shell
   kubectl apply -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.24/releases/cnpg-1.24.0.yaml
   ```

### Local tools

- **Node.js 18+** and npm
- **kubectl** configured for your cluster
- **git** to clone the repository

## Step 1: Get the example

Clone the repository and navigate to the example:

```shell
git clone https://github.com/bluedynamics/cdk8s-plone.git
cd cdk8s-plone/examples/volto
```

## Step 2: Install dependencies

```shell
npm install
```

## Step 3: Import CRDs

Generate TypeScript bindings for Kubernetes CRDs:

```shell
npm run import
```

This imports:
- Kubernetes core API
- CloudNativePG Cluster CRDs
- Traefik Middleware CRDs

## Step 4: Configure environment

Create a `.env` file from the example:

```shell
cp .env.example .env
```

Edit `.env` with your settings:

```shell
# Your domains
DOMAIN_CACHED=plone.example.com
DOMAIN_UNCACHED=plone-test.example.com
DOMAIN_MAINTENANCE=plone-admin.example.com

# Your cert-manager ClusterIssuer
CLUSTER_ISSUER=letsencrypt-prod

# Optional: Custom images
#PLONE_BACKEND_IMAGE=plone/plone-backend:6.1.3
#PLONE_FRONTEND_IMAGE=plone/plone-frontend:16.0.0

# Database: 'plain' or 'cloudnativepg'
DATABASE=plain
```

:::{tip}
For production high availability, use `cloudnativepg`. For development and small setups, `plain` is simpler and needs no operator.
:::

## Step 5: Generate manifests

Synthesize Kubernetes YAML:

```shell
npm run synth
```

This creates `dist/plone-example.k8s.yaml` with all resources.

## Step 6: Review generated manifests

Inspect what will be deployed:

```shell
# Count resources
grep "^kind:" dist/plone-example.k8s.yaml | sort | uniq -c

# View specific resources
kubectl apply --dry-run=client -f dist/plone-example.k8s.yaml
```

## Step 7: Deploy to Kubernetes

Deploy to your cluster:

```shell
kubectl apply -f dist/plone-example.k8s.yaml
```

Or deploy to a specific namespace:

```shell
kubectl apply -f dist/plone-example.k8s.yaml -n plone
```

## Step 8: Monitor deployment

Watch pods starting:

```shell
# Watch all pods
kubectl get pods -l app.kubernetes.io/part-of=plone -w

# Check specific components
kubectl get pods -l app.kubernetes.io/name=plone-backend
kubectl get pods -l app.kubernetes.io/name=plone-frontend
kubectl get pods -l app.kubernetes.io/name=plone-httpcache
```

Wait for all pods to be `Running`:

```shell
kubectl wait --for=condition=ready pod -l app.kubernetes.io/part-of=plone --timeout=300s
```

## Step 9: Verify services

Check that services are created:

```shell
kubectl get svc -l app.kubernetes.io/part-of=plone
```

You should see:
- `plone-backend` (backend service)
- `plone-frontend` (frontend service)
- `plone-httpcache` (Varnish cache)
- Database service (plain PostgreSQL or CloudNativePG)

## Step 10: Check ingress

Verify ingress routes:

```shell
kubectl get ingress
```

Check TLS certificates:

```shell
kubectl get certificate
```

## Step 11: Access your site

Once DNS is configured and TLS certificates are issued:

- **Public site (cached)**: https://plone.example.com
- **Testing (uncached)**: https://plone-test.example.com
- **Maintenance**: https://plone-admin.example.com

### Create Plone site

On first access to the maintenance domain:

1. Go to: https://plone-admin.example.com
2. Click "Create a new Plone site"
3. Fill in:
   - **Site ID**: `Plone`
   - **Title**: Your site title
   - **Language**: Choose language
4. Click "Create Plone Site"

## Troubleshooting

### Pods not starting

Check pod logs:

```shell
# Backend logs
kubectl logs -l app.kubernetes.io/name=plone-backend -f

# Frontend logs
kubectl logs -l app.kubernetes.io/name=plone-frontend -f

# Database logs (CloudNativePG)
kubectl logs -l postgresql=plone-postgresql -f
```

### Database connection issues

**CloudNativePG:**

```shell
# Check cluster status
kubectl get cluster

# Check secret
kubectl get secret -l cnpg.io/cluster
```

**Plain PostgreSQL:**

```shell
# Check the StatefulSet and pod
kubectl get statefulset -l app.kubernetes.io/name=plone-postgresql
kubectl get pods -l app.kubernetes.io/name=plone-postgresql
```

### TLS certificate issues

```shell
# Check certificate status
kubectl describe certificate

# Check cert-manager logs
kubectl logs -n cert-manager deployment/cert-manager
```

### Varnish cache not working

```shell
# Check httpcache logs
kubectl logs -l app.kubernetes.io/name=plone-httpcache

# Check if kube-httpcache operator is running
kubectl get pods -n kube-httpcache-system
```

## Updating your deployment

After making changes to the example:

1. Edit configuration files
2. Regenerate manifests: `npm run synth`
3. Apply changes: `kubectl apply -f dist/plone-example.k8s.yaml`

## Scaling

Scale replicas by editing `main.ts`:

```typescript
const plone = new Plone(this, 'plone', {
  backend: {
    replicas: 3,  // Scale backend
  },
  frontend: {
    replicas: 3,  // Scale frontend
  },
})
```

Then regenerate and reapply.

## Cleanup

Remove all resources:

```shell
kubectl delete -f dist/plone-example.k8s.yaml
```

## Next steps

- Configure monitoring and metrics through {doc}`enable-prometheus-monitoring`.
- Customize [Varnish caching rules](https://github.com/bluedynamics/cdk8s-plone/blob/main/examples/volto/config/varnish.tpl.vcl).
- Harden pods with {doc}`configure-security-context`.

## See also

- {doc}`deploy-blicca` — For the server-side rendered Plone UI.
- {doc}`setup-prerequisites` — Detailed cluster setup.
- {doc}`/reference/configuration-options` — API reference.
