{"id":17930915,"url":"https://github.com/rinmeister/k8s-nginx-wordpress","last_synced_at":"2026-04-11T11:01:53.811Z","repository":{"id":259017502,"uuid":"876039355","full_name":"rinmeister/k8s-nginx-wordpress","owner":"rinmeister","description":"A repository with a k8s implementation of wordpress using NGINX, MySQL and PHP in kubernetes","archived":false,"fork":false,"pushed_at":"2024-10-21T12:54:21.000Z","size":188,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-03T10:28:57.536Z","etag":null,"topics":["kubernetes","mysql","nginx","php","wordpress"],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rinmeister.png","metadata":{"files":{"readme":"README.adoc","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-10-21T09:49:36.000Z","updated_at":"2024-10-21T12:54:25.000Z","dependencies_parsed_at":"2024-10-22T14:54:15.144Z","dependency_job_id":null,"html_url":"https://github.com/rinmeister/k8s-nginx-wordpress","commit_stats":null,"previous_names":["rinmeister/k8s-nginx-wordpress"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/rinmeister/k8s-nginx-wordpress","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rinmeister%2Fk8s-nginx-wordpress","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rinmeister%2Fk8s-nginx-wordpress/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rinmeister%2Fk8s-nginx-wordpress/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rinmeister%2Fk8s-nginx-wordpress/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rinmeister","download_url":"https://codeload.github.com/rinmeister/k8s-nginx-wordpress/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rinmeister%2Fk8s-nginx-wordpress/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31677819,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-11T08:18:19.405Z","status":"ssl_error","status_checked_at":"2026-04-11T08:17:08.892Z","response_time":54,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["kubernetes","mysql","nginx","php","wordpress"],"created_at":"2024-10-28T21:18:34.937Z","updated_at":"2026-04-11T11:01:53.768Z","avatar_url":"https://github.com/rinmeister.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"== Implementing NGINX in Kubernetes \n\nI wrote this post because the implementation became quite complex. You should be\nprepared to do some debugging and troubleshooting. Using this guide will get you\nthrough as well as help you understand how it is supposed to work.\n\n=== A deployment with wordpress, mysql and php.\n\nMy setup is a 3 node cluster with 1 master and 2 worker nodes. Harware is\nraspberry pi 4 with 4 GB RAM and a 500 GB SSD for each node. The OS is Ubuntu\nserver 24.04.1 LTS. Kubernetes is k3s version is 1.30.3+k3s1.\n\nI ran in to a lot of problems with the wordpress deployment in particular. I\nwill try to document the problems and the solutions here. It took me a long time\nfiguring some of the problems out and the guides I found on the internet did not\nseem have any of the problems I had. I hope this blog will be helpful to others.\n\nFirs I will give a description of all components of the entire implementation\nwith some background information and specific problems I encountered. After that\nI will give a step by step guide on how to implement the entire setup.\n\n=== Prerequisites\n\n. Obviously you need to have a kubernetes cluster. I used k3s on raspberry pi 4.\n\n. Persistent Volume manager. I use _Longhorn_. I am writing anothrer post about\n  longhorn. For now refer to the official longhorn documentation.\n\n. Loadbalancer. I use _MetalLB_. I have plpans to write a blogpost about\n  MetalLB. For now please refer to the official MetalLB documentation to install\n  it in your cluster. You will also need a configMap file that includes a\n  separate addresspool called _web_. That name is used here in the\n  documentation.\n\n. Ingress controller. I use _NGINX-ingress_. Please use the official\n  documentation to install this in your cluster.\n\n. This guide assumes these components. You can of course use other solutions (like\nCEPH, Traefik etc.) but these will not be covered in this guide.\n\n. To follow the guide note that I created an alias for _k=kubectl_ (too much typing).\n\n. A docker account is assumed. The account name in this post is referenced as\n*\u003cdocker-uid\u003e*. You need to replace this with your own docker account name. Of\ncourse you can also us another repository or store files locally.\n\n=== Quick deployment\n\nIf you just want to deploy the entire setup without reading this entire blogpost\nyou can just grab all the Dockerfiles, yaml files from the repo and put them in\nthe same directory. Just clone the repo is even better.\n\nYou need:\n\n- Dockerfile-nginx\n- Dockerfile-phpfpm\n- ln-files.sh\n- production_issuer.yaml\n- staging_issuer.yaml\n- phpfpm-deployment.yaml\n- phpfpm-clusterip.yaml\n- mysql-manifest.yaml\n- mysql-deployment.yaml\n- mysql-secret.yaml\n- nginx-ingress.yaml\n- nginx-ingress-svc.yaml\n- nginx-deployment.yaml\n- nginx-manifest.yaml\n\nSome files in the repository have an _.example_ extension. This means there is\ninformation in the file you have to change for your own setup. Then save these files\nwithout the example extension.\nModify each file so it represents your own setup. File paths and directories may\ndiffer from mine. Also you have to change the domains in the files to your own\netc.\n\nStart by building the containers (here for arm64 architecture).  I assume you\nhave a sites-available directory copied from the nginx server you are migrating\nfrom. In the repo I included an example sites-available directory with two\nexample domains.\n\n----\ndocker buildx build --platform linux/arm64 \\\n-t docker.io/\u003cdocker-uid\u003e/\u003cnginx-image-name\u003e:latest -f Dockerfile-nginx .\n\ndocker buildx build --platform linux/arm64 \\\n-t docker.io/\u003cdocker-uid\u003e/\u003cphpfpm-image-name\u003e:latest -f Dockerfile-phpfpm .\n----\n\nPush the images to docker.io\n\n----\ndocker push docker.io/\u003cdocker-uid\u003e/\u003cnginx-image-name\u003e:latest\ndocker push docker.io/\u003cdocker-uid\u003e/\u003cphpfpm-image-name\u003e:latest\n----\n\nApply the yaml files. Preferably in this order.\n\n----\nk apply -f mysql-secret.yaml \\\nk apply -f mysql-deployment.yaml \\\nk apply -f mysql-manifest.yaml \\\nk apply -f php-deployment.yaml \\\nk apply -f php-clusterip.yaml \\\nk apply -f nginx-ingress.yaml \\\nk apply -f nginx-ingress-svc.yaml \\\nk apply -f production_issuer \\\nk apply -f staging_issuer \\\nk apply -f nginx-deployment.yaml \\\nk apply -f nginx-manifest.yaml \n----\nYou can also put all yaml files in one directory and apply the directory.\n\nThis should bring the entire deployment up. If things don't work as expected I\nsuggest you read the entire blogpost to understand the setup and the problems I\nencountered. With the help of this information you should definitely be able to\nunderstand what is going on an fix the problems. If everything works in one go,\ncongratulations! Maybe you still want to read the rest in case you run into some\nproblem later on.\n\n=== Docker containers\n\n==== NGINX\n\nFirst I created a Dockerfile for NGINX. I used the official NGINX image. I\nalready had this configuration running on a VM. It was a server implementation\nwith a couple of virtual hosts. I decided to just copy the configuration files\nin the Dockerfile. I did not copy the nginx.conf file because I wanted to use a\nconfigmap in Kubernetes for that. I upload every container to DockerHub. These\ncontainers are marked \"Private\" as they contain information that is strictly for\nmy use only. The Dockerfile looks like this:\n\n.Dockerfile-nginx\n[source, docker]\n----\nFROM nginx:latest\nLABEL maintainer=\"john@doe.com\"\nRUN mkdir -p /etc/nginx/sites-available\nRUN mkdir -p /etc/nginx/sites-enabled\nCOPY ./sites-available /etc/nginx/sites-available\nCOPY ./ln-files.sh /root/ln-files.sh\nRUN root/ln-files.sh\n\nEXPOSE 443/tcp\nEXPOSE 80/tcp\n----\n\nthe ln-files.sh script is a simple script that creates symbolic links in the\nsites-enabled directory. The script looks like this:\n\n.ln-files.sh\n[source, bash]\n----\n#!/bin/bash\n\ndeclare -a bestanden\nfor file in /etc/nginx/sites-available/*\ndo\n    bestanden=(\"${bestanden[@]}\" \"$(basename $file)\")\ndone\necho ${bestanden[@]}\nfor bestand in ${bestanden[@]}\ndo\n    ln -s /etc/nginx/sites-available/$bestand /etc/nginx/sites-enabled/$bestand\ndone\n----\n\nI build the image for arm64 and pushed it to DockerHub as a private container.\nThis image can now be used in the deployment file.\n\n==== PHP\n\nNext there is a container for PHP. Here I also created a custom image derived\nfrom the official php:fpm-alpine image. It is nothing fancy but you need extra\npackages in the container to run wordpress. These are php extensions for mysqli,\nexif and gd. I stumbled across this myself when I tried to run wordpress\nwithouth these extensions. It generates errors that point to these extensions.\nThe Dockerfile looks like this:\n\n.Dockerfile-php\n[source, docker]\n----\nFROM php:fpm-alpine\nRUN apk add libpng-dev\nRUN docker-php-ext-install mysqli\nRUN docker-php-ext-install exif\nRUN docker-php-ext-install gd\n----\n\nIt is also a build for ARM64. I pushed to Dockerhub as a public repository. So\nyou can save yourself the trouble of building this image yourself and just use\nmine. The image can be pulled at:\n\n----\nrinmeister/phpfpm-mysqli:latest\n----\n\n==== MySQL\n\nFinally you also need an MySQL container. For this I just used the official\nimage: _mysql:latest_. So I did not create a custom image for this. The\ndatabases are stored in a persistent volume and are imported in the database\nafter it has been deployed. More about that later.\n\n=== Setting up MySQL in kubernetes\n\nThe requirements for SQL:\n\n- support for multiple databases\n- support for multiple users.\n\nI did not succeed in setting up multiple users. I can set up a root user and one\nadministrative account but that's it. So right now I am using that. If a need\nanother user I set it up after the container is running in MySQL. The database\nis stored in a persistent volume and so is the user configuration.\n\nI use two separate yaml files for the deployment. In the first one I create the\nservice and the persistent volume. This is called the manifest file. The second\nis the deployment file. They are separate so I can delete the deployment without\ndeleting the persistent volume. Here are both files:\n\n==== MySQL manifest file\n\n.mysql-manifest.yaml\n[source, yaml]\n----\napiVersion: v1\nkind: Service\nmetadata:\n  name: wordpress-mysql\n  labels:\n    app: wordpress\nspec:\n  type: ClusterIP\n  selector:\n    app: wordpress\n    tier: mysql\n  ports:\n    - port: 3306\n  #clusterIP: None\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: mysql-pv-claim\n  labels:\n    app: wordpress\nspec:\n  accessModes:\n    - ReadWriteOnce\n  storageClassName: longhorn\n  resources:\n    requests:\n      storage: 2Gi\n----\n\nThe service selects all pods the have _app: wordpress_ and _tier: mysql_ labels.\nFor these pods the service offers a ClusterIP address on port 3306. The\npersistent volume claim is made on the longhorn storage class. I am running\nlonghorn in my cluster and is a prerequisite. Longhorn takes physical disks from\nthe nodes and creates one pool of storage. This pool is then referred to as a\nstorage class. Deployments and thus Pods can use this storage by using the\n_name: mysql-pv-claim_ in the volume section of the deployment.\n\n\n==== MySQL deployment \n\nWith these files it is easy to understand the deployment file:\n\n.mysql-deployment.yaml\n[source, yaml]\n----\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: wordpress-mysql\n  labels:\n    app: wordpress\nspec:\n  selector:\n    matchLabels:\n      app: wordpress\n      tier: mysql\n  strategy:\n    type: Recreate\n  template:\n    metadata:\n      labels:\n        app: wordpress\n        tier: mysql\n    spec:\n      containers:\n      - image: mysql:latest\n        name: mysql\n        env:\n        - name: MYSQL_ROOT_PASSWORD\n          valueFrom:\n            secretKeyRef:\n              name: mysql-pass-gd6fh98b8f\n              key: password\n        - name: MYSQL_DATABASE\n          value: wordpress\n        - name: MYSQL_USER\n          value: wordpress\n        - name: MYSQL_PASSWORD\n          valueFrom:\n            secretKeyRef:\n              name: mysql-pass-gd6fh98b8f\n              key: password\n        ports:\n        - containerPort: 3306\n          name: mysql\n        volumeMounts:\n        - name: mysql-persistent-storage\n          mountPath: /var/lib/mysql\n      volumes:\n      - name: mysql-persistent-storage\n        persistentVolumeClaim:\n          claimName: mysql-pv-claim\n----\n\nThe deployment selects pods with the _app: wordpress_ and _tier: mysql_ labels.\nThese labels come back in the template section under metadata. The container is\nthe latest official image. A number of _docker environment_ variables are set in the\ncontainer to be used in MySQL. These are variables that give flexibility to the\ncontainer implementation.\n\n- MYSQL_ROOT_PASSWORD: sets the root password for the MySQL database\n- MYSQL_DATABASE: sets the database name\n- MYSQL_USER: sets the user name\n- MYSQL_PASSWORD: sets the password for the user\n\nThe password is stored in a secret. This secret can be created using the\nfollowing file:\n\n.mysql-secret.yaml\n[source, yaml]\n----\napiVersion: v1\nkind: Secret\nmetadata:\n  name: mysql-pass-gd6fh98b8f\ntype: Opaque\ndata:\n  password: cGFzc3dvcmQ=\n----\n\nNote: The password must be a base64 encoded string. In my case the same password\nis used for the root user and the wordpress user. This is not a best practice.\nMake sure in a production environment to use different passwords.\n\nNote: Opaque means arbitrary user-defined data.\n\nDuring the configuration of the different domains we will import the databases\nand create more users. You can find this later in the document.\n\nThe container listens on port 3306 and mounts the persistent volume\n_mysql-pv-claim_ on /var/lib/mysql in the container.\n\n\n=== Setting op NGINX in Kubernetes\n\nThe requirements for the webserver are:\n\n- A webserver for:\n    - example1.com\n    - example2.com\n- Secure connection to all domains using Let's Encrypt\n- Redirect all http traffic to https\n- Nginx configuration should be easily changeable\n- Content must survive a reboot or a crash of the container\n\nI selected NGINX as the webserver of choice. I run it also on the VM that is\ncurrently in use and it runs fine. I am familiar with its configuration so there\nwas no need to change that setup. Moreover I can retain the configuration files\nand just use them in the container. The _nginx.conf_ file is used in a configMap\nin k8s. \n\n==== NGINX\n\nI use the self created private docker container I created \u003c\u003c_nginx,earlier\u003e\u003e.\nThe deployment consists of two files: a deployment with just the _deployment_\nsection and a manifest file with the _service_ the _configMap_ and the\n_persistentVolumeClaim_ sections. Separating the deployment from the rest makes\nit easy to delete the NGINX deployment but keep the persistent files. \n\nThe deployment file is as follows:\n\n.nginx-deployment.yaml\n[source, yaml]\n----\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: nginx\n  template:\n    metadata:\n      labels:\n        app: nginx\n      annotations:\n        prometheus.io/scrape: \"true\"\n        prometheus.io/port: \"9113\"\n    spec:\n      containers:\n      - name: nginx\n        image: \u003cdocker-uid\u003e/\u003cnginx-image-name\u003e:latest\n        env:\n        - name: WORDPRESS_DB_HOST\n          value: wordpress-mysql\n        - name: WORDPRESS_DB_PASSWORD\n          valueFrom:\n            secretKeyRef:\n              name: mysql-pass-gd6fh98b8f\n              key: password\n        - name: WORDPRESS_DB_USER\n          value: wordpress\n        ports:\n        - containerPort: 80\n        volumeMounts:\n        - mountPath: /etc/nginx/nginx.conf # mount nginx-conf volumn to /etc/nginx\n          readOnly: true\n          name: nginx-conf\n          subPath: nginx.conf\n        - mountPath: /var/log/nginx\n          name: log\n        - mountPath: /var/www\n          name: longhorn-pvc\n      imagePullSecrets:\n      - name: regcred\n      volumes:\n      - name: nginx-conf\n        configMap:\n          name: nginx-conf # place ConfigMap `nginx-conf` on /etc/nginx\n          items:\n            - key: nginx.conf\n              path: nginx.conf\n      - name: log\n        emptyDir: {}\n      - name: longhorn-pvc\n        persistentVolumeClaim:\n          claimName: nginx-pvc\n----\n\nThere is a prometheus section in the file that is optional. The _env_ section\ngets the information needed to login to the MySQL database. This was configured\n\u003c\u003c_mysql_deployment, here\u003e\u003e. The container port is 80, but everything is going\nto be redirected to 443 by the ingress and certmanager. Beware that this means\nthat the SSL connection is terminated at ingress and all communication inside\nthe cluster is http. In the container both TCP 80 and 443 have been opened.\nThere are three volumes mounted in the container:\n\n- /etc/nginx/nginx.conf: the configuration file for NGINX\n- /var/log/nginx: the log files for NGINX\n- /var/www: the webroot for NGINX\n\nthe section _imagePullSecrets_ is used to pull the image from a private\nregistry. This is not needed if the image is in a public registry. See\n\u003c\u003c_reading_from_a_private_docker_repository, this\u003e\u003e section to to set this up.\n\nThe first volumeMount is a _configMap_ that is created in the manifest file. The\nname of the _configMap_ is _nginx-conf_. The second volume is an _emptyDir_. The\nthird volume is a _persistentVolumeClaim_. This is also created in the manifest\nfile. Basically this is where the data is going to be copied. It has to be a\npersistent volume so it survives a reboot or a crash of the container. Both this\ncontainer and the php container use this volume with the name _nginx-pvc_.\n\nThe manifest file looks like below:\n\n.nginx-manifest.yaml\n[source, yaml]\n----\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: nginx-conf\ndata:\n  nginx.conf: |\n    user www-data;\n    worker_processes auto;\n    pid /run/nginx.pid;\n    #pid /tmp/nginx.pid;\n    include /etc/nginx/modules-enabled/*.conf;\n\n    events {\n      worker_connections 768;\n    }\n\n    http {\n      client_body_temp_path /tmp/client_temp;\n      proxy_temp_path       /tmp/proxy_temp_path;\n      fastcgi_temp_path     /tmp/fastcgi_temp;\n      uwsgi_temp_path       /tmp/uwsgi_temp;\n      scgi_temp_path        /tmp/scgi_temp;\n\n      sendfile on;\n      tcp_nopush on;\n      tcp_nodelay on;\n      keepalive_timeout 65;\n      types_hash_max_size 2048;\n\n      server_names_hash_bucket_size 64;\n\n      include /etc/nginx/mime.types;\n      default_type application/octet-stream;\n\n\n      ##\n      # SSL Settings\n      ##\n\n      ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE\n      ssl_prefer_server_ciphers on;\n\n      ##\n      # Logging Settings\n      ##\n\n      access_log /var/log/nginx/access.log;\n      error_log /var/log/nginx/error.log;\n\n      ##\n      # Gzip Settings\n      ##\n\n      gzip on;\n\n      # gzip_vary on;\n      # gzip_proxied any;\n      # gzip_comp_level 6;\n      # gzip_buffers 16 8k;\n      # gzip_http_version 1.1;\n      # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;\n\n      ##\n      # Virtual Host Configs\n      ##\n\n      include /etc/nginx/conf.d/*.conf;\n      include /etc/nginx/sites-enabled/*;\n    }\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: nginx-pvc\nspec:\n  accessModes:\n    - ReadWriteOnce\n  storageClassName: longhorn\n  resources:\n    requests:\n      storage: 2Gi\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: nginx-service\nspec:\n  type: ClusterIP\n  ports:\n  - name: http\n    port: 80\n    targetPort: 80\n  selector:\n    app: nginx\n----\n\nThe data section is a configMap with the nginx.conf data. This is used in the\nvolumeMount in /etc/nginx/nginx.conf.\n\nThe second section is a _persistentVolumeClaim_ with the name _nginx-pvc_. It is\na longhorn persistent volume with a size of 2GB.\n\nThe third section is a _service_ with the name _nginx-service_. This is a\nCluserIP type. We do not need to expose this service to the outside world, that\nis done by the ingress. We do need to expose the pods as a service internally\nof course and that is what this service is for. Selector is _app: nginx_. This\nmeans that this service will look for pods with the label _app: nginx_ and will\nput them in its service list of pods.\n\nIn the next section I will explain more about ingress and how to expose the\nservice to the outside world.\n\n==== Loadbalancer and Ingress\n\nMy NGINX implementation is one server that uses virtual hosts to server multiple\ndomains. To get traffic from external networks into Kubernetes you have to use\npreferably a _loadbalancer_. In cloud environments this is a service that is\nprovided by the cloud provider. In my case I am running my cluster at home and I\nhave to use something else. I use _MetalLB_ for this. MetalLB provides external\naddresses that point to services in the cluster. Check my blogpost about MetalLB\nhere.\n\nIngress is a way to route traffic from the outside to services in the cluster.\nIt is true that MetalLB can do the same but Ingress is much more flexible. What\nIngress *cannot* do is provide you with an external address. You need a\nLoadbalancer ore a Nodeport for that. Ingress could be really useful for example\nwhen you want to route traffic to different services (like different webservers)\nbased on the URL. In my case that is not really necessary because I use virtual\nhosts on the same NGINX webserver. So why still use Ingress That is because it\nhas a really nice integration with Let's Encrypt. You can use the _cert-manager_\nto automatically request and renew certificates for your domains. As SSL\ncertificates are an absolute must for all websites I decided to use ingress. The\ningress implementation I use is NGINX Ingress. Kubernetes has a default Ingress\nimplementation with Traefik but the documentation is less elaborate and I am\nmore familiar with NGINX anyway.\n\nThe following pictures help to understand Loadbalancer and Ingress better. It is\na description of my implementation but I think it is a useful visualization for\neveryone. The first picture shows that the MetalLB hands out an external address\nto the NGINX ingress service (1). Services run throughout the cluster and are not\nspecific to a pod or a node. A client from the \"outside\" connects to an url that\nis resolved to the external address of the NGINX ingress servicei (2). MetalLB has\nelected a speaker that handles the load-balanced traffic and that actually\nannounces the external IP address (3). So traffic to the external IP address are\nrouted to the node with the speaker that announces the IP address. After the\nnode receives the packets, the service proxy routes the packets to ann endpoint\nfor the service (4). The NGINX service will send the traffic to a pod that qualifies\nfor the labels that are in its selector. In my case this is the 10.42.0.34.\n\n.Loadbalancer to Ingress\nimage::./images/k8s-loadbalancer.drawio.png[\"Loadbalancer to Ingress\",align=\"center\"]\n\nreference to: https://docs.openshift.com/container-platform/4.9/networking/metallb/about-metallb.html\n\n\nThe second picture follows from the first. We have established how traffic gets\nfrom the external address to the NGINX-ingress POD. From there a rule describes\nto which service an URL should be routed (1). In my case this is one service (I run\none NGINX server with virtual hosts remember, they all run  behind the same\nservice). This NGINX service has, again in my case, one endpoint and that is the\nPOD where the NGINX container runs (2). For clarity: this is the NGINX webserver\ncontainer, not the NGINX ingress pod.\n\n[id=_ingress_to_nginx_pod]\n.Ingress to NGINX Pod\nimage::./images/k8s-ingress.drawio.png[\"Ingress to service\",align=\"center\"]\n\n\n==== Ingress\n\nI assume you have the ingress controller deployed. The file below is an ingress\nfile that is applied in the namespace where NGINX is running. It configures the\ningress controller for our webservice. First I will give you the file, followed\nby an explanation of the different sections.\n\n.nginx-ingress.yaml\n[source, yaml]\n----\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: nginx-ingress\n  annotations:\n    cert-manager.io/cluster-issuer: \"letsencrypt-prod\"\n    kubernetes.io/ingress.class: \"nginx\"\nspec:\n  tls:\n  - hosts:\n    - example1.com\n    - www.example1.com\n    secretName: example1-secret\n  - hosts:\n    - example2.com\n    - www.example2.com\n    secretName: example2-secret\n  rules:\n  - host: example1.com\n    http:\n      paths:\n      - path: \"/\"\n        pathType: Prefix\n        backend:\n          service:\n            name: nginx-service\n            port:\n              number: 80\n  - host: www.example1.com\n    http:\n      paths:\n      - path: \"/\"\n        pathType: Prefix\n        backend:\n          service:\n            name: nginx-service\n            port:\n              number: 80\n  - host: example2.com\n    http:\n      paths:\n      - path: \"/\"\n        pathType: Prefix\n        backend:\n          service:\n            name: nginx-service\n            port:\n              number: 80\n  - host: www.example2.com\n    http:\n      paths:\n      - path: \"/\"\n        pathType: Prefix\n        backend:\n          service:\n            name: nginx-service\n            port:\n              number: 80\n----\n\nThere is an annotation that points to _cert-manager_. This is an\nannotation for the cert-manager controller issuer-shim that will be explained in\nthe next section. You can see that ingress is tied to a production letsencrypt\nissuer. +\nThe _tls_section_ is used to *create* and *connect* a certificate to a domain\nname or SAN (Subject Alternative Name).\n\nThe _rules_ section is used to route traffic to different services.\nPlease observe that in the rule section all host URLs point to the same\nservice (nginx-service). This is because I run one NGINX server with a number of\nvirtual hosts. All traffic is forwarded to the service _nginx-service_ on\nTCP/80.\n\nThe ingress controller is our entry into the cluster. It must have an external\nIP adress and a configuration that selects the Pods that run _ingress-nginx_ and\ntherefore have this label. To enable this we need a service yaml. You can see\nhow this works from the \u003c\u003c_ingress_to_nginx_pod,drawing\u003e\u003e. The service file\nlooks like below.\n\n.nginx-ingress-svc.yaml\n[source, yaml]\n----\napiVersion: v1\nkind: Service\nmetadata:\n  annotations:\n    metallb.universe.tf/address-pool: web\n  labels:\n    helm.sh/chart: ingress-nginx-4.11.2\n    app.kubernetes.io/name: ingress-nginx\n    app.kubernetes.io/instance: ingress-nginx\n    app.kubernetes.io/version: 1.30.3\n    app.kubernetes.io/managed-by: Helm\n    app.kubernetes.io/component: controller\n  name: ingress-nginx-controller\n  namespace: ingress-nginx\nspec:\n  type: LoadBalancer\n  externalTrafficPolicy: Local\n  ports:\n    - name: http\n      port: 80\n      protocol: TCP\n      targetPort: http\n    - name: https\n      port: 443\n      protocol: TCP\n      targetPort: https\n  selector:\n    app.kubernetes.io/name: ingress-nginx\n    app.kubernetes.io/instance: ingress-nginx\n    app.kubernetes.io/component: controller\n----\n\nHere you can see that this is a service of type LoadBalancer. As an address we\nwant an address from the pool _web_. This is an address-pool configured the\nMetalLB configuration. This addresspool consists of one IP address so we are\nsure that the service will always get the same external IP address. This is\nimportant because we need to point our DNS records to this address and we do not\nwant to change that around all the time. + \nNext you will see that the service listens to two ports 80 and 443. All SSL\ntraffic is terminated on the ingress controller and is forwarded, _unencrypted_,\non port 80. The selector has three entries and on the pod you have to see all\nthese three labels being present. Only then the pod will be registered into this\nservice. To check that describe the service and check the endpoints. It is also\npossible to describe the pod and compare the labels. In the output below you can\nsee that the service registered the endpoint 10.42.0.34. This is the ingress pod\nwhich you can see in the output of the describe pod command.\n\n[source, bash]\n----\n❯ k describe svc ingress-nginx-controller\nName:                     ingress-nginx-controller\nNamespace:                ingress-nginx\nLabels:                   app.kubernetes.io/component=controller\n                          app.kubernetes.io/instance=ingress-nginx\n                          app.kubernetes.io/managed-by=Helm\n                          app.kubernetes.io/name=ingress-nginx\n                          app.kubernetes.io/part-of=ingress-nginx\n                          app.kubernetes.io/version=1.30.3\n                          helm.sh/chart=ingress-nginx-4.11.2\nAnnotations:              meta.helm.sh/release-name: ingress-nginx\n                          meta.helm.sh/release-namespace: ingress-nginx\n                          metallb.universe.tf/address-pool: web\n                          metallb.universe.tf/ip-allocated-from-pool: web\nSelector:                 app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx\nType:                     LoadBalancer\nIP Family Policy:         SingleStack\nIP Families:              IPv4\nIP:                       10.43.31.2\nIPs:                      10.43.31.2\nLoadBalancer Ingress:     10.10.1.64\nPort:                     http  80/TCP\nTargetPort:               http/TCP\nNodePort:                 http  30558/TCP\nEndpoints:                10.42.0.34:80\nPort:                     https  443/TCP\nTargetPort:               https/TCP\nNodePort:                 https  32599/TCP\nEndpoints:                10.42.0.34:443\nSession Affinity:         None\nExternal Traffic Policy:  Local\nHealthCheck NodePort:     32684\nEvents:                   \u003cnone\u003e\n\n❯ k get pods -o wide\nNAME                                       READY   STATUS    RESTARTS     AGE   IP           NODE       NOMINATED NODE   READINESS GATES\ningress-nginx-controller-55dd9c5f4-lkx8l   1/1     Running   8 (9d ago)   16d   10.42.0.34   k-master   \u003cnone\u003e           \u003cnone\u003e\n----\n\n==== Cert-manager\n\nAs said before, NGINX ingress has a nice integration with Let's Encrypt. In fact\nit is the top reason why we use an ingress controller for this implementation.\nHanding out and maintaining LetsEncrypt certificates is done through\n_cert-manager_, a Kubernetes add-on that automates the management and issuance of\nTLS certificates.\n\nI installed certmanager with a helm chart. We are currently at version 1.15.3\nbut please check for current versions when you read this. The installation is\ndone with the following command:\n\n----\nhelm repo add jetstack https://charts.jetstack.io\nhelm repo update\nhelm install cert-manager jetstack/cert-manager --namespace cert-manager --version v1.15.3 --set installCRDs=true\n----\n\nor follow: https://cert-manager.io/docs/installation/helm/\n\nThis will install cert-manager in the namespace cert-manager. You should see the\nfollowing services and pods:\n\n----\n❯ k get svc\nNAME                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE\ncert-manager           ClusterIP   10.43.171.145   \u003cnone\u003e        9402/TCP   31d\ncert-manager-webhook   ClusterIP   10.43.159.163   \u003cnone\u003e        443/TCP    31d\n\n❯ k get pods\nNAME                                       READY   STATUS    RESTARTS       AGE\ncert-manager-9647b459d-wxnmq               1/1     Running   7 (10d ago)    11d\ncert-manager-cainjector-5d8798687c-ffrkw   1/1     Running   14 (10d ago)   11d\ncert-manager-webhook-c77744d75-4hrn5       1/1     Running   9 (4d4h ago)   11d\n----\n\nA lot of documentation can be found about cert-manager and how to us it. In most\ndescriptions there is a staging issuer and a production issuer. I tried the\nstaging issuer first and that worked fine. In the final implementation I\ndescribe here I just use the production issuer. I had a lot of trouble with\nissuing certificates but we will get in to that later.\n\nThe way cert-manager works is by using an ingress-shim. A shim can be looked at\nas a side-car to container process. It is a process that runs alongside the main \nprocess and does some work for it. In this case the ingress-shim watches the\ningress resources. If it sees an Ingress with the right annotations it will\ninstall and maintain a certificate with the name provided in the certificate\ndefinition. The annotation has to be put in the ingress definition. In my case\nthis is:\n\n----\nannotations:\n    cert-manager.io/cluster-issuer: \"letsencrypt-prod\"\n----\n\nIn the namespace of NGINX you now need to create the production ClusterIssuer.\nThis is a kubernetes yaml file with the kind: ClusterIssuer. ClusterIssuers\nrepresent Certificate Authorities. Let's Encrypt is such an authority. Through a\nprocess of validation they can verify and vouch for the authenticity of your\ndomain. \n\nSource: https://cert-manager.io/docs/concepts/issuer/\n\nBelow is the yaml file for the ClusterIssuer in my setup. I called it\n_production_issuer.yaml_. For completeness sake I als include the\n_staging_issuer.yaml_ that I used for testing but which I do not use in the\nfinal implementation.\n\n\n.production_issuer.yaml\n[source, yaml]\n----\napiVersion: cert-manager.io/v1\nkind: ClusterIssuer\nmetadata:\n  name: letsencrypt-prod\n  namespace: cert-manager\nspec:\n  acme:\n    # The ACME server URL\n    server: https://acme-v02.api.letsencrypt.org/directory\n    # Email address used for ACME registration\n    email: john@doe.com\n    # Name of a secret used to store the ACME account private key\n    privateKeySecretRef:\n      name: letsencrypt-prod\n    # Enable the HTTP-01 challenge provider\n    solvers:\n    - http01:\n        ingress:\n          class: nginx\n----\n\nACME stands for Automated Certificate Management Environment. It is a protocol\nfor automating certificate lifecycle management communication between a CA and a\nWeb server.\n\nsource https://www.sectigo.com/resource-library/what-is-acme-protocol\n\nThis file points to the CA which in our case is LetsEncrypt. A mail address is\nprovided and the file also creates a secret that stores the private key. Lastly\na challenge solver is defined. This defines how Lets Encrypt is going to verify\nthat a domain really belongs to you. In this case this is done through an HTTP\nchallenge. This means that LetsEncrypt expects a file with a certain name and\ncontent to be available on a certain URL in your domain. This proves you are the\nowner of the domain because only if you are the owner of the domain you can\nplace this content there.\n\nThe staging issuer is very similar. The only difference is the server URL and the\nname of the issuer. The server URL points to the staging environment of Lets\nEncrypt. This is a test environment where you can test your setup without\nactually issuing a certificate.\n\n.staging_issuer.yaml\n[source, yaml]\n----\napiVersion: cert-manager.io/v1\nkind: ClusterIssuer\nmetadata:\n name: letsencrypt-staging\n namespace: cert-manager\nspec:\n acme:\n   # The ACME server URL\n   server: https://acme-staging-v02.api.letsencrypt.org/directory\n   # Email address used for ACME registration\n   email: john@doe.com\n   # Name of a secret used to store the ACME account private key\n   privateKeySecretRef:\n     name: letsencrypt-staging\n   # Enable the HTTP-01 challenge provider\n   solvers:\n   - http01:\n       ingress:\n         class:  nginx\n----\n\nIn the \u003c\u003c_ingress,ingress definition\u003e\u003e we saw a _tls_section_. This becomes\nimportant right now. This section tells kubernetes which domain name and which\nSANs (Subject Alternative Names) the certificate should be issued for. The\nsection also gives a name for the secret where the certificate is stored. So\nfrom the ingress you request and specify the certificate. There is no need to\ndefine and request the certificate in a separate file.\n\nAfter the _cert-manager_ and _ingress_ have been deployed LetsEncrypt\nintitiates a challenge using a http request. First internally to check, then\nexternally. The challenge file is placed in the .well-known/acme-challenge\ndirectory. This is done by the cert-manager pod. Make sure that the url is\nresolvable both internally and externally on http (port 80). I ran into a\nproblem with the internal check failing. This was because my Cisco ASA firewall\ndid not hairpin traffic from inside destined for the external IP of my NGINX\nimplementation (grijsbach.eu resolved to the external IP address from the inside\nof my network). So while externally the check worked, I checked that using curl\nfrom an external server, internally the check failed. I solved this by\nconfiguring split DNS where the internal DNS server resolves the domain url to\nan internal IP. This way the internal check also worked.\n\nAfter the challenge has been completed the certificate is issued and stored in\nthe secret. As stated in the ingress definition the certificate is then used to\nsecure the connection to the NGINX server by connection it to a domain name or a\nSAN. This is all done in the _tls_section_ of the ingress definition.\nA number of commands are useful for troubleshooting the issueing of\ncertificates:\n\n----\n#from the namespace where the implementation runs\nk describe cert \u003ccertname\u003e\nk describe order \u003cordername\u003e\nk describe challenge \u003cchallenge name\u003e\nk describe certificaterequest\n\n#from the cert-manager namespace\nk logs \u003ccert-manager-pod\u003e\n----\n\nk get certificates should give you a list of certificates that have been issued,\nall showing READY being true. If this is not the case you can use the above\ncommands to troubleshoot.\n\n----\n❯ k get certificates\nNAME                   READY   SECRET                 AGE\nexample1-secret        True    example1-secret       143m\nexample2-secret        True    example2-secret       133m\n----\n\nSo if everything is correct you now have: \n\n- Certmanager installed in its own namespace;\n- A production clusterIssuer yaml file;\n- Ingress with the right annotations\n- DNS pointing to the right IP adresses so the cluster reach the Webservice\n  internally and externally.\n\nIf you deploy this without the NGINX implementation present the certificates\nwill not be issued of course. After the NGINX deployment they will be because\ncert-manager will keep monitoring the ingress resources.\n\n=== Setting up PHP in kubernetes\n\nAs stated before I created a custom image for PHP. The PHP service only has to\nbe reachable internally. So the service kan be of the type ClusterIP. Again\nthere are two files, a deployment file and a service file. Check the output\nbelow and the explanation that follows.\n\n.phpfpm-deployment.yaml\n[source, yaml]\n----\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: phpfpm\n  labels:\n    app: phpfpm\n    layer: backend\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: phpfpm\n  template:\n    metadata:\n      labels:\n        app: phpfpm\n    spec:\n      containers:\n        - name: phpfpm\n          image: \u003cdocker uid\u003e/phpfpm-mysqli:latest\n          ports:\n            - containerPort: 9000\n          volumeMounts:\n            - mountPath: /var/www\n              name: longhorn-pvc\n      volumes:\n        - name: longhorn-pvc\n          persistentVolumeClaim:\n            claimName: nginx-pvc\n----\n\nBy now you should notice that the deployment defines labels. These labels are:\n\n- app: phpfpm\n- layer: backend\n\nThe pod template selects the deployment that has the label app: phpfpm. The\ncontainer used is the custom container the has been created and described\n\u003c\u003c_php,earlier\u003e\u003e. It listens on port TCP/9000. Next one volume is mounted into\nthe container. This is the /var/www directory that is on the longhorn persistent\nvolume. This is the same volume that is also used by the NGINX container pods.\n\n.phpfpm-clusterip.yaml\n[source, yaml]\n----\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: phpfpm\n  labels:\n    app: phpfpm\n    layer: backend\n\nspec:\n  type: ClusterIP\n  selector:\n    app: phpfpm\n\n  ports:\n    - port: 9000\n      targetPort: 9000\n----\n\nThe service registers pods that have the label app: phpfpm. The service is of\nthe type ClusterIP. The service listens on port 9000 and forwards the traffic to\nthe pods on port 9000. The service is only reachable from within the cluster.\n\nLet's check the endpoints of the service. There should be one, the pod that was\ncreated with the deployment file.\n\n[source, bash]\n----\n❯ k get endpoints phpfpm\nNAME     ENDPOINTS          AGE\nphpfpm   10.42.3.141:9000   33d\n----\n\n=== Reading from a private docker repository\n\nLogin to dockerhub (docker.io)\ndocker login docker.io -u \u003cdocker uid\u003e\\n\n\nThis creates a config.json in ~/.docker with the credentials. This file can be\nused to create a secret in kubernetes.\n\n----\nkubectl create secret generic regcred --from-file=.dockerconfigjson=/home/john/.docker/config.json --type=kubernetes.io/dockerconfigjson\n----\n\nThis secret can be used in the deployment file for the pod that needs to pull\nthe image from the private repository. In the output below this is done by\nreferring to _regcred_ in the imagePullSecrets section of the container spec.\n\n[source, yaml]\n----\n    spec:\n      #securityContext:\n      #  runAsUser: 33  # This is typically the user ID for www-data\n      #  fsGroup: 33    # This ensures the container has the right file system group\n      containers:\n      - name: nginx\n        image: rinmeister/nginx-thuis-php:latest\n        env:\n        - name: WORDPRESS_DB_HOST\n          value: wordpress-mysql\n        - name: WORDPRESS_DB_PASSWORD\n          valueFrom:\n            secretKeyRef:\n              name: mysql-pass-gd6fh98b8f\n              key: password\n        - name: WORDPRESS_DB_USER\n          value: wordpress\n        ports:\n        - containerPort: 80\n        volumeMounts:\n        - mountPath: /etc/nginx/nginx.conf # mount nginx-conf volumn to /etc/nginx\n          readOnly: true\n          name: nginx-conf\n          subPath: nginx.conf\n        - mountPath: /var/log/nginx\n          name: log\n        - mountPath: /var/www\n          name: longhorn-pvc\n      imagePullSecrets:\n      - name: regcred\n----\n\n\n== Troubleshooting\n\nVery often you will have to troubleshoot. \n\nI often trace from within the pod. For example I wanted to check the internal\nweb traffic. I wanted to see that ingress terminates traffic on 443 but that the\nforwarded traffic from ingress to the pod is on port 80 and therefore\nunencrypted. The easiest way to do this is to exect into the pod, install\ntcpdump and trace the traffic:\n\n----\nk exec -it \u003cpodname\u003e -- /bin/bash\napt update\napt install tcpdump\ntcpdump -i eth0 port 80\n----\n\nCheck it out and see for yourself. You can also listen on 443 and you will see no\ntraffic. Everything is forwarded on port 80. So be sure to protect your nodes\nwell. Anyone with access to the cluster can sniff in and read the traffic.\n\n== Uploading data\n\nTo upload data to the cluster you can use the kubectl cp command. This command\ncopies files to and from containers. As all data is on the persistent volume and\nthe mount is /var/www this involves creating the directories and copying all the\ndata. +\nKubectl has a _cp_ command that can copy local files to a pod. The command\ncopies files and directories. So to copy the directory  and its contents to the\npod, create the direcory and copy all the content use:\n\nk cp ./\u003cdatadir\u003e \u003cpodname\u003e:/var/www/. \n\nAfter that the fileowner and group should be set to www-data. This can be done\nwith the chown command. You can either _exec_ into the container or issue\ncommands from the client terminal. Most of the times I just _exec_ into the pod.\n\nIn the NGINX container the user www-data has been created as the web server user\nthat owns all the data files. When copying data to the container, make sure that\nthe owner and group is set to this user. Furthermore, make sure that all file\npermissions have been set to 644 and all directory permissions to 755. This is\nis the most secure way to set permissions. Never set any permission to 777.\n\nI had a big problem with file permissions causing all kinds of trouble with\nwordpress. I could not load the admin page and the site css and php did not load\nproperly. The problem was the file permissions. I had set all owner and group\nconfiguration to www-data and the problem still persisted. I troubleshooted for\na long time and eventually found out that the problem did not come from my NGINX\npod, but from the PHP pod. All actions are php scripts that are executed by the\nPHP pod on the same volume. The PHP pod runs on Alpine and in Alpine the\nwww-data user is using a userID of 82. This is different from the www-data user\nin ubuntu. So I ended up changing the owner and group to 82 and the problem was\nsolved.\n\nThe commands to set the correct file permissions from the current directory:\n\n[source, bash]\n----\nfind ./ -type d | xargs chmod 755\nfind ./ -type f | xargs chmod 644\n----\n\nThe command to set the owner and group to 82 to every subdirectory of /var/www:\n\n[source, bash]\n----\nchown -R 82:82 /var/www\n----\n\n\n== Configure sites-available\nThe most important thing to change in the domain configurations in the\nsites-available directory is the PHP connection. The PHP service is reachable\nunder its DNS name in kubernetes. This is the service name. In this\nimplementation it is _phpfpm:9000_. The bit after the colon is the TCP port the\nservice is listening on. You refer to the PHP service with the _fastcgi_pass_\ndirective.\n\nBelow the output for example1.com\n\n.example1 (in ./sites-available)\n----\nserver {\n\n        root /var/www/example1;\n\n        # Add index.php to the list if you are using PHP\n        index index.php index.html index.htm index.nginx-debian.html;\n\n        server_name example1.com www.example1.com;\n\n        location / {\n                # First attempt to serve request as file, then\n                # as directory, then fall back to displaying a 404.\n                try_files $uri $uri/ =404;\n        }\n\n    location = /favicon.ico {\n            log_not_found off;\n                    access_log off;\n    }\n\n    location ~* \\.(?:jpg|js|css|gif|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {\n        expires max;\n        access_log off;\n    }\n\n    location = /robots.txt {\n        allow all;\n        log_not_found off;\n        access_log off;\n    }\n\n    location ~ \\.php$ {\n        #include snippets/fastcgi-php.conf;\n        fastcgi_param REQUEST_METHOD $request_method;\n        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n        fastcgi_pass phpfpm:9000;\n        include fastcgi_params;\n    }\n}\n----\n\n== Configure Wordpress\n\nAfter all containers are running and you have made sure that the certificates\nall work it is time to configure Wordpress. Basically you just follow the\nofficial guide provided by Wordpress. You can also follow the wizard that is run\nwhen you first access the site. Because of my problems with the permissions (see\nprevious section) I just edited the wp-config.php file. First I created another\nclient user in MySQL and granted it permissions to the client database. I tried\nto set this up during installation of the container but that did not work. So in\nthe end I exec-ed into the MySQL database Pod and created a user by hand. MySQL\nalso uses Persistent Volumes so the user will be there even after a restart or\nrecreation of the Pod.\n\n.add user to MySQL\n[source, SQL]\n----\nCREATE USER 'client'@'localhost' IDENTIFIED BY 'password';\nGRANT ALL PRIVILEGES ON \u003cdatabase\u003e.* TO 'client'@'localhost';\n----\n\nIf you need to import a database into MySQL first copy the sql file into the\ncontainer. Then you can use the following SQL command:\n\n.import database\n[source, SQL]\n----\nmysql -u root -p wp_users \u003c wp_users.sql\n----\n\nAfter that edit the wp-config.php file. I spent a lot of time getting this\nright. In the end the file is as below.\n\n.wp-config.php\n[source, php]\n----\n\u003c?php\n/**\n * The base configuration for WordPress\n *\n * The wp-config.php creation script uses this file during the installation.\n * You don't have to use the website, you can copy this file to \"wp-config.php\"\n * and fill in the values.\n *\n * This file contains the following configurations:\n *\n * * Database settings\n * * Secret keys\n * * Database table prefix\n * * ABSPATH\n *\n * @link https://developer.wordpress.org/advanced-administration/wordpress/wp-config/\n *\n * @package WordPress\n */\ndefine('FORCE_SSL_ADMIN', true);\n\nif( strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false )\n   $_SERVER['HTTPS'] = 'on';\nelse\n   $_SERVER['HTTPS'] = 'off';\n\n// ** Database settings - You can get this info from your web host ** //\n/** The name of the database for WordPress */\ndefine( 'DB_NAME', 'db-name' );\n\n/** Database username */\ndefine( 'DB_USER', 'db-username' );\n\n/** Database password */\ndefine( 'DB_PASSWORD', 'Password' );\n\n/** Database hostname */\ndefine( 'DB_HOST', 'wordpress-mysql' );\n\n/** Database charset to use in creating database tables. */\ndefine( 'DB_CHARSET', 'utf8mb4' );\n\n/** The database collate type. Don't change this if in doubt. */\ndefine( 'DB_COLLATE', '' );\n\n/**#@+\n * Authentication unique keys and salts.\n *\n * Change these to different unique phrases! You can generate these using\n * the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}.\n *\n * You can change these at any point in time to invalidate all existing cookies.\n * This will force all users to have to log in again.\n *\n * @since 2.6.0\n */\ndefine( 'AUTH_KEY',         'hJCUQp\u0026.[2 3.*$oCXHyU$9{/iK!6#~qHfXVnXa*[l1+iOtgZtf%AX*/PU%m`?(t' );\ndefine( 'SECURE_AUTH_KEY',  'glI.y0CM2lFJY3y*A@vl*(6Iqj}Tz!]2fa\u003evDY(Hx3JMd#y@SJ^VL!-;3nhU(OXR' );\ndefine( 'LOGGED_IN_KEY',    'LWg|}=j{;RpSBGc-6,U96CG(=1CYL`@9\u003c(~5_x~B1\u003e{XAgP6@(TSJ`W1X;Vf4[A4' );\ndefine( 'NONCE_KEY',        '`QMw S@\u003e7s,H)1z/Az|d4O[)LjSw+CDexb-HrA#}NMfb}M F,Qa*C0s.!,q!:p%0' );\ndefine( 'AUTH_SALT',        '\u0026HjX;Z`Skw {*QqG`r\u003en5YsW\u0026i\u003eAGd.WbZTdVm](0mE{ZXZ7uf^\u0026Jz5uuO;i~S}t' );\ndefine( 'SECURE_AUTH_SALT', ':L~l!v_C\u003ePGkVWScG% ;B*$$8*4XGA={uswR$|0JK8V~/R+rfm#S ,2HjBO%*gP6' );\ndefine( 'LOGGED_IN_SALT',   's*Okpl:\u0026g!0!ojv{$]rcC,6\u003ef]\u003eOdK~k(!i c\u0026~(2$=?e@FnDuK:*)~M9I)912PU' );\ndefine( 'NONCE_SALT',       'NPx)N,P@+}#\u003cDMH]Tb/axhRK/zo~SGz }GG3HD0$c*F`nrd;FgRT!jpzs2D^-Swa' );\n\n/**#@-*/\n\n/**\n * WordPress database table prefix.\n *\n * You can have multiple installations in one database if you give each\n * a unique prefix. Only numbers, letters, and underscores please!\n */\n$table_prefix = 'wp_';\n\n/**\n * For developers: WordPress debugging mode.\n *\n * Change this to true to enable the display of notices during development.\n * It is strongly recommended that plugin and theme developers use WP_DEBUG\n * in their development environments.\n *\n * For information on other constants that can be used for debugging,\n * visit the documentation.\n *\n * @link https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/\n */\ndefine( 'WP_DEBUG', false );\n\n/* Add any custom values between this line and the \"stop editing\" line. */\n\n\n\n/* That's all, stop editing! Happy publishing. */\n\n/** Absolute path to the WordPress directory. */\nif ( ! defined( 'ABSPATH' ) ) {\n        define( 'ABSPATH', __DIR__ . '/' );\n}\n\n/** Sets up WordPress vars and included files. */\nrequire_once ABSPATH . 'wp-settings.php';\ndefine('WP_HOME','https://www.example1.com');\ndefine('WP_SITEURL','https://www.example1.com');\ndefine('FS_METHOD', 'direct');\n----\n\nThere is still some work to do here. As you can see the passwords and other\nsensitive information is hardcoded in the file. I tried to use environment\nvariables in the container for this but that did not work. If you get this\nworking please let me know.\n\n== Conclusion\n\nThis has been a lot of work. My cluster and NGINX is running stable right now. I\ndo still have some issues that I need to work on. One is the hardcoded passwords\nin the wp-config.php file. I also need to get the backups working. Another\nannoying thing is that one of the Wordpress websites loads really slow initially.\nIt is waiting for something, could be a database connection. It takes 15 seconds\nfor it to load until the opening page. I will have to look into that. But if any\nreader has suggestions please let me know. I hope this guide is helpful for\nother people that want to run Wordpress in a Kubernetes cluster. There are many\nguides on the internet that sort of help you out, but none of them was working\ncompletely for me. So I ended up doing al lot of trial and error and\ntroubleshooting. That is why I wrote this guide. I hope it helps you out. If you\nhave any questions or suggestions please let me know. I am happy to try and help\nyou.\n\nPlease mail me at rene@grijsbach.eu\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frinmeister%2Fk8s-nginx-wordpress","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frinmeister%2Fk8s-nginx-wordpress","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frinmeister%2Fk8s-nginx-wordpress/lists"}