252 lines
12 KiB
Markdown
252 lines
12 KiB
Markdown
# Deployment Guide
|
||
|
||
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 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 and Portainer
|
||
|
||
- 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`.
|
||
|
||
### 1b. First-time: push the image so the stack can deploy
|
||
|
||
Portainer pulls `git.mifi.dev/mifi-ventures/landing:latest`. If that image has never been pushed, you get **manifest unknown**. Push it once (from your machine or by running the Woodpecker pipeline), then create the stack.
|
||
|
||
**Option A – Run Woodpecker:** Ensure Woodpecker secrets and variables are set (steps 4–5 below), then push a commit to `main`. The pipeline will build and push the image; after it succeeds, create the stack in Portainer (step 2).
|
||
|
||
**Option B – Push from your machine:**
|
||
|
||
Use Docker (or OrbStack, Colima, Rancher Desktop) from the repo root. **If you’re on Apple Silicon (M1/M2/M3) or another ARM Mac**, the VPS is x86_64, so build for that platform to avoid “exec format error”:
|
||
|
||
```bash
|
||
docker login git.mifi.dev # use your Gitea username and token/password
|
||
docker buildx build --platform linux/amd64 -t git.mifi.dev/mifi-ventures/landing:latest --push .
|
||
```
|
||
|
||
Using `--push` (instead of `--load` then `docker push`) sends the amd64 image straight to the registry and avoids local cache mix-ups. If buildx says “multiple platforms not supported”, run `docker buildx create --name multiarch --use` once, then retry.
|
||
|
||
On an x86 Mac or Linux PC you can use `docker build -t ... .` and `docker push` if you prefer.
|
||
|
||
Then create the stack in Portainer; the image will exist and the deploy will succeed.
|
||
|
||
### 2. Create the Portainer stack
|
||
|
||
**If using Git repository (recommended):**
|
||
|
||
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**.
|
||
|
||
**If using Web editor:**
|
||
|
||
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**.
|
||
|
||
### 3. Gitea container registry
|
||
|
||
Use Gitea’s built-in container registry. Image path:
|
||
|
||
- **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
|
||
|
||
Woodpecker has no separate “Variables” UI — add everything under **Repo → Settings → Secrets** in Woodpecker:
|
||
|
||
| Name | Value |
|
||
|------|-------|
|
||
| `registry_username` | Your Gitea username (used for `docker login`) |
|
||
| `registry_password` | Gitea token or password (package write to the repo’s registry) |
|
||
| `portainer_webhook_url` | Portainer stack webhook URL (from step 2) |
|
||
|
||
`REGISTRY_URL` and `REGISTRY_REPO` are set in `.woodpecker.yml`; you don’t need to add them anywhere.
|
||
|
||
### 5. Test Deployment
|
||
|
||
#### Local Test Build
|
||
```bash
|
||
# Clone repo
|
||
git clone https://git.mifi.dev/mifi-ventures/landing.git
|
||
cd landing
|
||
|
||
# Build locally
|
||
docker build -t test .
|
||
|
||
# Run locally
|
||
docker run -d -p 8080:80 --name test test
|
||
|
||
# Test
|
||
curl http://localhost:8080
|
||
|
||
# Cleanup
|
||
docker stop test && docker rm test
|
||
```
|
||
|
||
#### Trigger CI/CD
|
||
```bash
|
||
# Make a small change
|
||
echo "# Test deployment" >> README.md
|
||
|
||
# Commit and push to main
|
||
git add README.md
|
||
git commit -m "test: trigger deployment"
|
||
git push origin main
|
||
|
||
# Watch pipeline in Woodpecker UI (Gitea → Repository → Pipelines).
|
||
# After build + push, deploy step calls Portainer webhook; Portainer redeploys the stack.
|
||
```
|
||
|
||
### 6. Verify Deployment
|
||
|
||
After the pipeline completes:
|
||
|
||
- 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 stack compose uses `LANDING_PORT` (default `8080`). Set it in Portainer stack env vars or in `docker-compose.yml`:
|
||
|
||
- **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 the service in `docker-compose.yml`:
|
||
|
||
```yaml
|
||
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
|
||
```
|
||
|
||
(Remove or omit `ports` if Traefik is the only entry point.)
|
||
|
||
## Common Issues
|
||
|
||
### 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`.
|
||
|
||
### unauthorized: reqPackageAccess (push rejected after login)
|
||
Gitea accepted your login but denied **package** access. Fix:
|
||
|
||
1. **Token scope** – Create a Gitea **Personal Access Token** with **Read & Write** (or equivalent) for **Packages**. Use that token as the password for `docker login git.mifi.dev`.
|
||
2. **Owner in the image path** – The path is `git.mifi.dev/{owner}/{image}`. The **owner** must be a user or org you can publish packages for:
|
||
- If the repo is under org **mifi-ventures**, your user must be a **member** of that org with **Write** (or Admin) so you can create packages under `mifi-ventures`.
|
||
- If the repo is under your **user** (e.g. `mifi/landing`), use **your username** as owner: `git.mifi.dev/mifi/landing` (and set `REGISTRY_REPO` to match in Woodpecker and in `docker-compose.yml`).
|
||
3. **2FA** – If your account uses 2FA, you must use a token; account password alone may not be enough for package push.
|
||
|
||
### 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.
|
||
|
||
### 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`.
|
||
|
||
### 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.
|
||
|
||
### exec format error (exec /docker-entrypoint.sh)
|
||
The container is running an image built for the wrong CPU architecture (e.g. **ARM64** on Apple Silicon vs **AMD64** on Linode).
|
||
|
||
1. **Rebuild and push for amd64** (from your Mac, after `docker login git.mifi.dev`):
|
||
```bash
|
||
docker buildx build --platform linux/amd64 -t git.mifi.dev/mifi-ventures/landing:latest --push .
|
||
```
|
||
Using `--push` sends the amd64 image straight to the registry. If you see “multiple platforms not supported”, run `docker buildx create --name multiarch --use` once, then retry.
|
||
|
||
2. **Force the VPS to pull the new image** — otherwise it may keep using a cached ARM64 copy of `latest`. On the Linode host (SSH):
|
||
```bash
|
||
docker rmi git.mifi.dev/mifi-ventures/landing:latest
|
||
```
|
||
Then in Portainer use **Pull and redeploy** (or redeploy with “Re-pull image” enabled) so it pulls the image again. Or redeploy the stack from the Portainer UI after removing the image on the host.
|
||
|
||
3. **Confirm the image in the registry is amd64** (optional): after pushing, run `docker buildx imagetools inspect git.mifi.dev/mifi-ventures/landing:latest` — the manifest should list `linux/amd64`.
|
||
|
||
Woodpecker runs on amd64, so pipeline-built images are already correct for typical VPS hosts.
|
||
|
||
## Rollback Procedure
|
||
|
||
If a bad image was deployed:
|
||
|
||
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
|
||
|
||
- **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
|
||
|
||
- [ ] 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 (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 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-30
|
||
**Maintainer**: Mike Fitzpatrick
|