Add Sigvild Gallery wedding photo application with automated deployment and improve Caddy plugin management

This commit is contained in:
2025-11-15 16:13:18 +01:00
parent 8162e789ee
commit 7c3b02e5ad
16 changed files with 923 additions and 10 deletions

View File

@@ -0,0 +1,180 @@
# Sigvild Gallery Ansible Role
Deploys the Sigvild Wedding Gallery application with PocketBase API backend and SvelteKit frontend.
## Architecture
- **Backend**: PocketBase-based Go application serving API on localhost:8090
- **Frontend**: SvelteKit static site served by Caddy
- **Database**: SQLite via PocketBase (file-based storage)
- **Authentication**: Shared password system (host/guest users)
- **Domains**:
- `sigvild.no` → Frontend static files
- `api.sigvild.no` → Backend API proxy
## Prerequisites
- Caddy role deployed and configured
- Local sigvild-gallery project with built assets in `build_tmp/`
- Vault-encrypted passwords configured in inventory
## Variables
### Required Variables
```yaml
# Domains
sigvild_gallery_frontend_domain: "sigvild.no"
sigvild_gallery_api_domain: "api.sigvild.no"
# Vault-encrypted passwords
vault_sigvild_host_password: "your-encrypted-host-password"
vault_sigvild_guest_password: "your-encrypted-guest-password"
```
### Optional Variables
```yaml
# Service configuration
sigvild_gallery_user: "sigvild"
sigvild_gallery_port: 8090
sigvild_gallery_host: "127.0.0.1"
# Paths
sigvild_gallery_home: "/opt/sigvild-gallery"
sigvild_gallery_web_root: "/var/www/sigvild-gallery"
sigvild_gallery_local_project_path: "{{ ansible_env.PWD }}/sigvild-gallery"
```
## Usage
### Full Deployment
```bash
# Deploy complete infrastructure including Sigvild Gallery
ansible-playbook site.yml
# Deploy just Sigvild Gallery
ansible-playbook playbooks/deploy-sigvild.yml
```
### Selective Updates
```bash
# Update just the frontend
ansible-playbook site.yml --tags="frontend"
# Update just the backend API
ansible-playbook site.yml --tags="backend"
# Update Caddy configuration
ansible-playbook site.yml --tags="caddy"
```
## Security Features
### Environment Variables
- **No .env files**: Secrets managed via systemd Environment directives
- **Vault encrypted**: Passwords stored in Ansible vault
- **Memory-only**: Environment variables only exist in process memory
### SystemD Sandboxing
- `NoNewPrivileges=yes`: Prevents privilege escalation
- `PrivateTmp=yes`: Isolated temporary directory
- `ProtectSystem=strict`: Read-only filesystem protection
- `ProtectHome=yes`: Home directory protection
- `ReadWritePaths`: Only data directory is writable
### Caddy Security
- **Security headers**: XSS protection, frame options, content type sniffing prevention
- **CORS configuration**: Restricted to frontend domain
- **Rate limiting**: API endpoint protection
- **HTTPS only**: Automatic TLS with Let's Encrypt
## Directory Structure
```
/opt/sigvild-gallery/ # Application home
├── sigvild-gallery-server # Go binary
└── data/ # PocketBase data directory
├── data.db # SQLite database
└── storage/ # File uploads
/var/www/sigvild-gallery/ # Frontend web root
├── index.html # SvelteKit build
├── _app/ # Application assets
└── assets/ # Static assets
/etc/systemd/system/
└── sigvild-gallery.service # SystemD service
/etc/caddy/sites-enabled/
├── sigvild-frontend.caddy # Frontend configuration
└── sigvild-api.caddy # API proxy configuration
```
## Build Process
The role performs local builds then transfers assets:
1. **Backend**: `GOOS=linux GOARCH=amd64 go build -o sigvild-gallery-server .`
2. **Frontend**: `npm run build` in `sigvild-kit/` directory
3. **Transfer**: Copy binary and sync frontend build to server
4. **Deploy**: Update systemd service and Caddy configuration
## Service Management
```bash
# Check service status
systemctl status sigvild-gallery
# View logs
journalctl -u sigvild-gallery -f
# Restart service
systemctl restart sigvild-gallery
# Reload Caddy configuration
systemctl reload caddy
```
## Troubleshooting
### Build Failures
- Ensure Go toolchain is available locally
- Verify `sigvild-kit/` directory exists with `package.json`
- Check Node.js and npm are installed for frontend builds
### Service Startup Issues
- Check systemd logs: `journalctl -u sigvild-gallery`
- Verify binary permissions and ownership
- Ensure data directory is writable by service user
### Domain Resolution
- Verify DNS records point to server IP
- Check Caddy logs: `journalctl -u caddy`
- Test local connectivity: `curl -H "Host: api.sigvild.no" http://localhost:8090`
## Dependencies
- **caddy**: Required for web server and reverse proxy
- **systemd**: Service management
- **Local build tools**: Go compiler, Node.js/npm
## Files Created
- `/etc/systemd/system/sigvild-gallery.service`
- `/etc/caddy/sites-enabled/sigvild-frontend.caddy`
- `/etc/caddy/sites-enabled/sigvild-api.caddy`
- `/opt/sigvild-gallery/` (application directory)
- `/var/www/sigvild-gallery/` (frontend files)
## Tags
- `sigvild`: Complete Sigvild Gallery deployment
- `backend`: API service deployment
- `frontend`: Static site deployment
- `build`: Local build processes
- `service`: SystemD service management
- `caddy`: Caddy configuration
- `verify`: Post-deployment verification

