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