Pipeline stuff

This commit is contained in:
2026-01-30 17:39:41 -03:00
parent 1519dfa460
commit 49fc8b4d9c
4 changed files with 160 additions and 352 deletions

View File

@@ -1,125 +1,86 @@
# Deployment Guide
Quick reference for setting up Woodpecker CI/CD deployment.
Woodpecker builds the site, pushes the image to **Giteas 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 Giteas 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 repos 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 stacks **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 Wont 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:<commit-sha>`) 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 <container>`, `docker stats <container>`.
- **Nginx logs** (from host): `docker exec <container> 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