View File

@@ -0,0 +1,36 @@
---
# Sigvild Gallery Ansible Role - Default Variables
# Service Configuration
sigvild_gallery_user: sigvild
# Paths
sigvild_gallery_home: /opt/sigvild-gallery
sigvild_gallery_web_root: /var/www/sigvild-gallery
sigvild_gallery_binary: "{{ sigvild_gallery_home }}/sigvild-gallery"
sigvild_gallery_data_dir: "{{ sigvild_gallery_home }}/pb_data"
# Domains
sigvild_gallery_frontend_domain: sigvild.no
sigvild_gallery_api_domain: api.sigvild.no
# Backend Service
sigvild_gallery_port: 8090
sigvild_gallery_host: "127.0.0.1"
# Environment Variables (for SystemD service)
sigvild_gallery_host_username: host
sigvild_gallery_host_password: "{{ vault_sigvild_host_password }}"
sigvild_gallery_guest_username: guest
sigvild_gallery_guest_password: "{{ vault_sigvild_guest_password }}"
# Build configuration
sigvild_gallery_local_project_path: "{{ ansible_env.PWD }}/sigvild-gallery"
# Service configuration
sigvild_gallery_service_enabled: true
sigvild_gallery_service_state: started
# Caddy integration (assumes caddy role provides these)
# caddy_sites_enabled_dir: /etc/caddy/sites-enabled
# caddy_user: caddy

View File

@@ -0,0 +1,16 @@
---
# Sigvild Gallery Handlers
- name: reload systemd
systemd:
daemon_reload: yes
- name: restart sigvild-gallery
systemd:
name: sigvild-gallery
state: restarted
- name: reload caddy
systemd:
name: caddy
state: reloaded

View File

@@ -0,0 +1,20 @@
---
# Role Dependencies
dependencies:
- role: caddy
galaxy_info:
role_name: sigvild-gallery
author: "Rick Infrastructure Team"
description: "Deploys Sigvild Wedding Gallery with PocketBase API and SvelteKit frontend"
company: ""
license: "license (MIT)"
min_ansible_version: "2.9"
platforms:
- name: Archlinux
versions:
- all
- name: Ubuntu
versions:
- all

View File

