Skip to main content
Version: 4.x

Deploy on Kubernetes

If you already run a Kubernetes cluster you can deploy Spug there with the official Docker image openspug/spug-service. The Kubernetes deployment uses the same image as Install with Docker: Nginx, Redis and every background service are bundled in the image, and the database runs in a separate MariaDB. A complete manifest is provided so a single kubectl apply brings everything up. It creates both a NodePort (access by IP) and an Ingress (access by domain), so once the installation is done you only need to bind your domain to the Ingress to log in through it.

Requirements

  • Kubernetes 1.19 or later, with kubectl installed locally and access to the cluster
  • A default StorageClass (the manifest creates 3 PersistentVolumeClaims); check with kubectl get storageclass
  • Nodes can pull images from Docker Hub, and pods can reach gitee.com and cdn.spug.cc (the code and front-end bundle are fetched on first start)
  • The namespace may run privileged containers (privileged: true, needed by the sshfs mounts of file distribution and pipelines); the Pod Security level must not be restricted
  • For access by domain, an Ingress controller is installed in the cluster (the manifest is written for ingress-nginx, other controllers are covered below); without one you can still access Spug by IP through the NodePort
Supported CPU architectures

The openspug/spug-service:4.0.0 image is published for both amd64 and arm64, so ARM64 nodes work out of the box; 32-bit ARM (armv7) is not supported. The Alibaba Cloud registry has no 4.0 image, so do not replace the image in the manifest; if pulling from Docker Hub is difficult, configure a registry mirror in the container runtime of the nodes.

Architecture

The manifest creates the following resources in the spug namespace:

ResourceNameDescription
Secretspug-dbDatabase name, user and passwords, shared by the spug-db and spug Deployments
PersistentVolumeClaimspug-db-dataMariaDB data directory (10Gi)
PersistentVolumeClaimspug-serviceSpug program directory, /data/spug inside the container (10Gi)
PersistentVolumeClaimspug-reposGit checkouts and build artifacts of standard deployments, /data/repos inside the container (20Gi)
Deployment / Servicespug-dbMariaDB 10.8, reachable inside the cluster as spug-db:3306
DeploymentspugThe Spug service, single replica, privileged container
ServicespugNodePort service, exposed on port 30080 of every node by default, for access by IP
IngressspugAccess by domain; ships with the placeholder host spug.example.com, which you replace with your domain after installing

Quick start

The manifest is hosted on this website, so four commands deploy, initialize and bind a domain:

# 1. create every resource
kubectl apply -f https://ops.spug.cc/k8s/spug.yaml

# 2. wait until the pod is ready (the first start fetches the code and the front-end bundle, usually 1 to 3 minutes)
kubectl -n spug rollout status deploy/spug

# 3. create the database tables and an administrator (user name admin, password spug.cc; replace them with your own values)
kubectl -n spug exec deploy/spug -- init_spug admin spug.cc

# 4. bind your domain (replace spug.yourdomain.com, and point the domain's DNS at the Ingress controller)
kubectl -n spug patch ingress spug --type=json -p='[{"op":"replace","path":"/spec/rules/0/host","value":"spug.yourdomain.com"}]'

Then open http://spug.yourdomain.com in the browser and log in; without a domain, http://<any-node-ip>:30080 works too. For production, change the database passwords and storage settings described below before deploying.

The manifest

This is the full content of https://ops.spug.cc/k8s/spug.yaml; save it locally, adjust it as needed and run kubectl apply -f spug.yaml:

