Files
landing/docs/DEPLOYMENT.md
mifi 911093f0b6
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
The Svelte 5 SSG Migration (#1)
- 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>
2026-02-01 05:50:41 +00:00

13 KiB
Raw Blame History

Deployment Guide

Woodpecker runs the SvelteKit build (pnpm run builddist/), builds the Docker image from that output, 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. 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 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 45 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 youre 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):

  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 updatesWebhook. 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).
  3. Enable the stack webhook, copy the URL for portainer_webhook_url.
  4. Deploy the stack.

3. Gitea container registry

Use Giteas 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 repos registry)
portainer_webhook_url Portainer stack webhook URL (from step 2)

REGISTRY_URL and REGISTRY_REPO are set in .woodpecker/deploy.yaml; you dont 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=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:

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 stacks 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 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.

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):

    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):

    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-31
Maintainer: Mike Fitzpatrick