einfra logoDocumentation
Case studies

How to Run an NGINX Web Server

Deploying an NGINX web server is a common way to test deploying an application or service into Kubernetes. While NGINX is straightforward to run and offers a ready-to-use Docker image, deployments can sometimes fail in restricted environments.

This guide provides step-by-step instructions to ensure your NGINX deployment works on the first try.

Privileged vs. Unprivileged

This Kubernetes platform is multitenant, meaning it is shared among many users. To maintain security, privilege escalation is disabled. You must configure a security context to run containers in unprivileged mode.

Instead of building a custom image to meet these restrictions, you can use the official unprivileged NGINX image.

  • Image Reference: nginxinc/nginx-unprivileged
  • This image is fully compatible with our platform as-is and requires no modifications.

Running User ID

The security context requires a specific user ID and group ID (configured via the runAsUser and runAsGroup fields). For the nginx-unprivileged image, the correct value for both is 101.

You can verify this using docker:

docker run -it --rm --entrypoint /bin/sh nginxinc/nginx-unprivileged
$ id
uid=101(nginx) gid=101(nginx) groups=101(nginx)
$

This example assumes the image contains /bin/sh and id binaries. Mainly containerized Go Lang applications usualy contain only the Go application and nothing else. In such a case, Dockerfile used for image build or project documentation must be consulted.

Deployment Manifest

Use the following manifest to deploy your NGINX instance.

  • Name: The name nginx can be replaced with any valid string (lowercase letters, numbers, dashes).
  • Resources: Adjust the memory and CPU requests/limits according to your expected usage.
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      securityContext:
        fsGroupChangePolicy: OnRootMismatch
        fsGroup: 101
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      containers:
      - name: nginx
        image: nginxinc/nginx-unprivileged
        imagePullPolicy: IfNotPresent
        securityContext:
          runAsUser: 101
          runAsGroup: 101
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL
        resources:
          requests:
            cpu: 500m
            memory: 2Gi
          limits:
            cpu: 2
            memory: 8Gi

Adding Persistent Storage

The deployment above works, but it only serves the default NGINX “hello” page. The container’s web root (/usr/share/nginx/html) is not writable by default, meaning you cannot copy data into it after it starts.

The optimal way to serve your own content is by attaching a Persistent Volume Claim (PVC). Read more about PVC.

  1. Create the PVC

First, create an ad-hoc PVC using the following manifest. Data on this PVC is preserved until the PVC is explicitly deleted.

pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-nginx
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 2Gi
  storageClassName: nfs-csi
  1. Update the Deployment

Once the PVC is created, update your deployment manifest to mount the volume to the NGINX web root: Data on this PVC are preserved until the PVC is deleted.

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      securityContext:
        fsGroupChangePolicy: OnRootMismatch
        fsGroup: 101
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      containers:
      - name: nginx
        image: nginxinc/nginx-unprivileged
        imagePullPolicy: IfNotPresent
        securityContext:
          runAsUser: 101
          runAsGroup: 101
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL
        resources:
          requests:
            cpu: 500m
            memory: 2Gi
          limits:
            cpu: 2
            memory: 8Gi
        volumeMounts:
        - name: vol-1
          mountPath: /usr/share/nginx/html
      volumes:
      - name: vol-1
        persistentVolumeClaim:
          claimName: test-nginx

The PVC name defined in pvc.yaml (name: test-nginx) must exactly match the claimName (claimName: test-nginx).

Directory indexing is disabled by default in NGINX. If you do not upload an index.html file to the PVC, opening the URL in a browser will return a 403 Forbidden error.

Exposing the NGINX Server

To make the running NGINX instance accessible from the internet, you need to expose it. For deeper details, see the Exposing applications section.

You will need two additional manifests: a Service and an Ingress.

Service

The Service routes internal traffic to your running Pod. It relies on the application label (app: nginx in our deployment) and the target port. The nginx-unprivileged image listens on port 8080 by default. See nginx-unprivileged documentation.

service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
  selector:
    app: nginx

Ingress

The Ingress manifest configures the external hostname and provisions a Let’s Encrypt TLS certificate for secure HTTPS access.

ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx
  annotations:
    kubernetes.io/tls-acme: "true"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
   - hosts:
       - "[some-name].dyn.cloud.e-infra.cz"
     secretName: [some-name]-dyn-cloud-e-infra-cz
  rules:
  - host: [some-name].dyn.cloud.e-infra.cz
    http:
      paths:
        - pathType: Prefix
          path: "/"
          backend:
            service:
              name: nginx
              port:
                number: 8080

Configuration Notes

Replace the [some-name] with a unique identifier for your project. Do not use generic names such as test, example, as they are likely already taken.

The backend.service.name and port.number in the Ingress manifest must exactly match the name and port defined in your Service object.

Last updated on

publicity banner

On this page

einfra banner