From 49fc8b4d9c5049532c749adaee8114e303d9573c Mon Sep 17 00:00:00 2001 From: mifi Date: Fri, 30 Jan 2026 17:39:41 -0300 Subject: [PATCH] Pipeline stuff --- .woodpecker.yml | 122 ++++----------- DEPLOYMENT.md | 366 ++++++++++++++------------------------------- docker-compose.yml | 23 +++ package.json | 1 + 4 files changed, 160 insertions(+), 352 deletions(-) create mode 100644 docker-compose.yml diff --git a/.woodpecker.yml b/.woodpecker.yml index 2373ada..e783fd4 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -60,111 +60,41 @@ steps: - build # ============================================ - # Stage 3: Deploy to Linode VPS + # Stage 3: Trigger Portainer stack redeploy (webhook) # ============================================ - name: deploy - image: appleboy/drone-ssh - settings: - host: - from_secret: deploy_host - username: - from_secret: deploy_username - key: - from_secret: deploy_ssh_key - port: - from_secret: deploy_port - command_timeout: 5m - envs: - - REGISTRY_URL - - REGISTRY_REPO - - REGISTRY_USERNAME - - REGISTRY_PASSWORD - - CONTAINER_NAME - - APP_PORT - script: - - set -e - - set -o pipefail - - echo "=== Deploying to Linode VPS ===" - - echo "Container: $CONTAINER_NAME" - - echo "Port: $APP_PORT" - - | - # Login to registry - echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" \ - -u "$REGISTRY_USERNAME" \ - --password-stdin - - | - # Pull latest image - echo "Pulling image: $REGISTRY_REPO:latest" - docker pull $REGISTRY_REPO:latest - - | - # Stop and remove existing container - echo "Stopping existing container..." - docker stop $CONTAINER_NAME 2>/dev/null || echo "Container not running" - docker rm $CONTAINER_NAME 2>/dev/null || echo "Container not found" - - | - # Start new container - echo "Starting new container..." - docker run -d \ - --name $CONTAINER_NAME \ - --restart unless-stopped \ - -p $APP_PORT:80 \ - --label "deployment.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ - --label "deployment.commit=${CI_COMMIT_SHA}" \ - $REGISTRY_REPO:latest - - | - # Wait for container to be healthy - echo "Waiting for container health check..." - sleep 5 - if ! docker ps | grep -q $CONTAINER_NAME; then - echo "❌ Container failed to start!" - docker logs $CONTAINER_NAME - exit 1 - fi - - | - # Verify container is responding - echo "Verifying container health..." - if ! docker exec $CONTAINER_NAME wget -q --spider http://localhost/; then - echo "❌ Container health check failed!" - docker logs $CONTAINER_NAME - exit 1 - fi - - | - # Cleanup old images - echo "Cleaning up old images..." - docker image prune -af --filter "until=72h" || true - - echo "✓ Deployment complete!" + image: curlimages/curl:latest environment: - REGISTRY_URL: ${REGISTRY_URL} - REGISTRY_REPO: ${REGISTRY_REPO} - REGISTRY_USERNAME: ${REGISTRY_USERNAME} - REGISTRY_PASSWORD: - from_secret: registry_password - CONTAINER_NAME: ${CONTAINER_NAME} - APP_PORT: ${APP_PORT} + PORTAINER_WEBHOOK_URL: + from_secret: portainer_webhook_url + commands: + - set -e + - echo "=== Triggering Portainer stack redeploy ===" + - | + resp=$(curl -s -w "\n%{http_code}" -X POST "$PORTAINER_WEBHOOK_URL") + body=$(echo "$resp" | head -n -1) + code=$(echo "$resp" | tail -n 1) + if [ "$code" != "200" ] && [ "$code" != "204" ]; then + echo "Webhook failed (HTTP $code): $body" + exit 1 + fi + echo "✓ Portainer redeploy triggered (HTTP $code)" depends_on: - push # ============================================ # Configuration Reference # ============================================ -# +# # Required Secrets (set in Woodpecker UI): -# - registry_password: Docker registry password/token -# - deploy_host: Linode VPS hostname or IP -# - deploy_username: SSH username (e.g., root, deploy) -# - deploy_ssh_key: Private SSH key (multi-line) -# - deploy_port: SSH port (default: 22) +# - registry_password: Gitea container registry password/token +# - portainer_webhook_url: Portainer stack webhook URL (Redeploy trigger) # -# Required Environment Variables: -# - REGISTRY_URL: Docker registry URL (e.g., registry.example.com) -# - REGISTRY_REPO: Full image path (e.g., registry.example.com/mifi-ventures-landing) -# - REGISTRY_USERNAME: Registry username -# - CONTAINER_NAME: Docker container name (e.g., mifi-ventures-landing) -# - APP_PORT: Host port to expose (e.g., 8080) +# Required Environment Variables (Gitea registry): +# - REGISTRY_URL: git.mifi.dev +# - REGISTRY_REPO: git.mifi.dev/mifi-ventures/landing +# - REGISTRY_USERNAME: your Gitea username # -# Example .env for local testing: -# REGISTRY_URL=registry.example.com -# REGISTRY_REPO=registry.example.com/mifi-ventures-landing -# REGISTRY_USERNAME=myuser -# CONTAINER_NAME=mifi-ventures-landing -# APP_PORT=8080 +# Portainer: Add stack from "Git repository" with this repo, compose path +# docker-compose.yml. Enable GitOps → Webhook and "Re-pull image". +# Add Gitea registry in Portainer (Settings → Registries) so the host can pull. diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index dddade6..0bf8c56 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,125 +1,86 @@ # Deployment Guide -Quick reference for setting up Woodpecker CI/CD deployment. +Woodpecker builds the site, pushes the image to **Gitea’s container registry**, then triggers a **Portainer stack redeploy** via webhook. The stack on your Linode VPS pulls the new image and recreates the container. + +## Portainer stack options + +You can run the stack in Portainer in two ways; both use the **pre-built image** (Option A). + +| Option | Description | When to use | +|--------|-------------|-------------| +| **Git repository** | Portainer pulls the stack definition from this repo (`docker-compose.yml`). Compose path: `docker-compose.yml`. On webhook: Portainer pulls latest compose + image and redeploys. | Stack definition (ports, env) lives in git; one repo for app + stack. | +| **Web editor** | You paste the compose YAML in Portainer. No compose file in the repo. On webhook: Portainer pulls the image and redeploys. | You prefer to manage stack only in Portainer and keep the repo app-only. | + +This repo includes `docker-compose.yml` for the **Repository** option. The compose file only references the image (`git.mifi.dev/mifi-ventures/landing:latest`); it does not build. In Portainer, enable **Re-pull image** so each webhook pulls the new `:latest` and recreates the stack. ## Prerequisites -- Gitea instance with Woodpecker CI configured -- Private Docker registry (Docker Hub, GitHub Container Registry, Harbor, etc.) -- Linode VPS with Docker installed -- SSH access to VPS +- Gitea with Woodpecker CI and **container registry** enabled +- Linode VPS with Docker; Portainer installed and managing that environment +- Portainer stack created (Repository pointing at this repo, or Web editor with equivalent compose) ## Step-by-Step Setup -### 1. Prepare VPS +### 1. Prepare VPS and Portainer -SSH into your Linode VPS and install Docker: +- Install Docker on the Linode VPS and run Portainer (e.g. as a container or on the host). +- In Portainer, add your **Gitea registry**: **Settings → Registries → Add registry**. Use the same URL and credentials you use for `REGISTRY_*` in Woodpecker so the host can pull `git.mifi.dev/mifi-ventures/landing:latest`. -```bash -# Update system -sudo apt update && sudo apt upgrade -y +### 2. Create the Portainer stack -# Install Docker -curl -fsSL https://get.docker.com -o get-docker.sh -sudo sh get-docker.sh +**If using Git repository (recommended):** -# Create deploy user (optional, recommended) -sudo useradd -m -s /bin/bash deploy -sudo usermod -aG docker deploy +1. **Stacks → Add stack** → name (e.g. `landing`). +2. Choose **Git repository**. +3. **Repository URL**: `https://git.mifi.dev/mifi-ventures/landing.git` (or your clone URL). Add credentials if the repo is private. +4. **Repository reference**: `main`. +5. **Compose path**: `docker-compose.yml`. +6. Enable **GitOps updates** → **Webhook**. Copy the webhook URL for the Woodpecker secret `portainer_webhook_url`. +7. Enable **Re-pull image** so each webhook pulls the new image. +8. Optionally set stack env vars: `LANDING_PORT=8080` (or leave default in compose). +9. **Deploy the stack**. -# Verify Docker works -docker --version -docker ps -``` +**If using Web editor:** -### 2. Generate SSH Key for Deployment +1. **Stacks → Add stack** → name (e.g. `landing`). +2. Choose **Web editor** and paste the same structure as `docker-compose.yml` (service with `image: git.mifi.dev/mifi-ventures/landing:latest`, `pull_policy: always`, `ports`, `restart`). +2. Enable the stack **webhook**, copy the URL for `portainer_webhook_url`. +3. **Deploy the stack**. -On your local machine or CI server: +### 3. Gitea container registry -```bash -# Generate SSH key pair -ssh-keygen -t ed25519 -C "woodpecker-deploy" -f ~/.ssh/woodpecker_deploy +Use Gitea’s built-in container registry. Image path: -# Copy public key to VPS -ssh-copy-id -i ~/.ssh/woodpecker_deploy.pub deploy@your-vps-ip - -# Test connection -ssh -i ~/.ssh/woodpecker_deploy deploy@your-vps-ip "echo 'Connection successful'" - -# Get private key content (copy this to Woodpecker secret) -cat ~/.ssh/woodpecker_deploy -``` - -### 3. Configure Docker Registry - -**Option A: Docker Hub** -```bash -REGISTRY_URL=docker.io -REGISTRY_REPO=docker.io/yourusername/mifi-ventures-landing -REGISTRY_USERNAME=yourusername -# Password: Use Docker Hub access token (not your account password) -# Generate at: https://hub.docker.com/settings/security -``` - -**Option B: GitHub Container Registry** -```bash -REGISTRY_URL=ghcr.io -REGISTRY_REPO=ghcr.io/yourusername/mifi-ventures-landing -REGISTRY_USERNAME=yourusername -# Password: GitHub Personal Access Token with write:packages scope -# Generate at: https://github.com/settings/tokens -``` - -**Option C: Self-hosted Registry** -```bash -REGISTRY_URL=registry.example.com -REGISTRY_REPO=registry.example.com/mifi-ventures-landing -REGISTRY_USERNAME=yourusername -# Password: Your registry password or token -``` +- **Registry URL**: `git.mifi.dev` +- **Image (REGISTRY_REPO)**: `git.mifi.dev/mifi-ventures/landing` +- Create a **Gitea token** or use your password with **package:write** (or equivalent) for the `mifi-ventures/landing` package. Use that as `registry_password` in Woodpecker. ### 4. Configure Woodpecker Secrets -Navigate to: `Gitea → Repository → Settings → Secrets` +In **Gitea → Repository → Settings → Secrets** (Woodpecker): -Add these secrets (click "Add secret" for each): - -| Name | Value | Notes | -|------|-------|-------| -| `registry_password` | Your registry password/token | From step 3 | -| `deploy_host` | `123.45.67.89` | Your Linode VPS IP | -| `deploy_username` | `deploy` | SSH user from step 1 | -| `deploy_ssh_key` | `-----BEGIN OPENSSH...` | Private key from step 2 | -| `deploy_port` | `22` | Default SSH port | - -**Important**: For `deploy_ssh_key`, paste the entire private key including: -``` ------BEGIN OPENSSH PRIVATE KEY----- -...entire key content... ------END OPENSSH PRIVATE KEY----- -``` +| Name | Value | +|------|-------| +| `registry_password` | Gitea token or password (package write to the repo’s registry) | +| `portainer_webhook_url` | Portainer stack webhook URL (from step 2) | ### 5. Configure Woodpecker Variables -Navigate to: `Gitea → Repository → Settings → Variables` +In **Gitea → Repository → Settings → Variables** (Woodpecker): -Add these variables: - -| Name | Value | Example | -|------|-------|---------| -| `REGISTRY_URL` | Registry base URL | `ghcr.io` | -| `REGISTRY_REPO` | Full image path | `ghcr.io/username/mifi-ventures-landing` | -| `REGISTRY_USERNAME` | Registry username | `yourusername` | -| `CONTAINER_NAME` | Container name | `mifi-ventures-landing` | -| `APP_PORT` | Host port | `8080` | +| Name | Value | +|------|-------| +| `REGISTRY_URL` | `git.mifi.dev` | +| `REGISTRY_REPO` | `git.mifi.dev/mifi-ventures/landing` | +| `REGISTRY_USERNAME` | Your Gitea username | ### 6. Test Deployment #### Local Test Build ```bash # Clone repo -git clone https://gitea.example.com/you/mifi-ventures-landing.git -cd mifi-ventures-landing +git clone https://git.mifi.dev/mifi-ventures/landing.git +cd landing # Build locally docker build -t test . @@ -144,212 +105,105 @@ git add README.md git commit -m "test: trigger deployment" git push origin main -# Watch pipeline in Woodpecker UI -# Check: Gitea → Repository → Pipelines +# Watch pipeline in Woodpecker UI (Gitea → Repository → Pipelines). +# After build + push, deploy step calls Portainer webhook; Portainer redeploys the stack. ``` ### 7. Verify Deployment -After pipeline completes: +After the pipeline completes: -```bash -# Check container is running -ssh deploy@your-vps-ip "docker ps | grep mifi-ventures-landing" - -# Check container logs -ssh deploy@your-vps-ip "docker logs mifi-ventures-landing" - -# Test HTTP response -curl http://your-vps-ip:8080 - -# Check from browser -# Visit: http://your-vps-ip:8080 -``` +- In **Portainer**, open the stack and confirm the service is running and the image was pulled. +- From your machine: `curl http://your-vps-ip:8080` (or the port you set in the stack). +- In a browser: visit `http://your-vps-ip:8080` (or your domain if you use a reverse proxy). ## Port Configuration -The pipeline uses `APP_PORT` to expose the container. Choose based on your setup: +The stack compose uses `LANDING_PORT` (default `8080`). Set it in Portainer stack env vars or in `docker-compose.yml`: -### Option 1: Direct Port 80 (No Reverse Proxy) -```bash -APP_PORT=80 -# Container listens on port 80 directly -# Access at: http://your-vps-ip -``` - -### Option 2: Custom Port (With Traefik/Nginx) -```bash -APP_PORT=8080 -# Container listens on 8080 -# Traefik/Nginx proxies from 80/443 → 8080 -# Access via domain: https://mifi.ventures -``` +- **Direct port 80**: set `LANDING_PORT=80` in the stack. +- **Custom port (e.g. behind Traefik)**: set `LANDING_PORT=8080` and proxy 80/443 → 8080. **Note**: With Traefik, security headers are added at the proxy level. ## Traefik Integration Example -If using Traefik as reverse proxy, add labels to container: +If using Traefik as reverse proxy, add labels to the service in `docker-compose.yml`: ```yaml -# In docker-compose.yml or docker run command -labels: - - "traefik.enable=true" - - "traefik.http.routers.mifi.rule=Host(`mifi.ventures`)" - - "traefik.http.routers.mifi.entrypoints=websecure" - - "traefik.http.routers.mifi.tls.certresolver=letsencrypt" - - "traefik.http.services.mifi.loadbalancer.server.port=80" +services: + landing: + image: ${LANDING_IMAGE:-git.mifi.dev/mifi-ventures/landing:latest} + pull_policy: always + restart: unless-stopped + labels: + - "traefik.enable=true" + - "traefik.http.routers.mifi.rule=Host(`mifi.ventures`)" + - "traefik.http.routers.mifi.entrypoints=websecure" + - "traefik.http.routers.mifi.tls.certresolver=letsencrypt" + - "traefik.http.services.mifi.loadbalancer.server.port=80" + networks: + - traefik-public + +networks: + traefik-public: + external: true ``` -Update Woodpecker deploy script to include labels: -```bash -docker run -d \ - --name $CONTAINER_NAME \ - --restart unless-stopped \ - --label "traefik.enable=true" \ - --label "traefik.http.routers.mifi.rule=Host(\`mifi.ventures\`)" \ - --label "traefik.http.routers.mifi.entrypoints=websecure" \ - --label "traefik.http.routers.mifi.tls.certresolver=letsencrypt" \ - --label "traefik.http.services.mifi.loadbalancer.server.port=80" \ - --network traefik-public \ - $REGISTRY_REPO:latest -``` +(Remove or omit `ports` if Traefik is the only entry point.) ## Common Issues -### SSH Permission Denied -```bash -# Check key permissions (must be 600) -chmod 600 ~/.ssh/woodpecker_deploy +### Registry Login Failed (Woodpecker push) +- Ensure `REGISTRY_URL` is `git.mifi.dev` and `REGISTRY_REPO` is `git.mifi.dev/mifi-ventures/landing`. +- Use a Gitea token with package write permission (or your password if allowed). +- Test locally: `echo "TOKEN" | docker login git.mifi.dev -u USERNAME --password-stdin`. -# Verify public key is on server -ssh deploy@host "cat ~/.ssh/authorized_keys" +### Portainer Cannot Pull Image +- In Portainer, add the Gitea registry under **Settings → Registries** with the same URL and credentials. +- Ensure the stack’s **Re-pull image** (or equivalent) is enabled so redeploys pull the new image. -# Test with verbose output -ssh -vvv -i ~/.ssh/woodpecker_deploy deploy@host -``` +### Webhook Returns 4xx/5xx +- Confirm the webhook URL is correct and the stack exists. +- In Portainer, open the stack → Webhook and verify the URL matches the secret `portainer_webhook_url`. -### Registry Login Failed -```bash -# Test login locally -echo "PASSWORD" | docker login registry.example.com -u username --password-stdin - -# For GitHub: Ensure token has write:packages scope -# For Docker Hub: Use access token, not password -``` - -### Container Won't Start -```bash -# Check logs -ssh deploy@host "docker logs mifi-ventures-landing" - -# Check for port conflicts -ssh deploy@host "netstat -tulpn | grep :80" - -# Verify image pulled successfully -ssh deploy@host "docker images | grep mifi-ventures" -``` - -### Health Check Fails -```bash -# Check nginx is running -ssh deploy@host "docker exec mifi-ventures-landing ps aux" - -# Test internally -ssh deploy@host "docker exec mifi-ventures-landing wget -O- http://localhost/" - -# Check if firewall blocking -ssh deploy@host "ufw status" -``` +### Container Won’t Start (Portainer) +- In Portainer, open the stack → service → **Logs**. +- Check for port conflicts on the host (e.g. another service using the same port). +- Ensure the Gitea registry is added in Portainer so the image can be pulled. ## Rollback Procedure -If deployment fails: +If a bad image was deployed: -```bash -# SSH to VPS -ssh deploy@your-vps-ip - -# List available images -docker images | grep mifi-ventures - -# Find previous working SHA -# (from Gitea commit history or Docker labels) - -# Stop current container -docker stop mifi-ventures-landing -docker rm mifi-ventures-landing - -# Start previous version -docker run -d \ - --name mifi-ventures-landing \ - --restart unless-stopped \ - -p 8080:80 \ - registry.example.com/mifi-ventures-landing:PREVIOUS_SHA - -# Verify -docker ps | grep mifi-ventures-landing -curl http://localhost:8080 -``` +1. **Portainer**: Open the stack → **Redeploy** with “Re-pull image” off, or edit the stack to use a specific image tag (e.g. `git.mifi.dev/mifi-ventures/landing:`) if you tag by SHA in Woodpecker. +2. **Or** in Gitea, revert the commit and push to `main`; the pipeline will build and push a new image, then the webhook will redeploy. Ensure the reverted commit builds successfully. ## Monitoring -### Container Status -```bash -# Check if running -docker ps | grep mifi-ventures-landing - -# View logs (last 100 lines) -docker logs --tail 100 mifi-ventures-landing - -# Follow logs in real-time -docker logs -f mifi-ventures-landing - -# Check resource usage -docker stats mifi-ventures-landing -``` - -### Nginx Access Logs -```bash -# View access logs -docker exec mifi-ventures-landing tail -f /var/log/nginx/access.log - -# View error logs -docker exec mifi-ventures-landing tail -f /var/log/nginx/error.log -``` - -### Disk Space -```bash -# Check Docker disk usage -docker system df - -# Cleanup old images (automatic in pipeline, or manual) -docker image prune -af --filter "until=72h" - -# Full cleanup (careful!) -docker system prune -a --volumes -``` +- **Portainer**: Stack → service → **Logs**, **Inspect**, **Stats**. +- **On the host** (if you have SSH): `docker ps`, `docker logs `, `docker stats `. +- **Nginx logs** (from host): `docker exec tail -f /var/log/nginx/access.log` (and `error.log`). +- **Disk**: Portainer → **Host** → disk usage; or on host: `docker system df`, `docker image prune -af --filter "until=72h"`. ## Security Checklist -- [ ] SSH key is Ed25519 (not RSA) -- [ ] Private key is stored securely in Woodpecker (never in repo) -- [ ] Deploy user has minimal permissions (not root if possible) -- [ ] Registry uses authentication (not public) +- [ ] Registry (Gitea) uses authentication; token stored only in Woodpecker secrets +- [ ] Portainer webhook URL stored only in Woodpecker secrets (not in repo) - [ ] VPS firewall configured (ufw or iptables) -- [ ] Traefik handles TLS termination (if used) -- [ ] Container runs as non-root user (nginx user) -- [ ] Regular security updates (`apt update && apt upgrade`) +- [ ] Traefik (or reverse proxy) handles TLS termination if used +- [ ] Container runs as non-root user (nginx user in the image) +- [ ] Regular security updates on the host (`apt update && apt upgrade`) ## Next Steps 1. Set up monitoring (Prometheus + Grafana) 2. Configure automated backups -3. Add Slack/Discord notifications to pipeline -4. Set up staging environment -5. Implement blue-green deployments for zero downtime +3. Add Slack/Discord notifications to the Woodpecker pipeline +4. Set up a staging stack in Portainer (e.g. different port or branch) +5. Optionally tag images by commit SHA in Woodpecker for easier rollback in Portainer --- -**Last Updated**: 2026-01-29 +**Last Updated**: 2026-01-30 **Maintainer**: Mike Fitzpatrick diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..64205ea --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +# Portainer stack: deploy pre-built image from Gitea container registry. +# Used with stack type "Git repository" — compose path: docker-compose.yml +# Image is built and pushed by Woodpecker; enable "Re-pull image" in Portainer +# so webhook redeploys pull the new :latest and recreate the stack. + +services: + landing: + image: ${LANDING_IMAGE:-git.mifi.dev/mifi-ventures/landing:latest} + container_name: mifi-ventures-landing + pull_policy: always + restart: unless-stopped + ports: + - "${LANDING_PORT:-8080}:80" + labels: + - "traefik.enable=true" + - "traefik.docker.network=marina-net" + - "traefik.http.routers.mifiventures.rule=Host(`mifi.ventures`)" + - "traefik.http.routers.mifiventures.entrypoints=websecure" + - "traefik.http.routers.mifiventures.middlewares=secure-all@file,redirect-www-to-non-www@file" + - "traefik.http.routers.mifiventures.tls=true" + - "traefik.http.routers.mifiventures.tls.certresolver=letsencrypt" + marina-net: + external: true diff --git a/package.json b/package.json index 3a115b8..b441677 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "mifi-ventures-landing", "version": "1.0.0", "private": true, + "repository": "https://git.mifi.dev/mifi-ventures/landing.git", "packageManager": "pnpm@9.15.0", "description": "mifi Ventures landing site — static build with critical CSS inlining", "scripts": {