build: add Docker and CI/CD pipeline for web deployment

- Add multi-stage Dockerfile (Flutter build → nginx:alpine)
- Add nginx.conf with SPA routing, gzip, and caching
- Add Gitea workflow for release-triggered builds
- Document release procedure in AGENTS.md

Deploys to port 8092, auto-updates via Watchtower.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeroen Schweitzer
2026-01-02 12:25:11 +01:00
co-authored by Claude Opus 4.5
parent 6989241ae7
commit aa3fe916b7
4 changed files with 192 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
name: Build and Push
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Gitea Registry
uses: docker/login-action@v3
with:
registry: git.schweitz.internal
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
git.schweitz.internal/jpmschweitzer/tatlock-ui:latest
git.schweitz.internal/jpmschweitzer/tatlock-ui:${{ github.ref_name }}
- name: Trigger Watchtower update
if: success()
run: |
curl -sf -H "Authorization: Bearer ${{ secrets.WATCHTOWER_TOKEN }}" \
http://watchtower:8080/v1/update
+30
View File
@@ -42,7 +42,36 @@ This document contains instructions and documentation references for AI assistan
* Tag after updating `pubspec.yaml` version and CHANGELOG * Tag after updating `pubspec.yaml` version and CHANGELOG
* Push tags with `git push --tags` * Push tags with `git push --tags`
### 🚀 Release Procedure
This project uses version-tag-based CI/CD. Releases trigger automated Docker builds and deployments.
**Release Steps:**
1. Update version in `pubspec.yaml` (bump major.minor.patch, not build number)
2. Update `CHANGELOG.md` with changes under `## [x.x.x] - YYYY-MM-DD`
3. Commit changes: `git commit -m "chore: release vX.X.X"`
4. Create git tag: `git tag vX.X.X`
5. Push with tags: `git push origin master --tags`
6. Create release in Gitea UI (git.schweitz.net → Releases → New Release)
* Select the tag
* Add release notes (can copy from CHANGELOG)
* **Publish** the release (this triggers CI/CD)
**What happens on release:**
* Gitea CI builds Flutter web app in Docker
* Image pushed to `git.schweitz.internal/jpmschweitzer/tatlock-ui:latest` and `:vX.X.X`
* Watchtower detects new image and auto-updates running container
* App available at `http://tower:8092` (and eventually `home.schweitz.net`)
**Rollback:**
* In Portainer, update image tag to previous version (e.g., `:v0.2.0`)
* Or: `docker pull git.schweitz.internal/jpmschweitzer/tatlock-ui:v0.2.0`
### 🧪 Testing Requirements ### 🧪 Testing Requirements
* **Always add tests for new code before committing.** No exceptions. * **Always add tests for new code before committing.** No exceptions.
* Tests should cover the happy path and key edge cases. * Tests should cover the happy path and key edge cases.
* Run `flutter test` before committing to ensure all tests pass. * Run `flutter test` before committing to ensure all tests pass.
@@ -50,6 +79,7 @@ This document contains instructions and documentation references for AI assistan
* Code coverage should not decrease with new commits. * Code coverage should not decrease with new commits.
### 📝 Changelog Maintenance ### 📝 Changelog Maintenance
* **Update `CHANGELOG.md`** with every user-facing change. * **Update `CHANGELOG.md`** with every user-facing change.
* Format: `## [Unreleased] - YYYY-MM-DD` followed by `### Added`, `### Changed`, or `### Fixed`. * Format: `## [Unreleased] - YYYY-MM-DD` followed by `### Added`, `### Changed`, or `### Fixed`.
+41
View File
@@ -0,0 +1,41 @@
# Stage 1: Build Flutter web application
FROM ghcr.io/cirruslabs/flutter:stable AS builder
WORKDIR /app
# Copy dependency files first for better caching
COPY pubspec.yaml pubspec.lock ./
# Get dependencies
RUN flutter pub get
# Copy the rest of the application
COPY . .
# Generate code with build_runner
RUN dart run build_runner build --delete-conflicting-outputs
# Build for web release
RUN flutter build web --release
# Stage 2: Serve with nginx
FROM nginx:alpine
# Install curl for healthcheck
RUN apk add --no-cache curl
# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Copy built web app to nginx html directory
COPY --from=builder /app/build/web /usr/share/nginx/html
# Expose port 80
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:80/ || exit 1
# Run nginx in foreground
CMD ["nginx", "-g", "daemon off;"]
+87
View File
@@ -0,0 +1,87 @@
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1000;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/x-javascript
application/xml
application/xml+rss
image/svg+xml
font/woff
font/woff2;
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# SPA routing: all routes fall back to index.html
location / {
try_files $uri $uri/ /index.html;
}
# No cache for index.html (ensures updates are picked up)
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# Long cache for Flutter assets (content-hash naming ensures freshness)
location ~* \.(js|css|woff|woff2|ttf|eot|ico|png|jpg|jpeg|gif|svg|webp)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Cache manifest and version files briefly
location ~* \.(json|webmanifest)$ {
expires 1h;
add_header Cache-Control "public";
}
# Health check endpoint
location /health {
access_log off;
return 200 "OK";
add_header Content-Type text/plain;
}
}
}