Initial scaffold: static site served by nginx on :8080
Build & Deploy / build (push) Failing after 3s
Build & Deploy / deploy (push) Has been skipped

- 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:
rlabadmin
2026-04-23 14:21:51 +02:00
commit 48a3d3f3f5
6 changed files with 185 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
.git
.gitea
.gitignore
.dockerignore
stack.yml
README.md
*.md
+90
View File
@@ -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 }}"
+9
View File
@@ -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
View File
@@ -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";
}
}
+29
View File
@@ -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>
+33
View File
@@ -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"