Files
landing/docs/DEPLOYMENT.md

264 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Deployment Guide
Woodpecker runs the SvelteKit build (`pnpm run build``dist/`), 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”:
```bash
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 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 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).
```bash
# 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
```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 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`):
```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-31
**Maintainer**: Mike Fitzpatrick