forked from Nixius/authelia
1
0
Fork 0

Compare commits

...

5 Commits

Author SHA1 Message Date
Leopere c68edc70d1
Switch customer stack to Gitea + PostgreSQL two-service pattern
- web: Gitea (self-hosted Git), exposed via Traefik behind Authelia
- db: PostgreSQL 16, internal backend network only, never exposed
- Establishes the canonical web+db template structure for future products

Made-with: Cursor
2026-03-03 17:02:49 -05:00
Leopere 463483f769
Unify stack action button behaviour via single event listener
Replace per-form onsubmit handlers with a single script that handles
all data-stack-action forms identically: confirm if needed, then
disable the button and show a contextual loading label.

Made-with: Cursor
2026-03-03 17:00:58 -05:00
Leopere 239d2c07e1
Disable stack action buttons on submit to prevent spam
Made-with: Cursor
2026-03-03 16:58:56 -05:00
Leopere 084548fcd7
Fix dashboard stack state UI after Destroy
- Inverted condition was showing 'being provisioned' when stack not deployed
- Actions block was gated on StackDeployed so no Start button after destroy
- Start button now always shown when not running
- Destroy button only shown when stack is deployed
- 'Being provisioned' message replaced with accurate 'stopped' message

Made-with: Cursor
2026-03-03 16:56:48 -05:00
Leopere 6356cbb1da
Fix Destroy being immediately undone by dashboard auto-redeploy
Dashboard was auto-deploying any missing stack on every page load.
This stomped on the Destroy action. Stack creation only happens at
activation and via explicit Start — not on dashboard render.

Made-with: Cursor
2026-03-03 16:52:31 -05:00
3 changed files with 65 additions and 30 deletions

View File

