diff --git a/docs/jnss-web-deployment.md b/docs/jnss-web-deployment.md new file mode 100644 index 0000000..87e624d --- /dev/null +++ b/docs/jnss-web-deployment.md @@ -0,0 +1,326 @@ +# jnss-web Static Site Deployment Guide + +## Overview + +This document describes the deployment process for jnss-web, a SvelteKit-based static website serving as the primary entrypoint for jnss.me. + +## Architecture + +- **Technology**: SvelteKit v2 with static adapter +- **Build Tool**: Bun +- **Deployment**: Ansible playbook with git-based artifact deployment +- **Web Server**: Caddy (direct file serving) +- **Repository**: git.jnss.me/joakim/jnss-web +- **Branch Strategy**: + - `main` - Source code + - `deploy` - Build artifacts only + +## Server Architecture + +``` +/opt/jnss-web-repo/ # Git clone of deploy branch +/var/www/jnss-web/ # Synced build artifacts (served by Caddy) +/etc/caddy/sites-enabled/ + └── jnss-web.caddy # Site config with www redirect +/var/log/caddy/jnss-web.log # Access logs +``` + +## Development Workflow + +### 1. Local Development (main branch) + +```bash +cd ~/dev/jnss-web +git checkout main + +# Make changes to source code +# ... + +# Test locally +bun run dev + +# Build to verify +bun run build +``` + +### 2. Prepare Deploy Branch + +```bash +# Build production version +bun run build + +# Switch to deploy branch +git checkout deploy + +# Merge changes from main +git merge main --no-commit + +# Rebuild to ensure fresh build +bun run build + +# Add only build artifacts +git add build/ + +# Commit with descriptive message +git commit -m "Deploy: Add new feature X" + +# Push to Gitea +git push origin deploy +``` + +### 3. Deploy to Server + +```bash +cd ~/rick-infra + +# Run deployment playbook +ansible-playbook -i inventory/hosts.yml playbooks/deploy-jnss-web.yml + +# Watch logs (optional) +ssh root@arch-vps 'tail -f /var/log/caddy/jnss-web.log' +``` + +## Playbook Details + +### Variables + +Located in `playbooks/deploy-jnss-web.yml`: + +- `jnss_web_repo_owner`: Your Gitea username (default: "joakim") +- `jnss_web_branch`: Branch to deploy (default: "deploy") +- `jnss_web_domain`: Primary domain (default: "jnss.me") + +### Tags + +- `jnss-web` - All jnss-web tasks +- `deploy` - Git clone/sync tasks +- `caddy` - Caddy configuration tasks + +### Example Usage + +```bash +# Full deployment +ansible-playbook -i inventory/hosts.yml playbooks/deploy-jnss-web.yml + +# Only update Caddy config (no git pull) +ansible-playbook -i inventory/hosts.yml playbooks/deploy-jnss-web.yml --tags caddy + +# Only sync files (skip Caddy reload) +ansible-playbook -i inventory/hosts.yml playbooks/deploy-jnss-web.yml --tags deploy +``` + +## Initial Setup + +### jnss-web Project Configuration + +The following changes need to be made in the jnss-web project: + +#### 1. Install Static Adapter + +```bash +cd ~/dev/jnss-web +bun add -D @sveltejs/adapter-static +``` + +#### 2. Update svelte.config.js + +```javascript +import adapter from '@sveltejs/adapter-static'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter({ + pages: 'build', + assets: 'build', + fallback: undefined, + precompress: false, + strict: true + }) + } +}; + +export default config; +``` + +#### 3. Create src/routes/+layout.js + +```javascript +export const prerender = true; +``` + +#### 4. Update .gitignore + +Comment out or remove the build directory from .gitignore: + +```gitignore +# build/ <- Comment this out or remove it +``` + +#### 5. Create Deploy Branch + +```bash +# Build the site first +bun run build + +# Create and switch to deploy branch +git checkout -b deploy + +# Remove source files (keep only build artifacts) +git rm -r src static *.config.js package.json bun.lock jsconfig.json .npmrc + +# Keep only build/ and documentation +git add build/ README.md + +# Commit +git commit -m "Initial deploy branch with build artifacts" + +# Push to Gitea +git push -u origin deploy + +# Switch back to main +git checkout main +``` + +## Troubleshooting + +### Build Directory Not Found + +**Error**: "Build directory not found in repository" + +**Solution**: Ensure you pushed the build artifacts to the deploy branch: +```bash +git checkout deploy +ls -la build/ # Should exist +git log --oneline -1 # Verify latest commit +``` + +### Caddy Not Serving New Content + +**Solution**: +```bash +# SSH to server +ssh root@arch-vps + +# Check Caddy status +systemctl status caddy + +# Reload Caddy manually +systemctl reload caddy + +# Check logs +journalctl -u caddy -f +``` + +### Permission Issues + +**Solution**: +```bash +# SSH to server +ssh root@arch-vps + +# Fix ownership +chown -R caddy:caddy /var/www/jnss-web + +# Verify +ls -la /var/www/jnss-web +``` + +### Git Clone Fails + +**Error**: Unable to clone from git.jnss.me + +**Solution**: +- Verify repository is public in Gitea +- Test clone manually: `git clone https://git.jnss.me/joakim/jnss-web.git /tmp/test` +- Check Gitea service status + +## Rollback Procedure + +### Option 1: Rollback Deploy Branch + +```bash +# In jnss-web repository +git checkout deploy + +# Find previous commit +git log --oneline + +# Reset to previous commit +git reset --hard + +# Force push +git push -f origin deploy + +# Re-run deployment +cd ~/rick-infra +ansible-playbook -i inventory/hosts.yml playbooks/deploy-jnss-web.yml +``` + +### Option 2: Quick Server-Side Rollback + +```bash +# SSH to server +ssh root@arch-vps + +# Go to repo directory +cd /opt/jnss-web-repo + +# Reset to previous commit +git reset --hard HEAD~1 + +# Re-sync +rsync -av --delete build/ /var/www/jnss-web/ + +# Reload Caddy +systemctl reload caddy +``` + +## Security Considerations + +- Repository is public (contains only build artifacts) +- No secrets in build output +- Caddy serves only /var/www/jnss-web (no parent directory access) +- Security headers configured in Caddy +- HTTPS enforced via Caddy with Let's Encrypt + +## Performance Optimizations + +- Static assets cached for 1 year (immutable) +- HTML cached for 1 hour with revalidation +- Gzip compression enabled +- No server-side processing required + +## Monitoring + +### Check Deployment Status + +```bash +# From control machine +ansible homelab -i inventory/hosts.yml -m shell -a "ls -lh /var/www/jnss-web" +``` + +### View Access Logs + +```bash +ssh root@arch-vps 'tail -100 /var/log/caddy/jnss-web.log | jq .' +``` + +### Check Site Health + +```bash +curl -I https://jnss.me +curl -I https://www.jnss.me # Should redirect to jnss.me +``` + +## Files Created + +This deployment adds the following files to rick-infra: + +- `playbooks/deploy-jnss-web.yml` - Main deployment playbook +- `playbooks/templates/jnss-web.caddy.j2` - Caddy configuration template +- `docs/jnss-web-deployment.md` - This documentation + +And modifies: + +- `roles/caddy/templates/Caddyfile.j2` - Removed default site section diff --git a/playbooks/deploy-jnss-web.yml b/playbooks/deploy-jnss-web.yml new file mode 100644 index 0000000..28ed06f --- /dev/null +++ b/playbooks/deploy-jnss-web.yml @@ -0,0 +1,133 @@ +--- +# ================================================================ +# jnss-web Static Site Deployment Playbook +# ================================================================ +# Deploys the jnss-web SvelteKit static site to jnss.me +# +# Usage: +# ansible-playbook -i inventory/hosts.yml playbooks/deploy-jnss-web.yml +# +# This playbook: +# - Clones the jnss-web repository (deploy branch) to a temp directory +# - Syncs build artifacts to /var/www/jnss-web +# - Deploys Caddy configuration for jnss.me with www redirect +# - Reloads Caddy to serve the new site +# ================================================================ + +- name: Deploy jnss-web static site + hosts: homelab + become: true + + vars: + # Git repository configuration + jnss_web_repo_url: "https://git.jnss.me/joakim/jnss-web.git" + jnss_web_branch: "deploy" + + # Server paths + jnss_web_root: "/var/www/jnss-web" + + # Domain configuration + jnss_web_domain: "jnss.me" + + # Caddy configuration + caddy_user: "caddy" + caddy_sites_enabled_dir: "/etc/caddy/sites-enabled" + + tasks: + # ============================================================ + # Git Repository Management + # ============================================================ + + - name: Create temporary directory for git clone + tempfile: + state: directory + suffix: -jnss-web + register: temp_clone_dir + tags: [jnss-web, deploy] + + - name: Clone jnss-web repository to temp directory + git: + repo: "{{ jnss_web_repo_url }}" + dest: "{{ temp_clone_dir.path }}" + version: "{{ jnss_web_branch }}" + depth: 1 + tags: [jnss-web, deploy] + + - name: Verify build directory exists in repository + stat: + path: "{{ temp_clone_dir.path }}/index.html" + register: build_dir + tags: [jnss-web, deploy] + + - name: Fail if index.html not found + fail: + msg: "Build index.html not found in repository root. Ensure the deploy branch contains the built artifacts." + when: not build_dir.stat.exists + tags: [jnss-web, deploy] + + # ============================================================ + # Web Root Deployment + # ============================================================ + + - name: Remove old web root + file: + path: "{{ jnss_web_root }}" + state: absent + tags: [jnss-web, deploy] + + - name: Create fresh web root directory + file: + path: "{{ jnss_web_root }}" + state: directory + owner: "{{ caddy_user }}" + group: "{{ caddy_user }}" + mode: '0755' + tags: [jnss-web, deploy] + + - name: Copy build files to web root + copy: + src: "{{ temp_clone_dir.path }}/" + dest: "{{ jnss_web_root }}/" + owner: "{{ caddy_user }}" + group: "{{ caddy_user }}" + mode: '0755' + remote_src: true + tags: [jnss-web, deploy] + + - name: Clean up temporary clone directory + file: + path: "{{ temp_clone_dir.path }}" + state: absent + tags: [jnss-web, deploy] + + # ============================================================ + # Caddy Configuration + # ============================================================ + + - name: Deploy Caddy configuration for jnss-web + template: + src: templates/jnss-web.caddy.j2 + dest: "{{ caddy_sites_enabled_dir }}/jnss-web.caddy" + owner: root + group: "{{ caddy_user }}" + mode: '0644' + notify: reload caddy + tags: [jnss-web, caddy] + + - name: Validate Caddy configuration + command: caddy validate --config /etc/caddy/Caddyfile + register: caddy_validate + changed_when: false + tags: [jnss-web, caddy] + + - name: Display Caddy validation result + debug: + msg: "Caddy configuration is valid" + when: caddy_validate.rc == 0 + tags: [jnss-web, caddy] + + handlers: + - name: reload caddy + systemd: + name: caddy + state: reloaded diff --git a/playbooks/templates/jnss-web.caddy.j2 b/playbooks/templates/jnss-web.caddy.j2 new file mode 100644 index 0000000..88b5539 --- /dev/null +++ b/playbooks/templates/jnss-web.caddy.j2 @@ -0,0 +1,57 @@ +# jnss-web Static Site Configuration +# Generated by Ansible - DO NOT EDIT MANUALLY + +# WWW Redirect - apex is primary +www.{{ jnss_web_domain }} { + redir https://{{ jnss_web_domain }}{uri} permanent +} + +# Primary Domain +{{ jnss_web_domain }} { + root * {{ jnss_web_root }} + file_server + + # SPA routing - serve index.html for all routes + try_files {path} /index.html + + # Security headers + header { + X-Frame-Options SAMEORIGIN + 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 *.css *.js + } + 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 + log { + output file /var/log/caddy/jnss-web.log { + roll_size 100mb + roll_keep 5 + } + format json { + time_format "2006-01-02T15:04:05.000Z07:00" + } + level INFO + } +} diff --git a/rick-infra.yml b/rick-infra.yml index acca03a..18e96c8 100644 --- a/rick-infra.yml +++ b/rick-infra.yml @@ -21,6 +21,10 @@ gather_facts: true tasks: + - name: Deploy Caddy + include_role: + name: caddy + tags: ['caddy'] # - name: Deploy Authentik # include_role: # name: authentik @@ -36,8 +40,8 @@ # name: nextcloud # tags: ['nextcloud', 'cloud', 'storage'] - - name: Deploy Vaultwarden - include_role: - name: vaultwarden - tags: ['vaultwarden', 'vault', 'password-manager', 'security'] + # - name: Deploy Vaultwarden + # include_role: + # name: vaultwarden + # tags: ['vaultwarden', 'vault', 'password-manager', 'security'] diff --git a/roles/caddy/tasks/main.yml b/roles/caddy/tasks/main.yml index a7cbad1..bb643b0 100644 --- a/roles/caddy/tasks/main.yml +++ b/roles/caddy/tasks/main.yml @@ -120,7 +120,7 @@ owner: root group: "{{ caddy_user }}" mode: '0640' - backup: yes + backup: no notify: reload caddy - name: Check Caddyfile syntax (basic check) diff --git a/roles/caddy/templates/Caddyfile.j2 b/roles/caddy/templates/Caddyfile.j2 index 8162276..d86f4d8 100644 --- a/roles/caddy/templates/Caddyfile.j2 +++ b/roles/caddy/templates/Caddyfile.j2 @@ -21,43 +21,3 @@ # Import service configurations import {{ caddy_sites_enabled_dir }}/* - -# Primary domain: {{ caddy_domain }} -{{ caddy_domain }} { - {% if caddy_tls_enabled %} - {% if caddy_dns_provider == "cloudflare" and cloudflare_api_token %} - # DNS challenge for automatic TLS - tls { - dns cloudflare {{ cloudflare_api_token }} - resolvers {{ caddy_dns_resolvers | join(' ') }} - } - {% elif caddy_tls_email %} - # HTTP challenge for automatic TLS - tls {{ caddy_tls_email }} - {% endif %} - {% endif %} - - # Serve static content - root * {{ caddy_default_site_root }} - file_server - - # Logging - log { - {% if caddy_log_format == "json" %} - output file {{ caddy_log_dir }}/{{ caddy_domain | replace('.', '_') }}.log { - roll_size 100mb - roll_keep 5 - } - format json { - time_format "2006-01-02T15:04:05.000Z07:00" - } - level {{ caddy_log_level }} - {% else %} - output file {{ caddy_log_dir }}/{{ caddy_domain | replace('.', '_') }}.log { - roll_size 100mb - roll_keep 5 - } - level {{ caddy_log_level }} - {% endif %} - } -} diff --git a/roles/gitea/defaults/main.yml b/roles/gitea/defaults/main.yml index f002623..5d9c98e 100644 --- a/roles/gitea/defaults/main.yml +++ b/roles/gitea/defaults/main.yml @@ -63,7 +63,7 @@ gitea_enable_lfs: true # Access Control - Private server with public repos allowed gitea_disable_registration: true # No public registration (admin only) -gitea_require_signin: true # Require sign-in (unauthorized users read-only) +gitea_require_signin: false # Require sign-in (unauthorized users read-only) gitea_show_registration_button: false # Hide registration UI # OAuth Configuration - Preferred but not forced @@ -86,7 +86,7 @@ gitea_smtp_addr: "smtp.titan.email" gitea_smtp_port: 587 gitea_mailer_from: "hello@jnss.me" gitea_mailer_user: "hello@jnss.me" -gitea_mailer_password: "{{ vault_gitea_smtp_password }}" +gitea_mailer_password: "{{ vault_smtp_password }}" gitea_mailer_subject_prefix: "[Gitea]" # ================================================================= diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml index 4c5efb1..0346b3f 100644 --- a/roles/nextcloud/defaults/main.yml +++ b/roles/nextcloud/defaults/main.yml @@ -105,7 +105,7 @@ nextcloud_smtp_secure: "tls" # tls, ssl, or empty string for no encryption nextcloud_smtp_auth: true # Enable SMTP authentication nextcloud_smtp_authtype: "PLAIN" # LOGIN or PLAIN nextcloud_smtp_username: "hello@jnss.me" # SMTP username -nextcloud_smtp_password: "{{ vault_nextcloud_smtp_password | default('') }}" +nextcloud_smtp_password: "{{ vault_smtp_password | default('') }}" # Email Addressing nextcloud_mail_from_address: "hello" # Local part only (before @) diff --git a/roles/vaultwarden/defaults/main.yml b/roles/vaultwarden/defaults/main.yml index 3c76e53..7495d12 100644 --- a/roles/vaultwarden/defaults/main.yml +++ b/roles/vaultwarden/defaults/main.yml @@ -64,7 +64,7 @@ vaultwarden_smtp_host: "smtp.titan.email" vaultwarden_smtp_port: 587 vaultwarden_smtp_from: "hello@jnss.me" vaultwarden_smtp_username: "hello@jnss.me" -vaultwarden_smtp_password: "{{ vault_vaultwarden_smtp_password | default('') }}" +vaultwarden_smtp_password: "{{ vault_smtp_password | default('') }}" vaultwarden_smtp_security: "starttls" # Options: starttls, force_tls, off # =================================================================