- Migrates the site to Svelte 5 - Still generates a static site with inlined critical path CSS for the ultimate in performance - Opens up future possibilities for site growth Reviewed-on: #1 Co-authored-by: mifi <badmf@mifi.dev> Co-committed-by: mifi <badmf@mifi.dev>
13 KiB
Deployment Guide
Woodpecker runs the SvelteKit build (pnpm run build → dist/), builds the Docker image from that output, 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. Opening a pull request runs a separate workflow (lint, tests, and a test build) on the branch without building or pushing the Docker image.
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:
From the repo root, run pnpm install and pnpm run build to produce dist/, then use Docker (or OrbStack, Colima, Rancher Desktop). 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”:
pnpm install
pnpm run build # SvelteKit → dist/; Critters inlines critical CSS
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/deploy.yaml; you don’t need to add them anywhere.
5. Test Deployment
Local Test Build
The Docker image is built from the contents of dist/. That directory is produced by the SvelteKit build, so you must run pnpm run build before docker build (or let the Dockerfile run it inside the image).
# Clone repo
git clone https://git.mifi.dev/mifi-ventures/landing.git
cd landing
# Install deps and build static site (SvelteKit + Critters)
pnpm install
pnpm run build
# Build Docker image (uses dist/ from the previous step if built on host,
# or the Dockerfile runs the build inside the container)
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
To test the static site without Docker, run pnpm run preview after pnpm run build and open the URL shown (e.g. http://localhost:4173).
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-31
Maintainer: Mike Fitzpatrick