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
kubectlinstalled locally and access to the cluster - A default
StorageClass(the manifest creates 3PersistentVolumeClaims); check withkubectl get storageclass - Nodes can pull images from Docker Hub, and pods can reach
gitee.comandcdn.spug.cc(the code and front-end bundle are fetched on first start) - The namespace may run privileged containers (
privileged: true, needed by thesshfsmounts of file distribution and pipelines); the Pod Security level must not berestricted - 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
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:
| Resource | Name | Description |
|---|---|---|
| Secret | spug-db | Database name, user and passwords, shared by the spug-db and spug Deployments |
| PersistentVolumeClaim | spug-db-data | MariaDB data directory (10Gi) |
| PersistentVolumeClaim | spug-service | Spug program directory, /data/spug inside the container (10Gi) |
| PersistentVolumeClaim | spug-repos | Git checkouts and build artifacts of standard deployments, /data/repos inside the container (20Gi) |
| Deployment / Service | spug-db | MariaDB 10.8, reachable inside the cluster as spug-db:3306 |
| Deployment | spug | The Spug service, single replica, privileged container |
| Service | spug | NodePort service, exposed on port 30080 of every node by default, for access by IP |
| Ingress | spug | Access 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:
| Setting | Description |
|---|---|
Secret spug-db | Database 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 |
storageClassName | The 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_VERSION | Code 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: true | File 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: 30080 | The 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 spug | Access 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 |
resources | The manifest only sets requests; add limits to match your cluster, at least 1 CPU and 2 GB of memory are recommended |
replicas: 1 | Redis 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 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
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.
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
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 theNodePortto the internet; if public access is unavoidable, enable HTTPS on the domain and turn on login MFA underSystem / Settings / Security Settings. - The
spugpod reaches managed hosts over SSH, so allow port 22 (or the custom SSH port of the hosts) from the cluster in your network policies.