Pipeline stuff
This commit is contained in:
120
.woodpecker.yml
120
.woodpecker.yml
@@ -60,87 +60,25 @@ steps:
|
|||||||
- build
|
- build
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Stage 3: Deploy to Linode VPS
|
# Stage 3: Trigger Portainer stack redeploy (webhook)
|
||||||
# ============================================
|
# ============================================
|
||||||
- name: deploy
|
- name: deploy
|
||||||
image: appleboy/drone-ssh
|
image: curlimages/curl:latest
|
||||||
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!"
|
|
||||||
environment:
|
environment:
|
||||||
REGISTRY_URL: ${REGISTRY_URL}
|
PORTAINER_WEBHOOK_URL:
|
||||||
REGISTRY_REPO: ${REGISTRY_REPO}
|
from_secret: portainer_webhook_url
|
||||||
REGISTRY_USERNAME: ${REGISTRY_USERNAME}
|
commands:
|
||||||
REGISTRY_PASSWORD:
|
- set -e
|
||||||
from_secret: registry_password
|
- echo "=== Triggering Portainer stack redeploy ==="
|
||||||
CONTAINER_NAME: ${CONTAINER_NAME}
|
- |
|
||||||
APP_PORT: ${APP_PORT}
|
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:
|
depends_on:
|
||||||
- push
|
- push
|
||||||
|
|
||||||
@@ -149,22 +87,14 @@ steps:
|
|||||||
# ============================================
|
# ============================================
|
||||||
#
|
#
|
||||||
# Required Secrets (set in Woodpecker UI):
|
# Required Secrets (set in Woodpecker UI):
|
||||||
# - registry_password: Docker registry password/token
|
# - registry_password: Gitea container registry password/token
|
||||||
# - deploy_host: Linode VPS hostname or IP
|
# - portainer_webhook_url: Portainer stack webhook URL (Redeploy trigger)
|
||||||
# - deploy_username: SSH username (e.g., root, deploy)
|
|
||||||
# - deploy_ssh_key: Private SSH key (multi-line)
|
|
||||||
# - deploy_port: SSH port (default: 22)
|
|
||||||
#
|
#
|
||||||
# Required Environment Variables:
|
# Required Environment Variables (Gitea registry):
|
||||||
# - REGISTRY_URL: Docker registry URL (e.g., registry.example.com)
|
# - REGISTRY_URL: git.mifi.dev
|
||||||
# - REGISTRY_REPO: Full image path (e.g., registry.example.com/mifi-ventures-landing)
|
# - REGISTRY_REPO: git.mifi.dev/mifi-ventures/landing
|
||||||
# - REGISTRY_USERNAME: Registry username
|
# - REGISTRY_USERNAME: your Gitea username
|
||||||
# - CONTAINER_NAME: Docker container name (e.g., mifi-ventures-landing)
|
|
||||||
# - APP_PORT: Host port to expose (e.g., 8080)
|
|
||||||
#
|
#
|
||||||
# Example .env for local testing:
|
# Portainer: Add stack from "Git repository" with this repo, compose path
|
||||||
# REGISTRY_URL=registry.example.com
|
# docker-compose.yml. Enable GitOps → Webhook and "Re-pull image".
|
||||||
# REGISTRY_REPO=registry.example.com/mifi-ventures-landing
|
# Add Gitea registry in Portainer (Settings → Registries) so the host can pull.
|
||||||
# REGISTRY_USERNAME=myuser
|
|
||||||
# CONTAINER_NAME=mifi-ventures-landing
|
|
||||||
# APP_PORT=8080
|
|
||||||
|
|||||||
366
DEPLOYMENT.md
366
DEPLOYMENT.md
@@ -1,125 +1,86 @@
|
|||||||
# Deployment Guide
|
# 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
|
## Prerequisites
|
||||||
|
|
||||||
- Gitea instance with Woodpecker CI configured
|
- Gitea with Woodpecker CI and **container registry** enabled
|
||||||
- Private Docker registry (Docker Hub, GitHub Container Registry, Harbor, etc.)
|
- Linode VPS with Docker; Portainer installed and managing that environment
|
||||||
- Linode VPS with Docker installed
|
- Portainer stack created (Repository pointing at this repo, or Web editor with equivalent compose)
|
||||||
- SSH access to VPS
|
|
||||||
|
|
||||||
## Step-by-Step Setup
|
## 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
|
### 2. Create the Portainer stack
|
||||||
# Update system
|
|
||||||
sudo apt update && sudo apt upgrade -y
|
|
||||||
|
|
||||||
# Install Docker
|
**If using Git repository (recommended):**
|
||||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
|
||||||
sudo sh get-docker.sh
|
|
||||||
|
|
||||||
# Create deploy user (optional, recommended)
|
1. **Stacks → Add stack** → name (e.g. `landing`).
|
||||||
sudo useradd -m -s /bin/bash deploy
|
2. Choose **Git repository**.
|
||||||
sudo usermod -aG docker deploy
|
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
|
**If using Web editor:**
|
||||||
docker --version
|
|
||||||
docker ps
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
Use Gitea’s built-in container registry. Image path:
|
||||||
# Generate SSH key pair
|
|
||||||
ssh-keygen -t ed25519 -C "woodpecker-deploy" -f ~/.ssh/woodpecker_deploy
|
|
||||||
|
|
||||||
# Copy public key to VPS
|
- **Registry URL**: `git.mifi.dev`
|
||||||
ssh-copy-id -i ~/.ssh/woodpecker_deploy.pub deploy@your-vps-ip
|
- **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.
|
||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Configure Woodpecker Secrets
|
### 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 |
|
||||||
|
|------|-------|
|
||||||
| Name | Value | Notes |
|
| `registry_password` | Gitea token or password (package write to the repo’s registry) |
|
||||||
|------|-------|-------|
|
| `portainer_webhook_url` | Portainer stack webhook URL (from step 2) |
|
||||||
| `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-----
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Configure Woodpecker Variables
|
### 5. Configure Woodpecker Variables
|
||||||
|
|
||||||
Navigate to: `Gitea → Repository → Settings → Variables`
|
In **Gitea → Repository → Settings → Variables** (Woodpecker):
|
||||||
|
|
||||||
Add these variables:
|
| Name | Value |
|
||||||
|
|------|-------|
|
||||||
| Name | Value | Example |
|
| `REGISTRY_URL` | `git.mifi.dev` |
|
||||||
|------|-------|---------|
|
| `REGISTRY_REPO` | `git.mifi.dev/mifi-ventures/landing` |
|
||||||
| `REGISTRY_URL` | Registry base URL | `ghcr.io` |
|
| `REGISTRY_USERNAME` | Your Gitea username |
|
||||||
| `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` |
|
|
||||||
|
|
||||||
### 6. Test Deployment
|
### 6. Test Deployment
|
||||||
|
|
||||||
#### Local Test Build
|
#### Local Test Build
|
||||||
```bash
|
```bash
|
||||||
# Clone repo
|
# Clone repo
|
||||||
git clone https://gitea.example.com/you/mifi-ventures-landing.git
|
git clone https://git.mifi.dev/mifi-ventures/landing.git
|
||||||
cd mifi-ventures-landing
|
cd landing
|
||||||
|
|
||||||
# Build locally
|
# Build locally
|
||||||
docker build -t test .
|
docker build -t test .
|
||||||
@@ -144,212 +105,105 @@ git add README.md
|
|||||||
git commit -m "test: trigger deployment"
|
git commit -m "test: trigger deployment"
|
||||||
git push origin main
|
git push origin main
|
||||||
|
|
||||||
# Watch pipeline in Woodpecker UI
|
# Watch pipeline in Woodpecker UI (Gitea → Repository → Pipelines).
|
||||||
# Check: Gitea → Repository → Pipelines
|
# After build + push, deploy step calls Portainer webhook; Portainer redeploys the stack.
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. Verify Deployment
|
### 7. Verify Deployment
|
||||||
|
|
||||||
After pipeline completes:
|
After the pipeline completes:
|
||||||
|
|
||||||
```bash
|
- In **Portainer**, open the stack and confirm the service is running and the image was pulled.
|
||||||
# Check container is running
|
- From your machine: `curl http://your-vps-ip:8080` (or the port you set in the stack).
|
||||||
ssh deploy@your-vps-ip "docker ps | grep mifi-ventures-landing"
|
- In a browser: visit `http://your-vps-ip:8080` (or your domain if you use a reverse proxy).
|
||||||
|
|
||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Port Configuration
|
## 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)
|
- **Direct port 80**: set `LANDING_PORT=80` in the stack.
|
||||||
```bash
|
- **Custom port (e.g. behind Traefik)**: set `LANDING_PORT=8080` and proxy 80/443 → 8080.
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: With Traefik, security headers are added at the proxy level.
|
**Note**: With Traefik, security headers are added at the proxy level.
|
||||||
|
|
||||||
## Traefik Integration Example
|
## 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
|
```yaml
|
||||||
# In docker-compose.yml or docker run command
|
services:
|
||||||
labels:
|
landing:
|
||||||
- "traefik.enable=true"
|
image: ${LANDING_IMAGE:-git.mifi.dev/mifi-ventures/landing:latest}
|
||||||
- "traefik.http.routers.mifi.rule=Host(`mifi.ventures`)"
|
pull_policy: always
|
||||||
- "traefik.http.routers.mifi.entrypoints=websecure"
|
restart: unless-stopped
|
||||||
- "traefik.http.routers.mifi.tls.certresolver=letsencrypt"
|
labels:
|
||||||
- "traefik.http.services.mifi.loadbalancer.server.port=80"
|
- "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:
|
(Remove or omit `ports` if Traefik is the only entry point.)
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Issues
|
## Common Issues
|
||||||
|
|
||||||
### SSH Permission Denied
|
### Registry Login Failed (Woodpecker push)
|
||||||
```bash
|
- Ensure `REGISTRY_URL` is `git.mifi.dev` and `REGISTRY_REPO` is `git.mifi.dev/mifi-ventures/landing`.
|
||||||
# Check key permissions (must be 600)
|
- Use a Gitea token with package write permission (or your password if allowed).
|
||||||
chmod 600 ~/.ssh/woodpecker_deploy
|
- Test locally: `echo "TOKEN" | docker login git.mifi.dev -u USERNAME --password-stdin`.
|
||||||
|
|
||||||
# Verify public key is on server
|
### Portainer Cannot Pull Image
|
||||||
ssh deploy@host "cat ~/.ssh/authorized_keys"
|
- 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
|
### Webhook Returns 4xx/5xx
|
||||||
ssh -vvv -i ~/.ssh/woodpecker_deploy deploy@host
|
- 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
|
### Container Won’t Start (Portainer)
|
||||||
```bash
|
- In Portainer, open the stack → service → **Logs**.
|
||||||
# Test login locally
|
- Check for port conflicts on the host (e.g. another service using the same port).
|
||||||
echo "PASSWORD" | docker login registry.example.com -u username --password-stdin
|
- Ensure the Gitea registry is added in Portainer so the image can be pulled.
|
||||||
|
|
||||||
# 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"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rollback Procedure
|
## Rollback Procedure
|
||||||
|
|
||||||
If deployment fails:
|
If a bad image was deployed:
|
||||||
|
|
||||||
```bash
|
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.
|
||||||
# SSH to VPS
|
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.
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring
|
## Monitoring
|
||||||
|
|
||||||
### Container Status
|
- **Portainer**: Stack → service → **Logs**, **Inspect**, **Stats**.
|
||||||
```bash
|
- **On the host** (if you have SSH): `docker ps`, `docker logs <container>`, `docker stats <container>`.
|
||||||
# Check if running
|
- **Nginx logs** (from host): `docker exec <container> tail -f /var/log/nginx/access.log` (and `error.log`).
|
||||||
docker ps | grep mifi-ventures-landing
|
- **Disk**: Portainer → **Host** → disk usage; or on host: `docker system df`, `docker image prune -af --filter "until=72h"`.
|
||||||
|
|
||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Checklist
|
## Security Checklist
|
||||||
|
|
||||||
- [ ] SSH key is Ed25519 (not RSA)
|
- [ ] Registry (Gitea) uses authentication; token stored only in Woodpecker secrets
|
||||||
- [ ] Private key is stored securely in Woodpecker (never in repo)
|
- [ ] Portainer webhook URL stored only in Woodpecker secrets (not in repo)
|
||||||
- [ ] Deploy user has minimal permissions (not root if possible)
|
|
||||||
- [ ] Registry uses authentication (not public)
|
|
||||||
- [ ] VPS firewall configured (ufw or iptables)
|
- [ ] VPS firewall configured (ufw or iptables)
|
||||||
- [ ] Traefik handles TLS termination (if used)
|
- [ ] Traefik (or reverse proxy) handles TLS termination if used
|
||||||
- [ ] Container runs as non-root user (nginx user)
|
- [ ] Container runs as non-root user (nginx user in the image)
|
||||||
- [ ] Regular security updates (`apt update && apt upgrade`)
|
- [ ] Regular security updates on the host (`apt update && apt upgrade`)
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
1. Set up monitoring (Prometheus + Grafana)
|
1. Set up monitoring (Prometheus + Grafana)
|
||||||
2. Configure automated backups
|
2. Configure automated backups
|
||||||
3. Add Slack/Discord notifications to pipeline
|
3. Add Slack/Discord notifications to the Woodpecker pipeline
|
||||||
4. Set up staging environment
|
4. Set up a staging stack in Portainer (e.g. different port or branch)
|
||||||
5. Implement blue-green deployments for zero downtime
|
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
|
**Maintainer**: Mike Fitzpatrick
|
||||||
|
|||||||
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
@@ -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
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "mifi-ventures-landing",
|
"name": "mifi-ventures-landing",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"repository": "https://git.mifi.dev/mifi-ventures/landing.git",
|
||||||
"packageManager": "pnpm@9.15.0",
|
"packageManager": "pnpm@9.15.0",
|
||||||
"description": "mifi Ventures landing site — static build with critical CSS inlining",
|
"description": "mifi Ventures landing site — static build with critical CSS inlining",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user