apiVersion: v1
kind: Namespace
metadata:
name: spug
---
# database credentials (change the default passwords)
apiVersion: v1
kind: Secret
metadata:
name: spug-db
namespace: spug
type: Opaque
stringData:
MYSQL_DATABASE: spug
MYSQL_USER: spug
MYSQL_PASSWORD: spug.cc
MYSQL_ROOT_PASSWORD: spug.cc
---
# database files
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: spug-db-data
namespace: spug
spec:
accessModes:
- ReadWriteOnce
# storageClassName: <your-storageclass>
resources:
requests:
storage: 10Gi
---
# Spug program directory (/data/spug in the container)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: spug-service
namespace: spug
spec:
accessModes:
- ReadWriteOnce
# storageClassName: <your-storageclass>
resources:
requests:
storage: 10Gi
---
# git checkouts and build artifacts (/data/repos in the container)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: spug-repos
namespace: spug
spec:
accessModes:
- ReadWriteOnce
# storageClassName: <your-storageclass>
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: spug-db
namespace: spug
labels:
app: spug-db
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: spug-db
template:
metadata:
labels:
app: spug-db
spec:
containers:
- name: mariadb
image: mariadb:10.8
args:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
envFrom:
- secretRef:
name: spug-db
ports:
- name: mysql
containerPort: 3306
readinessProbe:
tcpSocket:
port: mysql
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
tcpSocket:
port: mysql
initialDelaySeconds: 60
periodSeconds: 20
resources:
requests:
cpu: 100m
memory: 256Mi
volumeMounts:
- name: data
mountPath: /var/lib/mysql
volumes:
- name: data
persistentVolumeClaim:
claimName: spug-db-data
---
apiVersion: v1
kind: Service
metadata:
name: spug-db
namespace: spug
spec:
selector:
app: spug-db
ports:
- name: mysql
port: 3306
targetPort: mysql
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: spug
namespace: spug
labels:
app: spug
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: spug
template:
metadata:
labels:
app: spug
spec:
containers:
- name: spug
image: openspug/spug-service:4.0.0
securityContext:
privileged: true
env:
# code version fetched on first start, must match the image
- name: SPUG_DOCKER_VERSION
value: v4.0.0
- name: MYSQL_HOST
value: spug-db
- name: MYSQL_PORT
value: "3306"
envFrom:
- secretRef:
name: spug-db
ports:
- name: http
containerPort: 80
# the first start clones the code and downloads the bundle, allow 10 minutes
startupProbe:
httpGet:
path: /
port: http
periodSeconds: 10
failureThreshold: 60
readinessProbe:
httpGet:
path: /
port: http
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: http
periodSeconds: 30
resources:
requests:
cpu: 500m
memory: 1Gi
volumeMounts:
- name: service
mountPath: /data/spug
- name: repos
mountPath: /data/repos
volumes:
- name: service
persistentVolumeClaim:
claimName: spug-service
- name: repos
persistentVolumeClaim:
claimName: spug-repos
---
# access by IP: NodePort 30080; switch to ClusterIP if you only use the domain
apiVersion: v1
kind: Service
metadata:
name: spug
namespace: spug
spec:
type: NodePort
selector:
app: spug
ports:
- name: http
port: 80
targetPort: http
nodePort: 30080
---
# access by domain (ingress-nginx): after installing, replace spug.example.com with your domain and point its DNS at the Ingress controller
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: spug
namespace: spug
annotations:
# allow large uploads
nginx.ingress.kubernetes.io/proxy-body-size: "0"
# web terminal / deploy console keep WebSocket connections open, avoid the 60s idle cutoff
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
# change for other controllers (e.g. traefik)
ingressClassName: nginx
rules:
- host: spug.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: spug
port:
name: http
# HTTPS: create a TLS Secret, then uncomment
# tls:
# - hosts:
# - spug.example.com
# secretName: spug-tls

Settings worth reviewing:

SettingDescription
Secret spug-dbDatabase credentials; change MYSQL_PASSWORD and MYSQL_ROOT_PASSWORD for production. You can also delete the spug-db Deployment / Service and point MYSQL_HOST / MYSQL_PORT at an external MySQL, see external MySQL
storageClassNameThe three PVCs use the cluster's default StorageClass; uncomment and fill it in if there is none. Losing the data in spug-service or spug-repos means re-initializing, so always use persistent storage
SPUG_DOCKER_VERSIONCode version (git tag) fetched on first start; it must match the image version, use v4.0.0 with the 4.0.0 image. There is no default: without it the container exits while cloning and keeps restarting
privileged: trueFile distribution and pipeline data transfer mount remote directories with sshfs, which needs a privileged container; remove it if cluster policy forbids privileged pods, at the cost of those features
nodePort: 30080The port for access by IP, any port in 30000-32767 works; if you only use the domain you can change type to ClusterIP and drop nodePort
Ingress spugAccess by domain; host is a placeholder you replace with your domain after installing (see Bind a domain). ingressClassName defaults to nginx, and the annotations lift the upload size limit and extend the WebSocket timeout. Without an Ingress controller the resource simply has no effect
resourcesThe manifest only sets requests; add limits to match your cluster, at least 1 CPU and 2 GB of memory are recommended
replicas: 1Redis is built into the image and the scheduler / monitor services are singletons, Spug does not support multiple replicas, do not scale it up

Step by step

1. Create the resources

kubectl apply -f https://ops.spug.cc/k8s/spug.yaml

Use kubectl apply -f spug.yaml instead if you edited the manifest. Then check the status of the resources:

kubectl -n spug get pod,pvc,svc,ingress

2. Wait for the first start

On first start spug-db initializes the database, and spug clones the version selected by SPUG_DOCKER_VERSION from gitee.com, downloads the front-end bundle from cdn.spug.cc and generates spug_api/spug/overrides.py before starting the services. This usually takes 1 to 3 minutes; the startupProbe in the manifest allows up to 10 minutes.

# wait until the spug Deployment is ready
kubectl -n spug rollout status deploy/spug

# follow the start-up log
kubectl -n spug logs -f deploy/spug
The first start needs internet access

The 4.0.0 image does not contain the code, so the pod needs to reach gitee.com and cdn.spug.cc on its first start; the first start cannot complete on an isolated / offline cluster. If the pod stays in CrashLoopBackOff, check the log for a failed clone or a missing SPUG_DOCKER_VERSION.

