diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..f96083c --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/index.html b/public/index.html index 2e1590d..ef6dc6e 100644 --- a/public/index.html +++ b/public/index.html @@ -3,27 +3,133 @@ - testsite - + testsite — a deployment demo + + + -

testsite is live

-

Served by nginx on port 8080, deployed by Gitea Actions to Docker Swarm.

-

Build: dev

+ + +
+
+
+

Deployment demo

+

A static site, shipped on every git push.

+

+ Gitea Actions builds the container, pushes to the internal registry, + then Swarm does a zero-downtime rolling update behind Traefik. + No manual steps after the first deploy. +

+ +
+
+ +
+
+

What this site demonstrates

+
+
+

Push to deploy

+

+ Commit to main and a Gitea Actions workflow builds + a fresh image tagged with the commit SHA, pushes it, and tells + Swarm to roll the new version in. +

+
+
+

Zero downtime

+

+ Two replicas spread across worker nodes. The + start-first update order brings the new container + up before draining the old one — no dropped requests. +

+
+
+

Automatic TLS

+

+ Traefik watches Swarm labels, discovers the service on the + traefik-public overlay, and fetches a Let’s + Encrypt certificate without any extra config. +

+
+
+
+
+ +
+
+

The stack

+
+
+
Source & CI
+
Gitea at git.dev.serso.org with Gitea Actions.
+
+
+
Build
+
nginx:1.27-alpine serving /public on port 8080.
+
+
+
Registry
+
Gitea’s built-in OCI registry — one login per runner, per node.
+
+
+
Runtime
+
Docker Swarm, constrained to worker nodes.
+
+
+
Ingress
+
Traefik on the traefik-public overlay, Let’s Encrypt resolver.
+
+
+
Deploy trigger
+
docker service update --image <sha> testwebsite_web over SSH.
+
+
+
+
+ +
+
+

Contact

+

+ Questions or want to reuse this template for another app? + Drop a line at + robin@serso.be. +

+
+
+
+ + + + diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..1958847 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,262 @@ +:root { + color-scheme: light dark; + --bg: #ffffff; + --bg-alt: #f6f7f9; + --fg: #0f172a; + --fg-muted: #55607a; + --border: #e4e7ee; + --accent: #2b5bd7; + --accent-fg: #ffffff; + --radius: 10px; + --maxw: 68rem; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0b0f19; + --bg-alt: #111827; + --fg: #e6e8ef; + --fg-muted: #9aa3b8; + --border: #1f2738; + --accent: #7aa2ff; + --accent-fg: #0b0f19; + } +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + padding: 0; +} + +body { + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + background: var(--bg); + color: var(--fg); + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +.container { + width: 100%; + max-width: var(--maxw); + margin: 0 auto; + padding: 0 1.25rem; +} + +.container.narrow { max-width: 42rem; } + +code { + background: color-mix(in srgb, var(--fg) 8%, transparent); + padding: 0.12em 0.35em; + border-radius: 5px; + font-size: 0.92em; + font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; +} + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { text-decoration: underline; } + +/* ── Header / nav ──────────────────────────────────────── */ +.site-header { + border-bottom: 1px solid var(--border); + background: var(--bg); + position: sticky; + top: 0; + z-index: 10; + backdrop-filter: saturate(180%) blur(6px); +} + +.nav { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 3.75rem; + gap: 1rem; +} + +.logo { + display: inline-flex; + align-items: center; + gap: 0.6rem; + font-weight: 700; + color: var(--fg); + font-size: 1.05rem; +} + +.logo:hover { text-decoration: none; } + +.logo-mark { + width: 1.25rem; + height: 1.25rem; + border-radius: 6px; + background: linear-gradient(135deg, var(--accent), #9bb5ff); + display: inline-block; +} + +.nav nav { + display: flex; + gap: 1.25rem; +} + +.nav nav a { + color: var(--fg-muted); + font-size: 0.95rem; +} + +.nav nav a:hover { color: var(--fg); text-decoration: none; } + +/* ── Hero ──────────────────────────────────────────────── */ +.hero { + padding: 5rem 0 4rem; + text-align: left; +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.1em; + font-size: 0.78rem; + color: var(--accent); + font-weight: 600; + margin: 0 0 0.75rem; +} + +.hero h1 { + font-size: clamp(1.9rem, 4vw, 2.9rem); + line-height: 1.15; + margin: 0 0 1rem; + letter-spacing: -0.015em; + max-width: 36ch; +} + +.hero .lede { + font-size: 1.1rem; + color: var(--fg-muted); + max-width: 56ch; + margin: 0 0 1.75rem; +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.btn { + display: inline-flex; + align-items: center; + padding: 0.625rem 1.1rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg); + color: var(--fg); + font-weight: 500; + font-size: 0.95rem; + transition: background-color 0.15s, transform 0.06s; +} + +.btn:hover { text-decoration: none; background: var(--bg-alt); } +.btn:active { transform: translateY(1px); } + +.btn-primary { + background: var(--accent); + color: var(--accent-fg); + border-color: var(--accent); +} + +.btn-primary:hover { + background: color-mix(in srgb, var(--accent) 85%, #000); +} + +/* ── Sections ──────────────────────────────────────────── */ +.section { + padding: 4rem 0; + border-top: 1px solid var(--border); +} + +.section-alt { + background: var(--bg-alt); +} + +.section h2 { + font-size: clamp(1.4rem, 2.5vw, 1.75rem); + margin: 0 0 1.75rem; + letter-spacing: -0.01em; +} + +/* ── Feature grid ──────────────────────────────────────── */ +.grid-3 { + display: grid; + gap: 1.25rem; + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); +} + +.card { + padding: 1.5rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg); +} + +.card h3 { + margin: 0 0 0.5rem; + font-size: 1.05rem; +} + +.card p { + margin: 0; + color: var(--fg-muted); +} + +/* ── Stack description list ────────────────────────────── */ +.stack-list { + display: grid; + gap: 0.9rem 2rem; + grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr)); + margin: 0; +} + +.stack-list > div { + padding: 1rem 1.25rem; + border-radius: var(--radius); + background: var(--bg); + border: 1px solid var(--border); +} + +.stack-list dt { + font-weight: 600; + font-size: 0.82rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--fg-muted); + margin-bottom: 0.2rem; +} + +.stack-list dd { + margin: 0; + color: var(--fg); +} + +/* ── Footer ────────────────────────────────────────────── */ +.site-footer { + border-top: 1px solid var(--border); + padding: 2rem 0; + margin-top: 2rem; + color: var(--fg-muted); + font-size: 0.9rem; +} + +.footer-inner { + display: flex; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} + +.footer-inner .meta a { color: var(--fg-muted); } +.footer-inner p { margin: 0; }