@ -38,14 +38,6 @@ func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) {
if err != nil {
log.Printf("dashboard: stack check failed for %s: %v", remoteUser, err)
}
if !exists {
log.Printf("dashboard: deploying missing stack %s", stackName)
if err := a.swarm.DeployStack(stackName, remoteUser, a.cfg.TraefikDomain); err != nil {
log.Printf("dashboard: stack deploy failed for %s: %v", remoteUser, err)
} else {
exists = true
}
}
stackDeployed = exists
if exists {
replicas, _ := a.swarm.GetWebReplicas(stackName)

View File

@ -173,35 +173,35 @@
{{if .StackRunning}}
<p style="color: var(--muted); font-size: 0.9rem; margin-top: 0.75rem;">Your dedicated environment is accessible at:</p>
<a class="stack-link" href="https://{{.User}}.{{.Domain}}">{{.User}}.{{.Domain}}</a>
{{else if not .StackDeployed}}
<p style="color: var(--muted); font-size: 0.9rem; margin-top: 0.75rem;">Your stack is being provisioned. Refresh this page in a moment.</p>
{{else if and .StackDeployed (not .StackRunning)}}
<p style="color: var(--muted); font-size: 0.9rem; margin-top: 0.75rem;">Your stack is stopped. Start it to access your environment.</p>
{{end}}
{{if .StackDeployed}}
<hr class="divider">
<div class="actions">
{{if .StackRunning}}
<form method="POST" action="/stack-manage" style="margin:0">
<form method="POST" action="/stack-manage" style="margin:0" data-stack-action>
<input type="hidden" name="action" value="restart">
<button type="submit" class="btn btn-outline btn-sm">Restart</button>
</form>
<form method="POST" action="/stack-manage" style="margin:0">
<form method="POST" action="/stack-manage" style="margin:0" data-stack-action>
<input type="hidden" name="action" value="stop">
<button type="submit" class="btn btn-warning btn-sm">Stop</button>
</form>
{{else}}
<form method="POST" action="/stack-manage" style="margin:0">
<form method="POST" action="/stack-manage" style="margin:0" data-stack-action>
<input type="hidden" name="action" value="start">
<button type="submit" class="btn btn-sm">Start</button>
</form>
{{end}}
<form method="POST" action="/stack-manage" style="margin:0"
onsubmit="return confirm('Destroy your stack? All containers will be removed. Volumes are preserved.')">
{{if .StackDeployed}}
<form method="POST" action="/stack-manage" style="margin:0" data-stack-action
data-confirm="Destroy your stack? All containers will be removed. Volumes are preserved.">
<input type="hidden" name="action" value="destroy">
<button type="submit" class="btn btn-danger btn-sm">Destroy</button>
</form>
{{end}}
</div>
{{end}}
</div>
<div class="card">
<h2>Manage</h2>
@ -244,5 +244,18 @@
{{end}}
</div>
<div class="version-badge">{{.Commit}}</div>
<script>
const pending = { restart:'Restarting…', stop:'Stopping…', start:'Starting…', destroy:'Destroying…' };
document.querySelectorAll('form[data-stack-action]').forEach(function(form) {
form.addEventListener('submit', function(e) {
var msg = form.dataset.confirm;
if (msg && !confirm(msg)) { e.preventDefault(); return; }
var btn = form.querySelector('button');
var action = (form.querySelector('input[name="action"]') || {}).value;
btn.disabled = true;
btn.textContent = pending[action] || '…';
});
});
</script>
</body>
</html>

View File

@ -1,17 +1,21 @@
# =============================================================================
# CUSTOMER STACK TEMPLATE — Uptime Kuma
# CUSTOMER STACK TEMPLATE — Gitea + PostgreSQL
# =============================================================================
# This is the Docker Swarm stack that gets deployed for each paying customer.
# This is the Docker Swarm stack deployed for each paying customer.
# It defines what product/service they receive when they subscribe.
#
# PRODUCT: Uptime Kuma — a self-hosted uptime/monitoring dashboard.
# PRODUCT: Gitea — a self-hosted Git service, backed by PostgreSQL.
# Each customer gets their own isolated instance at their subdomain.
#
# To sell a different product, replace the `web` service image and adjust
# the port in the Traefik loadbalancer label accordingly.
# Structure:
# web — the application, exposed via Traefik behind Authelia auth
# db — PostgreSQL, internal only (backend network, never exposed)
#
# To sell a different product: replace the `web` image, update the port
# in the Traefik loadbalancer label, and adjust `db` env/image as needed.
#
# Template variables (injected at deploy time by swarm/client.go):
# {{.ID}} - customer's username (used for unique resource naming)
# {{.ID}} - customer's username (unique resource naming)
# {{.Subdomain}} - customer's subdomain (same as ID by default)
# {{.Domain}} - base domain (e.g. bc.a250.ca)
# {{.TraefikNetwork}} - Traefik overlay network name
@ -19,16 +23,23 @@
# Each customer gets their stack at: https://{{.Subdomain}}.{{.Domain}}
# Access is restricted to the owning user via Authelia forward-auth.
# =============================================================================
version: "3.8"
services:
web:
image: louislam/uptime-kuma:1
image: gitea/gitea:1-rootless
environment:
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: db:5432
GITEA__database__NAME: gitea
GITEA__database__USER: gitea
GITEA__database__PASSWD: gitea
GITEA__server__DOMAIN: "{{.Subdomain}}.{{.Domain}}"
GITEA__server__ROOT_URL: "https://{{.Subdomain}}.{{.Domain}}"
GITEA__server__HTTP_PORT: "3000"
volumes:
- uptime_data:/app/data
- gitea_data:/var/lib/gitea
networks:
- traefik_net
- backend
deploy:
replicas: 1
labels:
@ -38,7 +49,22 @@ services:
traefik.http.routers.customer-{{.ID}}-web.entrypoints: "websecure"
traefik.http.routers.customer-{{.ID}}-web.tls: "true"
traefik.http.routers.customer-{{.ID}}-web.middlewares: "authelia-auth@swarm"
traefik.http.services.customer-{{.ID}}-web.loadbalancer.server.port: "3001"
traefik.http.services.customer-{{.ID}}-web.loadbalancer.server.port: "3000"
restart_policy:
condition: on-failure
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: gitea
POSTGRES_USER: gitea
POSTGRES_PASSWORD: gitea
volumes:
- db_data:/var/lib/postgresql/data
networks:
- backend
deploy:
replicas: 1
restart_policy:
condition: on-failure
@ -46,7 +72,11 @@ networks:
traefik_net:
external: true
name: "atlas_{{.TraefikNetwork}}"
backend:
driver: overlay
volumes:
uptime_data:
gitea_data:
driver: local
db_data:
driver: local