Add prod deploy workflow (verso namespace, persistent, friends-only)
Build and Deploy Verso / deploy (push) Successful in 1m20s
Build and Deploy Verso / deploy (push) Successful in 1m20s
New .gitea/workflows/deploy-verso-prod.yml triggered by pushes to the 'prod' branch — a real production target distinct from the ephemeral test rig: - Runs in the 'verso' namespace; Mongo/Redis/app-data on PersistentVolumeClaims, applied idempotently and NEVER deleted (data survives deploys). - Replica set initialised only once; admin created only if no users exist. - Builds/pushes verso:stable (separate tag from test's verso:latest); imagePullPolicy Always so each rollout pulls the new build. - SMTP via an optional 'verso-smtp' Secret (no credentials in the repo); anonymous read-write sharing left off and public registration off (friends-only). - Example Ingress for verso.alocoq.fr at server-ce/k8s/verso-prod-ingress.example.yaml (apply by hand to match the existing TLS/annotation setup). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
name: Build and Deploy Verso (prod)
|
||||
|
||||
# Production deploy. Triggered only by pushes to the `prod` branch — keep `main`
|
||||
# for day-to-day work and fast-forward `prod` when a build is stable.
|
||||
#
|
||||
# Differences from the test deploy (deploy-verso.yml):
|
||||
# - Runs in the `verso` namespace (test runs in `test`).
|
||||
# - Mongo / Redis / app data live on PersistentVolumeClaims and are applied
|
||||
# idempotently: this workflow NEVER deletes them, so data survives deploys.
|
||||
# - The replica set is initialised only once.
|
||||
# - Builds/pushes a distinct image tag (verso:stable) so prod and test never
|
||||
# clobber each other's image.
|
||||
# - SMTP comes from the `verso-smtp` Secret (create it with kubectl); email is
|
||||
# optional so the app still boots before the secret exists.
|
||||
# - Public self-registration stays off (CE default): friends-only, admin
|
||||
# creates accounts / sends invites.
|
||||
#
|
||||
# Out of band (do once): create the `verso-smtp` Secret and a verso.alocoq.fr
|
||||
# Ingress (see server-ce/k8s/verso-prod-ingress.example.yaml) + DNS.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- prod
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
SITE_URL: https://verso.alocoq.fr
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: native
|
||||
timeout-minutes: 240
|
||||
|
||||
steps:
|
||||
- name: Build and push Verso prod image with BuildKit
|
||||
run: |
|
||||
kubectl -n ci delete job verso-buildkit-prod --ignore-not-found=true --wait=true
|
||||
|
||||
cat <<'EOF' | kubectl apply -f -
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: verso-buildkit-prod
|
||||
namespace: ci
|
||||
spec:
|
||||
backoffLimit: 0
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
initContainers:
|
||||
- name: prepare
|
||||
image: alpine/git:latest
|
||||
command: ["sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -eux
|
||||
REG=registry.git.svc.cluster.local:5000
|
||||
git clone --depth 1 --branch prod https://git.alocoq.fr/alois/verso.git /workspace/repo
|
||||
|
||||
# Build the base image only when Dockerfile-base changes
|
||||
# (content-hash tag); otherwise reuse the cached base.
|
||||
BTAG=$(sha256sum /workspace/repo/server-ce/Dockerfile-base | cut -c1-16)
|
||||
printf '%s' "$BTAG" > /workspace/base_tag
|
||||
if wget -qO- "http://$REG/v2/verso-base/tags/list" 2>/dev/null | grep -q "\"base-$BTAG\""; then
|
||||
echo "Base image base-$BTAG already present - skipping base build"
|
||||
else
|
||||
touch /workspace/build-base
|
||||
echo "Base image base-$BTAG not found - base will be built"
|
||||
fi
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
|
||||
containers:
|
||||
- name: buildkit
|
||||
image: moby/buildkit:latest
|
||||
securityContext:
|
||||
privileged: true
|
||||
command: ["sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -eux
|
||||
|
||||
REG=registry.git.svc.cluster.local:5000
|
||||
|
||||
mkdir -p /etc/buildkit
|
||||
printf '[registry."%s"]\n http = true\n insecure = true\n' "$REG" > /etc/buildkit/buildkitd.toml
|
||||
|
||||
BTAG=$(cat /workspace/base_tag)
|
||||
BASE_REF="$REG/verso-base:base-$BTAG"
|
||||
|
||||
if [ -f /workspace/build-base ]; then
|
||||
buildctl-daemonless.sh build \
|
||||
--frontend=dockerfile.v0 \
|
||||
--local context=/workspace/repo \
|
||||
--local dockerfile=/workspace/repo/server-ce \
|
||||
--opt filename=Dockerfile-base \
|
||||
--import-cache type=registry,ref=$REG/verso-cache:base \
|
||||
--export-cache type=registry,ref=$REG/verso-cache:base,mode=max \
|
||||
--output type=image,name=$BASE_REF,push=true,registry.insecure=true
|
||||
else
|
||||
echo "Reusing existing base image $BASE_REF"
|
||||
fi
|
||||
|
||||
# App image → verso:stable (prod tag).
|
||||
buildctl-daemonless.sh build \
|
||||
--frontend=dockerfile.v0 \
|
||||
--local context=/workspace/repo \
|
||||
--local dockerfile=/workspace/repo/server-ce \
|
||||
--opt filename=Dockerfile \
|
||||
--opt build-arg:OVERLEAF_BASE_TAG=$BASE_REF \
|
||||
--import-cache type=registry,ref=$REG/verso-cache:app \
|
||||
--export-cache type=registry,ref=$REG/verso-cache:app,mode=max \
|
||||
--output type=image,name=$REG/verso:stable,push=true,registry.insecure=true
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
|
||||
volumes:
|
||||
- name: workspace
|
||||
emptyDir: {}
|
||||
EOF
|
||||
|
||||
- name: Wait for build
|
||||
run: |
|
||||
kubectl -n ci wait --for=condition=complete job/verso-buildkit-prod --timeout=14400s
|
||||
|
||||
- name: Show build logs
|
||||
if: always()
|
||||
run: |
|
||||
kubectl -n ci logs job/verso-buildkit-prod -c prepare || true
|
||||
kubectl -n ci logs job/verso-buildkit-prod -c buildkit || true
|
||||
|
||||
- name: Ensure namespace + persistent data services (never deleted)
|
||||
run: |
|
||||
kubectl create namespace verso --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# PVCs + Mongo/Redis. Applied idempotently — this step must never
|
||||
# delete these, so project data survives every deploy.
|
||||
cat <<'EOF' | kubectl apply -f -
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: mongo-data
|
||||
namespace: verso
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: redis-data
|
||||
namespace: verso
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: verso-data
|
||||
namespace: verso
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mongo
|
||||
namespace: verso
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mongo
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mongo
|
||||
spec:
|
||||
containers:
|
||||
- name: mongo
|
||||
image: mongo:8
|
||||
command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
|
||||
ports:
|
||||
- containerPort: 27017
|
||||
volumeMounts:
|
||||
- name: mongo-data
|
||||
mountPath: /data/db
|
||||
volumes:
|
||||
- name: mongo-data
|
||||
persistentVolumeClaim:
|
||||
claimName: mongo-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mongo
|
||||
namespace: verso
|
||||
spec:
|
||||
selector:
|
||||
app: mongo
|
||||
ports:
|
||||
- name: mongo
|
||||
port: 27017
|
||||
targetPort: 27017
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: verso
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:7
|
||||
# AOF persistence so a restart doesn't drop in-flight edits
|
||||
# before they're flushed to Mongo.
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
volumeMounts:
|
||||
- name: redis-data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: redis-data
|
||||
persistentVolumeClaim:
|
||||
claimName: redis-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: verso
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- name: redis
|
||||
port: 6379
|
||||
targetPort: 6379
|
||||
EOF
|
||||
|
||||
kubectl -n verso rollout status deployment/mongo --timeout=300s
|
||||
kubectl -n verso rollout status deployment/redis --timeout=300s
|
||||
|
||||
- name: Initialise Mongo replica set (only if not already initialised)
|
||||
run: |
|
||||
kubectl -n verso exec deploy/mongo -- mongosh --quiet --eval '
|
||||
try {
|
||||
rs.status()
|
||||
print("replica set already initialised")
|
||||
} catch (e) {
|
||||
if (e.codeName === "NotYetInitialized" || /no replset config/i.test(e.message)) {
|
||||
rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "mongo:27017" }] })
|
||||
print("replica set initiated")
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
'
|
||||
kubectl -n verso exec deploy/mongo -- mongosh --quiet --eval '
|
||||
while (rs.status().myState !== 1) { sleep(1000) }
|
||||
print("Mongo replica set is PRIMARY")
|
||||
'
|
||||
|
||||
- name: Ensure Verso deployment + service
|
||||
run: |
|
||||
# Stamp the instance name with this build number, e.g. "Verso V0.12 Alpha".
|
||||
NAV_TITLE="Verso V0.${GITHUB_RUN_NUMBER:-${GITEA_RUN_NUMBER:-0}} Alpha"
|
||||
cat <<'EOF' | sed "s|__NAV_TITLE__|${NAV_TITLE}|g" | kubectl apply -f -
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: verso
|
||||
namespace: verso
|
||||
spec:
|
||||
replicas: 1
|
||||
# RWO data volume → can't run two pods at once; recreate on update.
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: verso
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: verso
|
||||
spec:
|
||||
securityContext:
|
||||
# App runs as www-data (uid/gid 33); make the data volume
|
||||
# group-writable by it.
|
||||
fsGroup: 33
|
||||
initContainers:
|
||||
- name: init-data-perms
|
||||
image: busybox:latest
|
||||
command: ["sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -eux
|
||||
mkdir -p /data/template_files /data/user_files \
|
||||
/data/compiles /data/cache /data/output /data/published
|
||||
chown -R 33:33 /data
|
||||
volumeMounts:
|
||||
- name: verso-data
|
||||
mountPath: /data
|
||||
containers:
|
||||
- name: verso
|
||||
image: registry.alocoq.fr/verso:stable
|
||||
# :stable is a fixed tag, so force a pull on every rollout to
|
||||
# pick up the freshly built image.
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
env:
|
||||
- name: OVERLEAF_MONGO_URL
|
||||
value: mongodb://mongo:27017/sharelatex?replicaSet=rs0
|
||||
- name: OVERLEAF_REDIS_HOST
|
||||
value: redis
|
||||
- name: REDIS_HOST
|
||||
value: redis
|
||||
- name: OVERLEAF_APP_NAME
|
||||
value: Verso
|
||||
- name: OVERLEAF_NAV_TITLE
|
||||
value: "__NAV_TITLE__"
|
||||
- name: OVERLEAF_SITE_URL
|
||||
value: https://verso.alocoq.fr
|
||||
- name: OVERLEAF_SITE_LANGUAGE
|
||||
value: fr
|
||||
# Allow anonymous visitors so public published-presentation
|
||||
# links and read-only share links work without login.
|
||||
- name: OVERLEAF_ALLOW_PUBLIC_ACCESS
|
||||
value: "true"
|
||||
# NB: anonymous read-AND-write sharing is intentionally NOT
|
||||
# enabled (compiles are unsandboxed → only trusted accounts
|
||||
# may trigger them). Public self-registration is also off
|
||||
# (CE default): admin creates accounts / sends invites.
|
||||
- name: OVERLEAF_ENABLE_PROJECT_PYTHON_VENV
|
||||
value: "true"
|
||||
# SMTP for password-reset / invite emails. Values come from
|
||||
# the 'verso-smtp' Secret; all optional so the app boots
|
||||
# before the secret exists (email stays off until
|
||||
# OVERLEAF_EMAIL_FROM_ADDRESS is present).
|
||||
- name: OVERLEAF_EMAIL_FROM_ADDRESS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: verso-smtp
|
||||
key: from-address
|
||||
optional: true
|
||||
- name: OVERLEAF_EMAIL_SMTP_HOST
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: verso-smtp
|
||||
key: smtp-host
|
||||
optional: true
|
||||
- name: OVERLEAF_EMAIL_SMTP_PORT
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: verso-smtp
|
||||
key: smtp-port
|
||||
optional: true
|
||||
- name: OVERLEAF_EMAIL_SMTP_SECURE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: verso-smtp
|
||||
key: smtp-secure
|
||||
optional: true
|
||||
- name: OVERLEAF_EMAIL_SMTP_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: verso-smtp
|
||||
key: smtp-user
|
||||
optional: true
|
||||
- name: OVERLEAF_EMAIL_SMTP_PASS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: verso-smtp
|
||||
key: smtp-pass
|
||||
optional: true
|
||||
volumeMounts:
|
||||
- name: verso-data
|
||||
mountPath: /var/lib/overleaf/data
|
||||
volumes:
|
||||
- name: verso-data
|
||||
persistentVolumeClaim:
|
||||
claimName: verso-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: verso
|
||||
namespace: verso
|
||||
spec:
|
||||
selector:
|
||||
app: verso
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 80
|
||||
EOF
|
||||
|
||||
- name: Deploy Verso image
|
||||
run: |
|
||||
kubectl -n verso set image deployment/verso \
|
||||
verso=registry.alocoq.fr/verso:stable
|
||||
kubectl -n verso rollout restart deployment/verso
|
||||
kubectl -n verso rollout status deployment/verso --timeout=600s
|
||||
|
||||
- name: Create initial admin (only if no users exist)
|
||||
run: |
|
||||
COUNT=$(kubectl -n verso exec deploy/mongo -- mongosh sharelatex --quiet --eval 'db.users.countDocuments()' | tr -d '[:space:]')
|
||||
if [ "$COUNT" = "0" ]; then
|
||||
echo "No users yet — creating the initial admin account"
|
||||
kubectl -n verso exec deploy/verso -- bash -lc '
|
||||
cd /overleaf/services/web
|
||||
node modules/server-ce-scripts/scripts/create-user \
|
||||
--admin \
|
||||
--email=alois.coquillard@gmail.com
|
||||
'
|
||||
else
|
||||
echo "Users already exist ($COUNT) — skipping admin creation"
|
||||
fi
|
||||
@@ -0,0 +1,44 @@
|
||||
# Example Ingress for the prod (verso namespace) instance at verso.alocoq.fr.
|
||||
#
|
||||
# This is NOT applied by the deploy workflow on purpose: the test ingress is
|
||||
# managed by hand, and TLS/annotations depend on your cluster's ingress
|
||||
# controller (Traefik) and cert setup. Copy this, adapt it to match how
|
||||
# test.alocoq.fr is wired, then `kubectl apply -f` it once.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - DNS: verso.alocoq.fr → your ingress/load-balancer IP.
|
||||
# - A TLS cert for verso.alocoq.fr (cert-manager, or a manually created
|
||||
# Secret referenced under tls.secretName).
|
||||
#
|
||||
# Adjust:
|
||||
# - ingressClassName (e.g. "traefik") to match your controller.
|
||||
# - annotations (cert-manager issuer, Traefik entrypoints/router, etc.) to
|
||||
# match the test ingress.
|
||||
# - the TLS block (cert-manager will create the secret; otherwise create it).
|
||||
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: verso
|
||||
namespace: verso
|
||||
annotations:
|
||||
# --- adapt these to match your test.alocoq.fr ingress ---
|
||||
# cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
# traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- verso.alocoq.fr
|
||||
secretName: verso-tls
|
||||
rules:
|
||||
- host: verso.alocoq.fr
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: verso
|
||||
port:
|
||||
number: 80
|
||||
Reference in New Issue
Block a user