12 KiB
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 pullgit.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”:
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):
- Stacks → Add stack → name (e.g.
landing). - Choose Git repository.
- Repository URL:
https://git.mifi.dev/mifi-ventures/landing.git(or your clone URL). Add credentials if the repo is private. - Repository reference:
main. - Compose path:
docker-compose.yml. - Enable GitOps updates → Webhook. Copy the webhook URL for the Woodpecker secret
portainer_webhook_url. - Enable Re-pull image so each webhook pulls the new image.
- Optionally set stack env vars:
LANDING_PORT=8080(or leave default in compose). - Deploy the stack.
If using Web editor:
- Stacks → Add stack → name (e.g.
landing). - Choose Web editor and paste the same structure as
docker-compose.yml(service withimage: git.mifi.dev/mifi-ventures/landing:latest,pull_policy: always,ports,restart). - Enable the stack webhook, copy the URL for
portainer_webhook_url. - 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/landingpackage. Use that asregistry_passwordin 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
# 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
# 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=80in the stack. - Custom port (e.g. behind Traefik): set
LANDING_PORT=8080and 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:
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_URLisgit.mifi.devandREGISTRY_REPOisgit.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:
- 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. - 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 setREGISTRY_REPOto match in Woodpecker and indocker-compose.yml).
- 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
- 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).
-
Rebuild and push for amd64 (from your Mac, after
docker login git.mifi.dev):docker buildx build --platform linux/amd64 -t git.mifi.dev/mifi-ventures/landing:latest --push .Using
--pushsends the amd64 image straight to the registry. If you see “multiple platforms not supported”, rundocker buildx create --name multiarch --useonce, then retry. -
Force the VPS to pull the new image — otherwise it may keep using a cached ARM64 copy of
latest. On the Linode host (SSH):docker rmi git.mifi.dev/mifi-ventures/landing:latestThen 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.
-
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 listlinux/amd64.
Woodpecker runs on amd64, so pipeline-built images are already correct for typical VPS hosts.
Rollback Procedure
If a bad image was deployed:
- 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. - 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(anderror.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
- Set up monitoring (Prometheus + Grafana)
- Configure automated backups
- Add Slack/Discord notifications to the Woodpecker pipeline
- Set up a staging stack in Portainer (e.g. different port or branch)
- Optionally tag images by commit SHA in Woodpecker for easier rollback in Portainer
Last Updated: 2026-01-30
Maintainer: Mike Fitzpatrick