Files
homelab-optimized/docs/guides/deploy-new-service-gitops.md
Gitea Mirror Bot 082633dad9
Some checks failed
Documentation / Deploy to GitHub Pages (push) Has been cancelled
Documentation / Build Docusaurus (push) Has been cancelled
Sanitized mirror from private repository - 2026-04-05 10:50:43 UTC
2026-04-05 10:50:43 +00:00

11 KiB
Raw Blame History

Deploying a New Service via GitOps

Last Updated: March 7, 2026

This guide walks through every step needed to go from a bare docker-compose.yml file to a live, Portainer-managed container that auto-deploys on every future git push. It covers the complete end-to-end flow: writing the compose file, wiring it into the repo, adding it to Portainer, and verifying the CI pipeline fires correctly.


How the pipeline works

You write a compose file
        │
        ▼
git push to main
        │
        ▼
Gitea CI runs portainer-deploy.yml
        │  detects which files changed
        │  matches them against live Portainer stacks
        ▼
Portainer redeploys matching stacks
        │
        ▼
Container restarts on the target host
        │
        ▼
ntfy push notification sent to your phone

Every push to main that touches a file under hosts/** or common/** triggers this automatically. You never need to click "redeploy" in Portainer manually once the stack is registered.


Prerequisites

  • SSH access to the target host (or Portainer UI access to it)
  • Portainer access: http://192.168.0.200:10000
  • Git push access to git.vish.gg/Vish/homelab
  • A docker-compose.yml (or .yaml) for the service you want to run

Step 1 — Choose your host

Pick the host where the container will run. Use this table:

Host Portainer Endpoint ID Best for
Atlantis (DS1823xs+) 2 Media, high-storage services, primary NAS workloads
Calypso (DS723+) 443397 Secondary media, backup services, Authentik SSO
Concord NUC 443398 DNS (AdGuard), Home Assistant, network services
Homelab VM 443399 Monitoring, dev tools, lightweight web services
RPi 5 443395 IoT, uptime monitoring, edge sensors

The file path you choose in Step 2 determines which host Portainer deploys to — they must match.


Step 2 — Place the compose file in the repo

Clone the repo if you haven't already:

git clone https://git.vish.gg/Vish/homelab.git
cd homelab

Create your compose file in the correct host directory:

hosts/synology/atlantis/      ← Atlantis
hosts/synology/calypso/       ← Calypso
hosts/physical/concord-nuc/   ← Concord NUC
hosts/vms/homelab-vm/         ← Homelab VM
hosts/edge/rpi5-vish/         ← Raspberry Pi 5

For example, deploying a service called myapp on the Homelab VM:

# create the file
nano hosts/vms/homelab-vm/myapp.yaml

Step 3 — Write the compose file

Follow these conventions — they're enforced by the pre-commit hooks:

# myapp — one-line description of what this does
# Port: 8080
services:
  myapp:
    image: vendor/myapp:1.2.3          # pin a version, not :latest
    container_name: myapp
    restart: unless-stopped            # always use unless-stopped, not always
    security_opt:
      - no-new-privileges:true
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=America/Los_Angeles
      - SOME_SECRET=${MYAPP_SECRET}    # secrets via Portainer env vars, not plaintext
    volumes:
      - /home/homelab/docker/myapp:/config
    ports:
      - "8080:8080"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 20s

Key rules:

Rule Why
restart: unless-stopped Allows docker stop for maintenance without immediate restart
no-new-privileges:true Prevents container from gaining extra Linux capabilities
Pin image versions Renovate Bot will open a PR when a new version is available; :latest gives you no control
Secrets via ${VAR} Never commit real passwords or tokens — set them in Portainer's stack environment UI
2-space indentation yamllint will block the commit otherwise

If your service needs a secret, use variable interpolation and set the value in Portainer later (Step 6):

environment:
  - API_KEY=${MYAPP_API_KEY}
  - DB_PASSWORD="REDACTED_PASSWORD"

Step 4 — Validate locally before pushing

The pre-commit hooks run this automatically on git commit, but you can run it manually first:

# Validate compose syntax
docker compose -f hosts/vms/homelab-vm/myapp.yaml config

# Run yamllint
yamllint -c .yamllint hosts/vms/homelab-vm/myapp.yaml

# Scan for accidentally committed secrets
detect-secrets scan hosts/vms/homelab-vm/myapp.yaml

If docker compose config returns clean YAML with no errors, you're good.


Step 5 — Commit and push

git add hosts/vms/homelab-vm/myapp.yaml
git commit -m "feat: add myapp to homelab-vm

Brief description of what this service does and why."
git push origin main

The pre-commit hooks will run automatically on git commit:

  • yamllint — checks indentation and syntax
  • docker-compose-check — validates the compose file parses correctly
  • detect-secrets — blocks commits containing passwords or tokens

If any hook fails, fix the issue and re-run git commit.


Step 6 — Add the stack to Portainer

This is a one-time step per new service. After this, every future git push will auto-redeploy the stack without any manual Portainer interaction.

  1. Open Portainer: http://192.168.0.200:10000
  2. In the left sidebar, select the correct endpoint (e.g. "Homelab VM")
  3. Click Stacks+ Add stack
  4. Fill in the form:
Field Value
Name myapp-stack (lowercase, hyphens, no spaces)
Build method Git Repository
Repository URL https://git.vish.gg/Vish/homelab
Repository reference refs/heads/main
Authentication Enable → username vish, password = "REDACTED_PASSWORD" token
Compose path hosts/vms/homelab-vm/myapp.yaml
GitOps updates Enable (toggle on)
  1. If your compose file uses ${VAR} placeholders, scroll down to Environment variables and add each one:
Variable Value
MYAPP_API_KEY your-actual-key
MYAPP_DB_PASSWORD your-actual-password
  1. Click Deploy the stack

Portainer pulls the file from Gitea, runs docker compose up -d, and the container starts.

Note on GitOps updates toggle: Enabling this makes Portainer poll Gitea every 5 minutes for changes. However, the CI pipeline (portainer-deploy.yml) handles redeployment on push much faster — the toggle is useful as a fallback but the CI is the primary mechanism.


Step 7 — Verify the CI pipeline fires

After your initial push (Step 5), check that the CI workflow ran:

  1. Go to https://git.vish.gg/Vish/homelab/actions
  2. You should see a portainer-deploy.yml run triggered by your push
  3. Click into it — the log should show:
Changed files (1):
  hosts/vms/homelab-vm/myapp.yaml

Checking 80 GitOps stacks for matches...

Deploying (GitOps): myapp-stack  (stack=XXX)
  File: hosts/vms/homelab-vm/myapp.yaml
  ✓ deployed successfully

==================================================
Deployed (1): myapp-stack

If the run shows "No stacks matched the changed files — nothing deployed", it means the compose file path in Portainer doesn't exactly match the path in the repo. Double-check the Compose path field in Portainer (Step 6, step 4) — it must be identical, including the hosts/ prefix.


Step 8 — Verify the container is running

On the Homelab VM (which is the machine you're reading this on):

docker ps --filter name=myapp
docker logs myapp --tail 50

For other hosts, SSH in first:

ssh calypso
sudo /usr/local/bin/docker ps --filter name=myapp

Or use Portainer's built-in log viewer: Stacksmyapp-stack → click the container name → Logs.


Step 9 — Test future auto-deploys work

Make a trivial change (add a comment, bump an env var) and push:

# edit the file
nano hosts/vms/homelab-vm/myapp.yaml

git add hosts/vms/homelab-vm/myapp.yaml
git commit -m "chore: test auto-deploy for myapp"
git push origin main

Watch https://git.vish.gg/Vish/homelab/actions — a new portainer-deploy.yml run should appear within 1015 seconds, complete in under a minute, and the container will restart with the new config.


Common problems

"No stacks matched the changed files"

The path stored in Portainer doesn't match the file path in the repo.

  • In Portainer: Stacks → your stack → Editor tab → check the Compose path field
  • It must exactly match the repo path, e.g. hosts/vms/homelab-vm/myapp.yaml
  • Note: All Portainer stacks use canonical hosts/ paths — ensure the Compose path field matches exactly (e.g. hosts/synology/calypso/myapp.yaml)

"Conflict. The container name is already in use"

A container with the same container_name already exists on the host from a previous manual deploy or a different stack.

# Find and remove it
docker rm -f myapp

# Then re-trigger: edit any line in the compose file and push

Or via Portainer API:

curl -X DELETE \
  -H "X-API-Key: $PORTAINER_TOKEN" \
  "http://192.168.0.200:10000/api/endpoints/443399/docker/containers/$(docker inspect --format '{{.Id}}' myapp)?force=true"

Pre-commit hook blocks the commit

yamllint indentation error — you have 4-space indent instead of 2-space. Fix with:

# Check which lines are wrong
yamllint -c .yamllint hosts/vms/homelab-vm/myapp.yaml

detect-secrets blocks a secret — you have a real token/password in the file. Move it to a ${VAR} placeholder and set the value in Portainer's environment variables instead.

docker-compose-check fails — the compose file has a syntax error:

docker compose -f hosts/vms/homelab-vm/myapp.yaml config

Portainer shows HTTP 500 on redeploy

Usually a docker-level error — check the full error message in the CI log or Portainer stack events. Common causes:

  • Port already in use on the host → change the external port mapping
  • Volume path doesn't exist → create the directory on the host first
  • Image pull failed (private registry, wrong tag) → verify the image name and tag

Checklist

  • Compose file placed in correct hosts/<host>/ directory
  • Image pinned to a specific version (not :latest)
  • restart: unless-stopped set
  • Secrets use ${VAR} placeholders, not plaintext values
  • docker compose config passes with no errors
  • git push to main succeeded
  • Stack added to Portainer with correct path and environment variables
  • CI run at git.vish.gg/Vish/homelab/actions shows successful deploy
  • docker ps on the target host confirms container is running
  • Future push triggers auto-redeploy (tested with a trivial change)