@@ -0,0 +1,43 @@
---
# Backend Deployment Tasks
- name: Build Go binary locally
local_action:
module: shell
cmd: GOOS=linux GOARCH=amd64 go build -o sigvild-gallery .
chdir: "{{ sigvild_gallery_local_project_path }}"
become: no
tags: [backend, build]
- name: Check if binary was built successfully
local_action:
module: stat
path: "{{ sigvild_gallery_local_project_path }}/sigvild-gallery"
register: binary_stat
become: no
tags: [backend, build]
- name: Fail if binary doesn't exist
fail:
msg: "Failed to build sigvild-gallery binary"
when: not binary_stat.stat.exists
tags: [backend, build]
- name: Transfer Go binary
copy:
src: "{{ sigvild_gallery_local_project_path }}/sigvild-gallery"
dest: "{{ sigvild_gallery_binary }}"
owner: "{{ sigvild_gallery_user }}"
group: "{{ sigvild_gallery_user }}"
mode: '0755'
notify: restart sigvild-gallery
tags: [backend]
- name: Create data directory for PocketBase
file:
path: "{{ sigvild_gallery_data_dir }}"
state: directory
owner: "{{ sigvild_gallery_user }}"
group: "{{ sigvild_gallery_user }}"
mode: '0755'
tags: [backend]

View File

@@ -0,0 +1,57 @@
---
# Frontend Deployment Tasks
- name: Check if frontend source exists
local_action:
module: stat
path: "{{ sigvild_gallery_local_project_path }}/sigvild-kit"
register: frontend_source
become: no
tags: [frontend, build]
- name: Fail if frontend source doesn't exist
fail:
msg: "Frontend source directory not found at {{ sigvild_gallery_local_project_path }}/sigvild-kit"
when: not frontend_source.stat.exists
tags: [frontend, build]
- name: Install frontend dependencies
local_action:
module: shell
cmd: npm install
chdir: "{{ sigvild_gallery_local_project_path }}/sigvild-kit"
become: no
tags: [frontend, build]
- name: Build frontend for production
local_action:
module: shell
cmd: npm run build:production
chdir: "{{ sigvild_gallery_local_project_path }}/sigvild-kit"
become: no
tags: [frontend, build]
- name: Check if frontend build exists
local_action:
module: stat
path: "{{ sigvild_gallery_local_project_path }}/sigvild-kit/build"
register: frontend_build
become: no
tags: [frontend, build]
- name: Fail if frontend build doesn't exist
fail:
msg: "Frontend build failed - build directory not found"
when: not frontend_build.stat.exists
become: no
tags: [frontend, build]
- name: Sync frontend files to web root
synchronize:
src: "{{ sigvild_gallery_local_project_path }}/sigvild-kit/build/"
dest: "{{ sigvild_gallery_web_root }}/"
delete: yes
rsync_opts:
- "--exclude=.git"
- "--chown={{ sigvild_gallery_user }}:{{ sigvild_gallery_user }}"
tags: [frontend]

View File

@@ -0,0 +1,91 @@
---
# Sigvild Gallery Deployment Tasks
- name: Install required packages
pacman:
name:
- rsync
state: present
- name: Create sigvild gallery user
user:
name: "{{ sigvild_gallery_user }}"
system: yes
shell: /bin/bash
home: "{{ sigvild_gallery_home }}"
create_home: yes
- name: Create directories
file:
path: "{{ item }}"
state: directory
owner: "{{ sigvild_gallery_user }}"
group: "{{ sigvild_gallery_user }}"
mode: '0755'
loop:
- "{{ sigvild_gallery_home }}"
- "{{ sigvild_gallery_data_dir }}"
- "{{ sigvild_gallery_web_root }}"
- name: Build and deploy backend
include_tasks: deploy_backend.yml
tags: [backend, build]
- name: Build and deploy frontend
include_tasks: deploy_frontend.yml
tags: [frontend, build]
- name: Deploy systemd service
template:
src: sigvild-gallery.service.j2
dest: /etc/systemd/system/sigvild-gallery.service
owner: root
group: root
mode: '0644'
notify:
- reload systemd
- restart sigvild-gallery
tags: [backend, service]
- name: Deploy Caddy configurations
template:
src: "{{ item.src }}"
dest: "{{ caddy_sites_enabled_dir }}/{{ item.dest }}"
owner: root
group: "{{ caddy_user }}"
mode: '0644'
loop:
- { src: 'frontend.caddy.j2', dest: 'sigvild-frontend.caddy' }
- { src: 'api.caddy.j2', dest: 'sigvild-api.caddy' }
notify: reload caddy
tags: [caddy, frontend, backend]
- name: Enable and start sigvild-gallery service
systemd:
name: sigvild-gallery
enabled: "{{ sigvild_gallery_service_enabled }}"
state: "{{ sigvild_gallery_service_state }}"
daemon_reload: yes
tags: [backend, service]
- name: Create superuser account
command: >
{{ sigvild_gallery_binary }} superuser upsert
"{{ vault_pb_su_email }}"
"{{ vault_pb_su_password }}"
args:
chdir: "{{ sigvild_gallery_home }}"
become: yes
become_user: "{{ sigvild_gallery_user }}"
register: superuser_result
failed_when: superuser_result.rc != 0
- name: Verify gallery health
uri:
url: "https://{{ sigvild_gallery_api_domain }}/api/health"
method: GET
status_code: [200, 404] # 404 is ok if health endpoint doesn't exist yet
timeout: 15
retries: 5
delay: 5
ignore_errors: yes
tags: [verify]

