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:
co-authored by
Claude Opus 4.5
parent
6989241ae7
commit
aa3fe916b7
@@ -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
|
||||
@@ -42,7 +42,36 @@ This document contains instructions and documentation references for AI assistan
|
||||
* Tag after updating `pubspec.yaml` version and CHANGELOG
|
||||
* 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
|
||||
|
||||
* **Always add tests for new code before committing.** No exceptions.
|
||||
* Tests should cover the happy path and key edge cases.
|
||||
* 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.
|
||||
|
||||
### 📝 Changelog Maintenance
|
||||
|
||||
* **Update `CHANGELOG.md`** with every user-facing change.
|
||||
* Format: `## [Unreleased] - YYYY-MM-DD` followed by `### Added`, `### Changed`, or `### Fixed`.
|
||||
|
||||
|
||||
+41
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user