3. Initialize

Once the pod is ready, run the command below to create the database tables and an administrator account with user name admin and password spug.cc; replace them with your own values.

kubectl -n spug exec deploy/spug -- init_spug admin spug.cc
Tip

If it fails with can't open file '/data/spug/spug_api/manage.py' (the code is still being fetched) or Can't connect to MySQL server on 'spug-db' (the database is still initializing), wait a few seconds and run it again.

4. Test access by IP

Open http://<any-node-ip>:30080 in the browser and log in with the account created in step 3. The globe icon in the header switches between English and Chinese. Open port 30080 in the security group / firewall of the nodes if needed.

"Failed to get the real IP of the visitor" after logging in

Through a NodePort the pod sees the translated node address instead of the client IP, so Spug warns that it cannot get the real IP. On an internal network you can ignore it or disable IP verification under System / Settings / Security Settings. Once you bind a domain in the next step and access Spug through the Ingress, X-Forwarded-For carries the real client IP and the warning goes away.

5. Bind a domain

The manifest already created an Ingress named spug with the placeholder host spug.example.com; replace it with your domain to access Spug through it:

# replace spug.yourdomain.com with your domain
kubectl -n spug patch ingress spug --type=json -p='[{"op":"replace","path":"/spec/rules/0/host","value":"spug.yourdomain.com"}]'

You can also edit host in your local copy of spug.yaml and run kubectl apply -f spug.yaml again. Then point the domain's DNS at the external address of the Ingress controller. With ingress-nginx, the address to use is the EXTERNAL-IP (cloud load balancer) or a node IP (NodePort / hostNetwork setups):

kubectl -n ingress-nginx get svc ingress-nginx-controller

Once DNS resolves, open http://spug.yourdomain.com in the browser and log in. If access by IP is no longer needed after switching to the domain, change the spug Service to ClusterIP:

kubectl -n spug patch svc spug -p '{"spec":{"type":"ClusterIP"}}'

Enabling HTTPS: create a TLS Secret from your certificate and add a tls section to the Ingress (cert-manager can also issue the certificate automatically):

kubectl -n spug create secret tls spug-tls --cert=fullchain.pem --key=privkey.pem
kubectl -n spug patch ingress spug --type=json -p='[{"op":"add","path":"/spec/tls","value":[{"hosts":["spug.yourdomain.com"],"secretName":"spug-tls"}]}]'

Other Ingress controllers: the manifest sets ingressClassName: nginx; list the controllers in your cluster with kubectl get ingressclass, and with Traefik, a cloud ALB or similar, change it to the matching name. Make sure the controller forwards WebSocket, passes the X-Forwarded-For header, lifts the upload size limit and keeps idle WebSocket connections open for at least an hour (the nginx.ingress.kubernetes.io/* annotations in the manifest only apply to ingress-nginx), see WebSocket:

kubectl -n spug patch ingress spug -p '{"spec":{"ingressClassName":"traefik"}}'

Day-to-day commands

# pod status
kubectl -n spug get pod

# Spug log (per-service logs are in /data/spug/spug_api/logs inside the container)
kubectl -n spug logs -f deploy/spug

# open a shell in the container
kubectl -n spug exec -it deploy/spug -- bash

# restart Spug (data lives in the PVCs and survives restarts)
kubectl -n spug rollout restart deploy/spug

# show the bound domain
kubectl -n spug get ingress spug

# back up the database
kubectl -n spug exec deploy/spug-db -- mysqldump -uroot -pspug.cc spug > spug-backup.sql

The directory layout inside the container is the same as with Docker, see Install with Docker.

Upgrading

For minor updates between 4.x versions run the update command inside the container and restart the Deployment; the code lives in the spug-service PVC, so the update survives pod recreation:

kubectl -n spug exec deploy/spug -- python3 /data/spug/spug_api/manage.py update
kubectl -n spug rollout restart deploy/spug

When a new image version is released, update both the image tag and SPUG_DOCKER_VERSION in the manifest, run kubectl apply again, and switch the code inside the container following the Docker steps in Upgrade, replacing docker exec spug with kubectl -n spug exec deploy/spug --.

Uninstalling

kubectl delete -f https://ops.spug.cc/k8s/spug.yaml
Note

Deleting the manifest removes the spug namespace together with its PVCs, so the database and code directories are lost (depending on the reclaim policy of the StorageClass). Back up the database before uninstalling.

Security recommendations

  • Change the default database passwords in Secret spug-db. Do not expose the NodePort to the internet; if public access is unavoidable, enable HTTPS on the domain and turn on login MFA under System / Settings / Security Settings.
  • The spug pod reaches managed hosts over SSH, so allow port 22 (or the custom SSH port of the hosts) from the cluster in your network policies.