View File

@@ -0,0 +1,45 @@
{{ sigvild_gallery_api_domain }} {
reverse_proxy {{ sigvild_gallery_host }}:{{ sigvild_gallery_port }} {
header_up Host {upstream_hostport}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto https
# Health check
health_uri /api/health
health_timeout 5s
health_interval 30s
}
# CORS headers for frontend domain
@cors {
header Origin https://{{ sigvild_gallery_frontend_domain }}
}
header @cors {
Access-Control-Allow-Origin "https://{{ sigvild_gallery_frontend_domain }}"
Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS"
Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"
Access-Control-Allow-Credentials true
Access-Control-Max-Age 86400
}
# Handle preflight requests
@preflight {
method OPTIONS
}
respond @preflight 204
# Security headers for API
header {
X-Frame-Options DENY
X-Content-Type-Options nosniff
X-XSS-Protection "1; mode=block"
Referrer-Policy strict-origin-when-cross-origin
}
# API logging
log {
output file /var/log/caddy/sigvild-api.log
level INFO
format json
}
}

View File

@@ -0,0 +1,42 @@
{{ sigvild_gallery_frontend_domain }} {
root * {{ sigvild_gallery_web_root }}
file_server
# SPA routing - serve index.html for all routes
try_files {path} /index.html
# Security headers
header {
X-Frame-Options DENY
X-Content-Type-Options nosniff
X-XSS-Protection "1; mode=block"
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy "geolocation=(), microphone=(), camera=()"
}
# Cache static assets aggressively
@static {
path /_app/* /assets/* /icons/* *.ico *.png *.jpg *.jpeg *.svg *.webp *.woff *.woff2
}
header @static {
Cache-Control "public, max-age=31536000, immutable"
Vary "Accept-Encoding"
}
# Cache HTML with shorter duration
@html {
path *.html /
}
header @html {
Cache-Control "public, max-age=3600, must-revalidate"
}
# Enable compression
encode gzip
# Logging for debugging (can be removed in production)
log {
output file /var/log/caddy/sigvild-frontend.log
level INFO
}
}

View File

@@ -0,0 +1,36 @@
[Unit]
Description=Sigvild Wedding Gallery API
After=network.target
[Service]
Type=simple
User={{ sigvild_gallery_user }}
Group={{ sigvild_gallery_user }}
WorkingDirectory={{ sigvild_gallery_home }}
ExecStart={{ sigvild_gallery_binary }} serve --http={{ sigvild_gallery_host }}:{{ sigvild_gallery_port }}
# Environment variables
Environment="SIGVILD_ENVIRONMENT"="production" # Lets caddy handle CORS
Environment="HOST_USERNAME={{ sigvild_gallery_host_username }}"
Environment="HOST_PASSWORD={{ sigvild_gallery_host_password }}"
Environment="HOST_DISPLAY_NAME=Wedding Host"
Environment="GUEST_USERNAME={{ sigvild_gallery_guest_username }}"
Environment="GUEST_PASSWORD={{ sigvild_gallery_guest_password }}"
Environment="GUEST_DISPLAY_NAME=Wedding Guest"
# Restart configuration
Restart=always
RestartSec=3
# Security sandboxing
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths={{ sigvild_gallery_data_dir }}
# Allow binding to port (if needed)
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target