K3s Cluster in Docker-Compose Running PHP Nginx
Run a K3s cluster in docker-compose with PHP + Nginx on 1 Gb, 1 vCPU server.
If anything goes wrong: read documents; check if the versions match (things are changing every day)
At the end of this page includes a list of links used as reference when writing.
Table of Content
- Table of Content
- Environment
- Prerequisites
- Set Up Directory
- Launch the Cluster using Docker-Compose
- Talk to the Cluster
- Create Cluster Configs
- Config and Run the Cluster
- Result
- Stop and Clean up
- Conclusion and Thoughts
- Future Works
- Reference
Environment
- 1 GB Memory
- 1 vCPU
- 25 GB SSD
- Debian 10.2 on Digital Ocean Droplet
Prerequisites
- docker
- docker-compose
- a domain if wish to use https (and certificates)
Try this to set up but it’s more stable to use iptables at this moment (01/04/2020):
Set Up Debian 10 Server on Digital Ocean
Set Up Directory
Feel free to choose any work directory, but for the purpose of simplicity,
a directory owned by a non-root user in the root directory (/) will be used
for this task.
sudo mkdir -v /docker/
# replace with your non-root user name and group
# (default group name is the same as the user name)
sudo chown __NAME__:__GROUP__ /docker/
To make it easier to manage and run scripts, create a bin directory
- here it is under
/docker/ - alternatively, put it under
~/, some distribution will automatically append it to$PATH- (
$PATHis a list of directories that the shell will use to look for commands/executables)
- (
- editing the
$PATHvariable is optional- it makes calling the script the same as calling normal commands
mkdir -v /docker/bin/
# pre-pend it to PATH, making the search end earlier
echo "PATH=/docker/bin:$PATH" >> ~/.bashrc
source ~/.bashrc
It doesn’t hurt to create several sub-directories for:
- Kubernetes resource yaml files
- volumes to mount
mkdir -v /docker/k3s
mkdir -v /docker/kube
# this directory holds the PHP code, i.e. the index.php
# explained later
mkdir -v /docker/www
Launch the Cluster using Docker-Compose
As easy as one simple docker-compile file from k3s official repo
Modifications:
- rename services
- disable
traefikby--no-deploy traefik - mount directory with kubeconfig to host’s
./k3s(created above) - mount php-code directory to container’s
/var/www - open 80 and 443 (http/https) port for agents
# to run define K3S_TOKEN, K3S_VERSION is optional, eg:
# K3S_TOKEN=${RANDOM}${RANDOM}${RANDOM} docker-compose up
version: '3'
services:
k3s-server:
image: "rancher/k3s:${K3S_VERSION:-latest}"
command: server --no-deploy traefik
tmpfs:
- /run
- /var/run
privileged: true
environment:
- K3S_TOKEN=${K3S_TOKEN:?err}
- K3S_KUBECONFIG_OUTPUT=/output/kubeconfig.yaml
- K3S_KUBECONFIG_MODE=666
volumes:
- k3s-server:/var/lib/rancher/k3s
# This is just so that we get the kubeconfig file out
- ./k3s:/output
- /docker/www:/var/www
ports:
- 6443:6443
k3s-agent:
image: "rancher/k3s:${K3S_VERSION:-latest}"
tmpfs:
- /run
- /var/run
privileged: true
environment:
- K3S_URL=https://k3s-server:6443
- K3S_TOKEN=${K3S_TOKEN:?err}
volumes:
- /docker/www:/var/www
ports:
- 80:80
- 443:443
volumes:
k3s-server: {}
Let’s create a script to run it:
touch /docker/bin/k3s-up
chmod u+x /docker/bin/k3s-up
Inside the up script:
#!/bin/bash
# feel free to change this
export K3S_TOKEN=${RANDOM}${RANDOM}${RANDOM}${RANDOM}${RANDOM}
echo "K3S_TOKEN: ${K3S_TOKEN}"
# might want to store token somewhere, e.g.
#echo ${K3S_TOKEN} > ~/.token/K3S_TOKEN
docker-compose -f /docker/docker-compose.yml up -d --scale k3s-agent="${1:-1}"
Now we just need to run this script
- since the directory is in
$PATH, we can call it directly - it creates 1 agent by default, can change it by providing command line argument
k3s-up # spawn 1 agents
#k3s-up 3 # spawn 3 agents
Due to extreme memory constrain, let’s begin with 1 agent
Talk to the Cluster
Now the k3s cluster is up and running.
The system might go through a short thrashing period but it will settle down soon (tested on real machine).
It’s time to kubectl. Let’s use the $KUBECONFIG variable to simplify things:
export KUBECONFIG=/docker/k3s/kubeconfig.yaml
Might as well add this line to bashrc (optional):
- so that current user’s shell will automatically set KUBECONFIG variable
echo "export KUBECONFIG=/docker/k3s/kubeconfig.yaml" >> ~/.bashrc
Test connection
kubectl get nodes
kubectl get pods --all-namespaces
If not working, consult docker and other logs
docker ps
docker logs
...
Create Cluster Configs
Attempts to use nginx ingress have been made in vain because of the limited resources.
We shall use our own nginx, plus load balancer service provided by k3s (good to be simple for simple tasks).
Start with the config maps:
- note: provided nginx configs may not suit every one’s need
mkdir /docker/kube/config-nginx-key # SSL key, if intend to use https
mkdir /docker/kube/config-nginx-sites # nginx config for each site
mkdir /docker/kube/config-nginx-snippets # re-useable snippets
Let’s go through each config file
- main reference for nginx config
SSL Key
(skip if using http)
Files in /docker/kube/config-nginx-key:
# SSL certificates
cert.pem
cert.key
# Diffie-Hellman parameters
# used by Nginx
# https://wiki.openssl.org/index.php/Diffie-Hellman_parameters
dhparam.pem # openssl dhparam -out ./dhparam.pem 4096
For security reasons, my own SSL certificates will not be included here.
- Because I use Cloudflare, my cluster uses Cloudflare Origin CA certificates
- Also it does not require complex verification, renewal steps etc.
- If not suitable, might consider a cert manager (linked article uses ingress). That is beyond of the scope of this article
Nginx Snippets
Files in /docker/kube/config-nginx-snippets:
ssl-some.host.conf
ssl-params.conf
security.conf
ssl-some.host.conf
- just telling nginx which certificates to use
ssl_certificate /etc/nginx/ssl-key/cert.pem;
ssl_certificate_key /etc/nginx/ssl-key/cert.key;
ssl-params.conf
- cipherli.st
- String SSL Security On Nginx
- Security/Server Side TLS - MozillaWiki
- also if you are using Cloudflare like I do, end-users may see different headers etc.
- since clients are talking to Cloudflare, not the host machine directly in general cases
# SSL
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# Diffie-Hellman parameter for DHE ciphersuites
ssl_dhparam /etc/nginx/ssl-key/dhparam.pem;
# Mozilla Intermediate configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=60s;
resolver_timeout 2s;
security.conf
# security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# . files
location ~ /\.(?!well-known) {
deny all;
}
## misc
## not for security but included here for simplicity
# favicon.ico
location = /favicon.ico {
log_not_found off;
access_log off;
}
# robots.txt
location = /robots.txt {
log_not_found off;
access_log off;
}
Nginx Site
/docker/kube/config-nginx-sites/php.conf
Heavily inspired by:
Choose one of the below:
- http
server {
index index.php index.html;
root /var/www;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass kube-php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}
- https
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
include /etc/nginx/snippets/ssl-some.host.conf;
include /etc/nginx/snippets/ssl-params.conf;
root /var/www;
index index.html index.php;
### change to your own host name ###
server_name some.host;
include /etc/nginx/snippets/security.conf;
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
if (!-f $document_root$fastcgi_script_name) {
return 404;
}
fastcgi_param HTTP_PROXY "";
fastcgi_pass kube-php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Back to our kubernetes cluster, now we need to create:
- PHP code
- PHP service
- PHP deployment
- nginx service LoadBalancer
- nginx deployment
mkdir -v /docker/kube/objects
Will use one file for related resources
PHP code
For simplicity, the PHP code is directly put into /docker/www
Sample PHP file, put to /docker/www/index.php:
<?php
echo phpinfo();
PHP Resources
editor /docker/kube/objects/php.yaml
Inside the yaml:
- it exposes port 9000 via ClusterIP (default networking for service) for php-fpm
- php:7-fpm image is used
- the php code directory mounted earlier,
/var/www, is mounted as ahostPathvolume
apiVersion: v1
kind: Service
metadata:
name: kube-php
labels:
tier: backend
spec:
ports:
- protocol: TCP
port: 9000
selector:
app: kube-php
tier: backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kube-php
labels:
tier: backend
spec:
replicas: 1
selector:
matchLabels:
app: kube-php
tier: backend
template:
metadata:
labels:
app: kube-php
tier: backend
spec:
containers:
- name: php
image: php:7-fpm
volumeMounts:
- name: www
mountPath: /var/www
volumes:
- name: www
hostPath:
path: /var/www
type: Directory
Nginx Resources
editor /docker/kube/objects/nginx.yaml
Inside the yaml:
- a LoadBalancer service is created with port 80 and 443
- thanks to k3s’s networking, normally a bare-mental kubernetes cannot use LoadBalancer services
- (“LoadBalancer services … points to external load balancers that are NOT in your cluster”)
- a deployment is created using nginx:1.16 image
- a naive and simple approach to test the config online is included
- the config maps to be created are mounted as directories
- about nginx conf:
- nginx will automatically load conf in
/etc/nginx/conf.d - for our case, our php.conf will include all other config files
- nginx will automatically load conf in
apiVersion: v1
kind: Service
metadata:
name: kube-nginx
labels:
tier: backend
spec:
type: LoadBalancer
ports:
- protocol: TCP
port: 80
name: http
- protocol: TCP
port: 443
name: https
selector:
app: kube-nginx
tier: backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kube-nginx
labels:
tier: backend
spec:
replicas: 1
selector:
matchLabels:
app: kube-nginx
tier: backend
template:
metadata:
labels:
app: kube-nginx
tier: backend
spec:
containers:
- name: nginx
image: nginx:1.16
## uncomment this to test the nginx config
## use something like `kubectl logs kube-nginx-...` to see the output
## use `kubectl delete deployment ...` to remove the test deployment
#command: ["/bin/bash","-c"]
#args: ["echo testing-nginx-conf; nginx -t; sleep 10h"]
ports:
- containerPort: 80
- containerPort: 443
volumeMounts:
- name: nginx-key
mountPath: /etc/nginx/ssl-key
- name: nginx-snippets
mountPath: /etc/nginx/snippets
- name: nginx-sites
mountPath: /etc/nginx/conf.d
- name: www
mountPath: /var/www
volumes:
- name: nginx-key
configMap:
name: config-nginx-key
- name: nginx-snippets
configMap:
name: config-nginx-snippets
- name: nginx-sites
configMap:
name: config-nginx-sites
- name: www
hostPath:
path: /var/www
type: Directory
Config and Run the Cluster
Now we have both the configs and the Kubernetes resources files ready.
Just a short script will do all the setup
touch /docker/bin/k3s-setup
chmod u+x /docker/bin/k3s-setup
Inside the setup script
#!/bin/bash
# ensure the config works
export KUBECONFIG=/docker/k3s/kubeconfig.yaml
set -e
# test connection (optional)
kubectl get node
# nginx config
kubectl create configmap config-nginx-key --from-file=/docker/kube/config_nginx-key
kubectl create configmap config-nginx-snippets --from-file=/docker/kube/config_nginx-snippets
kubectl create configmap config-nginx-sites --from-file=/docker/kube/config_nginx-sites
# apply
kubectl apply -f /docker/kube/objects
Run the script
k3s-setup
Result
The whole server might suffer from a short period of thrashing after starting everything.
Assume all firewall, certificates and so forth all set up, go to http://host-ip or https://some.host (depend on your choice).
The PHP application will show up.
Each access to my host leads to around 40K context switches.
Stop and Clean up
If encounter errors in the previous steps, or need a graceful shutdown, here is a nothing-left-behind clean up script.
touch /docker/bin/k3s-down
chmod u+x /docker/bin/k3s-down
Inside the down script:
- shut down via docker-compose
- remove the used master server volume
# the existence of this variable is required by the docker-compose file
export K3S_TOKEN=NOTHING
# if you want the original token, might consider storing it somewhere, e.g.
#export K3S_TOKEN=$(cat ~/.token/K3S_TOKEN)
docker-compose -f /docker/docker-compose.yml down -v
# optional, delete the no-longer-valid kubeconfig
rm -f /docker/k3s/kubeconfig.yaml
#might remove the saved K3S_TOKEN as well
Conclusion and Thoughts
The purpose of Kubernetes is not, clearly, doing things like this.
But the k3s cluster created is perfect for dev and testing, especially for single-machine. And it’s fun to run a fully-functional Kubernetes on the cutest VPS!
Future Works
- make the cluster persistent
- currently cleaning everything up
- might use
docker-compose stop, but ideally only the master data is needed - potentially related: How to make a full backup of k3s server’s data? · Issue #927 · rancher/k3s
- use secrets
Reference
Some of documents/tutorials are not updated or no longer working.
Tutorials
- How To Deploy a PHP Application with Kubernetes on Ubuntu 18.04 | DigitalOcean
- How to Set Up an Nginx Ingress with Cert-Manager on DigitalOcean Kubernetes | DigitalOcean
- docker-composeでK3sを試してみた - Qiita
- Single node Kubernetes setup – /techblog
- How To Create a Kubernetes Cluster Using Kubeadm on Ubuntu 18.04 | DigitalOcean
K3s
- Installation Options
- Networking
- Advanced Options and Configuration
- Volumes and Storage
- local-path-provisioner/README.md at master · rancher/local-path-provisioner
Kubernetes
- K3s, minikube or microk8s? : kubernetes
- Resources + Controllers Overview · The Kubectl Book
- Debug Pod Replication Controller - Kubernetes
- Creating a single control-plane cluster with kubeadm - Kubernetes
Kubernetes Volumes
- Volumes - Kubernetes hostPath
- Configure a Pod to Use a ConfigMap - Kubernetes
- Persistent Volumes - Kubernetes
- Kubernetes 1.14: Local Persistent Volumes GA - Kubernetes
- Storage Classes - Kubernetes local
- community/resources.md at master · kubernetes/community
Kubernetes Networking
- Ingress vs Load Balancer
- Ingress - Kubernetes
- Bare-metal considerations - NGINX Ingress Controller
- Exposing FCGI services - NGINX Ingress Controller
- symfony - NGINX - PHP-FPM multiple application K8s/Ingress - Stack Overflow
- ingress-nginx/docs/examples/multi-tls at master · kubernetes/ingress-nginx
Nginx config
- Strong SSL Security on nginx - Raymii.org
- h5bp/server-configs-nginx: Nginx HTTP server boilerplate configs
- Remove deprecated ciphers and protocols by aeris · Pull Request #190 · h5bp/server-configs-nginx
- Use modern SSL config for Nginx by swalkinshaw · Pull Request #1127 · roots/trellis
- Security/Server Side TLS - MozillaWiki
- CryptCheck
Comments