Initial scaffold: static site served by nginx on :8080
- public/index.html served via nginx.conf (port 8080) - Dockerfile: nginx:1.27-alpine + HEALTHCHECK - .gitea/workflows/flow.yml: build + push to Gitea registry, rolling deploy - stack.yml: Swarm service wired to Traefik (host testwebsite.dev.serso.org) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
.git
|
||||||
|
.gitea
|
||||||
|
.gitignore
|
||||||
|
.dockerignore
|
||||||
|
stack.yml
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# .gitea/workflows/flow.yml
|
||||||
|
# =============================================================
|
||||||
|
# serso — generic CI/CD flow
|
||||||
|
# Copy into any app repo as .gitea/workflows/flow.yml
|
||||||
|
# Adjust the env block (STACK_NAME, SERVICE_NAME, APP_DOMAIN) per app
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
name: Build & Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
# ─── Registry ─────────────────────────────────────────────
|
||||||
|
REGISTRY: git.dev.serso.org
|
||||||
|
IMAGE: ${{ github.repository }} # → owner/reponame
|
||||||
|
|
||||||
|
# ─── Per-app knobs (edit these) ───────────────────────────
|
||||||
|
STACK_NAME: testwebsite # Swarm stack name
|
||||||
|
SERVICE_NAME: testwebsite_web # {stack}_{service}
|
||||||
|
APP_DOMAIN: testwebsite.dev.serso.org # used only for logging
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 1. Build image + push to Gitea registry
|
||||||
|
# ============================================================
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
image_tag: ${{ steps.meta.outputs.tag }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Compute tag
|
||||||
|
id: meta
|
||||||
|
run: echo "tag=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Log in to Gitea registry
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
|
||||||
|
docker login ${{ env.REGISTRY }} \
|
||||||
|
--username ${{ secrets.REGISTRY_USER }} \
|
||||||
|
--password-stdin
|
||||||
|
|
||||||
|
- name: Build image
|
||||||
|
run: |
|
||||||
|
docker build \
|
||||||
|
-t ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ steps.meta.outputs.tag }} \
|
||||||
|
-t ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
||||||
|
.
|
||||||
|
|
||||||
|
- name: Push image
|
||||||
|
run: |
|
||||||
|
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ steps.meta.outputs.tag }}
|
||||||
|
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 2. Deploy to Swarm (rolling update)
|
||||||
|
# ============================================================
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Prepare SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
- name: Rolling deploy on Swarm
|
||||||
|
run: |
|
||||||
|
ssh -i ~/.ssh/deploy_key \
|
||||||
|
${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} \
|
||||||
|
"docker service update \
|
||||||
|
--image ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ needs.build.outputs.image_tag }} \
|
||||||
|
--with-registry-auth \
|
||||||
|
--update-order start-first \
|
||||||
|
--update-failure-action rollback \
|
||||||
|
${{ env.SERVICE_NAME }}"
|
||||||
|
|
||||||
|
- name: Deployed 🎉
|
||||||
|
run: |
|
||||||
|
echo "App deployed at https://${{ env.APP_DOMAIN }}"
|
||||||
|
echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ needs.build.outputs.image_tag }}"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY public/ /usr/share/nginx/html/
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
|
||||||
|
CMD wget -q -O - http://127.0.0.1:8080/ >/dev/null || exit 1
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /healthz {
|
||||||
|
access_log off;
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
return 200 "ok\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>testsite</title>
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: light dark; }
|
||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: 4rem auto;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||||
|
padding: 0.1em 0.35em;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
}
|
||||||
|
.meta { opacity: 0.7; font-size: 0.9rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>testsite is live</h1>
|
||||||
|
<p>Served by <code>nginx</code> on port 8080, deployed by Gitea Actions to Docker Swarm.</p>
|
||||||
|
<p class="meta">Build: <code id="build">dev</code></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# stack.yml — generic Swarm deploy file
|
||||||
|
# Deploy once: docker stack deploy -c stack.yml myapp
|
||||||
|
# After that, CI updates the image via `docker service update` — no redeploy needed
|
||||||
|
|
||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: git.dev.serso.org/test/testsite:latest
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
deploy:
|
||||||
|
replicas: 2
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.role == worker # run on node01/node02, not cp
|
||||||
|
update_config:
|
||||||
|
parallelism: 1
|
||||||
|
order: start-first # zero-downtime (spin new up, then down old)
|
||||||
|
failure_action: rollback
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.testwebsite.rule=Host(`testwebsite.dev.serso.org`)"
|
||||||
|
- "traefik.http.routers.testwebsite.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.testwebsite.tls.certresolver=letsencrypt"
|
||||||
|
# Port that your app listens on INSIDE the container
|
||||||
|
- "traefik.http.services.testwebsite.loadbalancer.server.port=8080"
|
||||||
Reference in New Issue
Block a user