Add a new service to Harbor — scaffold the compose config, environment variables, metadata, documentation, and cross-service integrations. Use this skill whenever the user wants to add a new service to Harbor, integrate a new tool/app/model server, create a compose configuration for a new project, or onboard any software into the Harbor ecosystem. Triggers on phrases like "add X to Harbor", "new service", "integrate Y", "onboard Z", "create a service for", or when the user provides a GitHub repo link and expects it to become a Harbor service. Even if the user just says a project name and implies they want it in Harbor, this skill applies.
74
92%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Subagent instruction: Before doing any work, read this skill file in full using the
read_filetool at path/home/everlier/code/harbor/.agents/skills/new-service/SKILL.md. Do not proceed until the file has been read.
Harbor is a modular Docker Compose project. Each service is a self-contained unit:
a compose file, a service directory, environment variables, app metadata, and documentation.
The detailed technical reference lives in .github/copilot-new-service.md — consult it
for edge cases or patterns not covered here.
Gather Info → Scaffold → Compose Config → Env Vars → Metadata → Documentation → Integrations → TestEvery step has validation criteria. Do not advance until the current step passes.
Before writing any files, understand what you're building.
Required inputs:
Research the repo. Look for:
docker-compose.yml, Dockerfile, or docker/ directoryIf the repo has its own docker-compose, study the service definitions — you'll adapt them to Harbor's conventions, not copy them verbatim.
If ambiguous, ask the user. Don't guess the category or deployment method.
The handle is the service's identifier everywhere in Harbor.
Rules:
Validate:
ls services/compose.${handle}.yml 2>/dev/null && echo "TAKEN" || echo "OK"
grep -q "\"${handle}\"\\|'${handle}'\\|${handle}:" app/src/serviceMetadata.ts && echo "TAKEN" || echo "OK"harbor dev scaffold ${handle}This creates:
services/compose.${handle}.yml — starter compose configservices/${handle}/override.env — service-specific env overridesEdit services/compose.${handle}.yml. The scaffold gives you a skeleton — fill it in.
Mandatory conventions:
${HARBOR_CONTAINER_PREFIX}.${handle}env_file includes both ./.env and ./services/${handle}/override.envharbor-networkHARBOR_${HANDLE}_* pattern (HANDLE = uppercase handle)${HARBOR_${HANDLE}_HOST_PORT}:${internal_port}./services/${handle}/... for config, ${HARBOR_${HANDLE}_WORKSPACE}/... for datarestart policy — automatic restart is not expected in Harbor${HARBOR_${HANDLE}_IMAGE}:${HARBOR_${HANDLE}_VERSION}If building from source (no pre-built image), use build instead of image:
build:
context: ${HARBOR_${HANDLE}_GIT_REF}
dockerfile: DockerfileIf the service needs multiple containers (e.g., app + database), define them all in the
same compose file. The primary container must use the handle as its service name.
Auxiliary containers use ${handle}-<role> naming (e.g., windmill-db, windmill-worker).
Example — a typical web service:
services:
myservice:
container_name: ${HARBOR_CONTAINER_PREFIX}.myservice
image: ${HARBOR_MYSERVICE_IMAGE}:${HARBOR_MYSERVICE_VERSION}
env_file:
- ./.env
- ./services/myservice/override.env
ports:
- ${HARBOR_MYSERVICE_HOST_PORT}:8080
volumes:
- ${HARBOR_MYSERVICE_WORKSPACE}/data:/app/data
networks:
- harbor-networkAdd a section to profiles/default.env for the service.
Port allocation: Read the end of default.env to find the last used port.
Pick the next available port in the 33000–34999 range. Increment by 10 for services
needing multiple ports.
Required variables:
# Service Name
HARBOR_${HANDLE}_HOST_PORT=34XXX
HARBOR_${HANDLE}_IMAGE="repo/image"
HARBOR_${HANDLE}_VERSION="latest"Common additional variables:
HARBOR_${HANDLE}_WORKSPACE=./${handle} # persistent data dir
HARBOR_${HANDLE}_MODEL=some-default-model # if service uses AI models
HARBOR_${HANDLE}_GIT_REF=https://github.com/...#branch # if building from sourceEvery env var referenced in the compose file must be defined in default.env.
After editing default.env, propagate:
harbor config updateAdd an entry at the end of the serviceMetadata object in app/src/serviceMetadata.ts.
${handle}: {
name: 'Display Name',
tags: [HST.${category}], // + additional tags: HST.cli, HST.api, HST.rag, etc.
projectUrl: 'https://github.com/...',
wikiUrl: `${wikiUrl}/2.${cat_num}.${next_num}-${Category}-${Name}`,
tooltip: 'Brief description for the UI.',
},Logo resolution: After adding the metadata entry, resolve the service logo:
harbor dev add-logos --dry-run # preview
harbor dev add-logos # write to serviceMetadata.tsThis populates the logo field automatically from the project URL.
Category → cat_num mapping:
2.12.22.3Find the next doc number:
ls docs/2.${cat_num}.*.md | sort -t. -k3 -n | tail -1Available tags (from HST enum): backend, frontend, satellite, api, cli,
partial, builtIn, eval, audio, rag, image, workflows, tools, infra.
Add HST.cli for CLI-only services (blocks "Open" button in the app).
Create docs/2.${cat_num}.${next_num}-${Category}-${ServiceName}.md.
Follow the format from docs/2.3.52-Satellite-Windmill.md — it's the reference example.
Required sections:
### [Service Name](https://github.com/repo-link)
> Handle: `${handle}`<br/>
> URL: [http://localhost:PORT](http://localhost:PORT)
Brief description of what the service does.
## Starting
\`\`\`bash
harbor pull ${handle} # or harbor build ${handle}
harbor up ${handle} --open
\`\`\`
First-launch notes (default credentials, setup steps, etc.)
## Configuration
### Environment Variables
Following options can be set via [`harbor config`](./3.-Harbor-CLI-Reference.md#harbor-config):
\`\`\`bash
# Document every HARBOR_${HANDLE}_* variable with descriptions
\`\`\`
### Volumes
Describe persistent data and configuration mounts.
## Troubleshooting
\`\`\`bash
harbor logs ${handle}
\`\`\`
Common issues and solutions.
## Links
- [Official Documentation](...)
- [GitHub Repository](...)Rules:
default.envEvery new service ships with one screenshot of its UI in the docs.
docs/harbor-${handle}.png — the harbor- prefix is the convention for service screenshots; older shots without the prefix predate it.agent-browser so the screenshot comes from the same browser workflow used for validation:
agent-browser open http://localhost:${PORT}/
agent-browser wait --load networkidle
agent-browser screenshot docs/harbor-${handle}.png
agent-browser closeCreate services/${handle}/.gitignore for persistent data directories:
data/
cache/
logs/Add any config files, entrypoints, or Dockerfiles the service needs into services/${handle}/.
apk add at runtimeIf you add an init/bootstrap sidecar that needs tools beyond what alpine:3.20 ships
(curl, jq, sqlite, ssh-keygen, etc.), do not install them with apk add in
the entrypoint. Every container creation re-pays the install cost: empirically apk add --no-cache curl jq takes ~24s on each harbor up, blocking everything that depends on
the sidecar via service_completed_successfully (e.g. webui waiting on
unsloth-studio-bootstrap).
The correct pattern is one of:
(a) Inline Dockerfile in compose — preferred for sidecars wholly owned by one service:
services:
${handle}-bootstrap:
build:
context: ./services/${handle}
dockerfile_inline: |
FROM alpine:3.20
RUN apk add --no-cache curl jq
entrypoint: ["/bin/sh", "/usr/local/bin/bootstrap.sh"]
volumes:
- ./services/${handle}/bootstrap.sh:/usr/local/bin/bootstrap.sh:ro(b) Standalone Dockerfile — services/${handle}/Dockerfile.bootstrap + build: { context, dockerfile } — use when the sidecar pulls in non-trivial setup (multi-stage, COPYed assets) or could be shared across services.
Either way the apk install runs once at build time, not on every up. The build itself is cached by Docker, so subsequent ups skip it entirely.
Cross-files are applied when multiple services run together. They live in services/ with
the naming pattern compose.x.${handle}.${other}.yml.
When to create cross-files:
| Integration | File | Purpose |
|---|---|---|
| Ollama | compose.x.${handle}.ollama.yml | Set Ollama URL env vars, add depends_on |
| GPU | compose.x.${handle}.nvidia.yml | GPU passthrough via deploy.resources |
| Traefik | compose.x.traefik.${handle}.yml | Reverse proxy labels |
Ollama integration pattern:
services:
${handle}:
depends_on:
- ollama
environment:
- OLLAMA_API_BASE=${HARBOR_OLLAMA_INTERNAL_URL}Check the service's docs for the correct Ollama env var name. Common variants:
OLLAMA_API_BASE, OLLAMA_URL, OLLAMA_BASE_URL, OLLAMA_HOST.
The goal is to prove the service is useful from an end-user point of view. Avoid superficial checks that do not validate the important workflow. A container starting, a port returning HTML, or /health returning OK is necessary but not sufficient.
Build/pull first:
harbor pull ${handle} # pre-built image
harbor build ${handle} # source buildStart the user-facing scenario:
harbor up ${handle}If the service's main value depends on Harbor integrations, start the integrated scenario too. For example, for an AI app that should use local models and search, test the real workflow with:
harbor up ${handle} ollama searxngValidate the most important workflow:
harbor up.agent-browser to interact with the UI: sign up or log in, complete onboarding, configure/select the Harbor backend if needed, submit a real prompt/task/upload, and verify the expected result appears./health.Useful web UI loop:
agent-browser open http://localhost:${PORT}/
agent-browser snapshot -i
# interact with refs from the snapshot
agent-browser click @e3
agent-browser snapshot -iAlso check logs for errors after the workflow, not only during startup:
harbor logs ${handle}Clean up after testing:
harbor down ${handle}Before declaring the service complete, verify all of these:
default.envharbor config update ran after editing default.envserviceMetadata.ts with correct category and doc linkdocs/harbor-${handle}.png and embedded in the doc.gitignore in service directory covers generated filesharbor up ${handle})agent-browser interactions when applicableharbor logs ${handle})harbor dev add-logos ran to resolve the service logo (or added manually)harbor config update after editing default.env — your .env won't
have the new variables and the service will fail with empty substitutions../services/${handle}/... (relative to repo root),
not absolute paths.restart: always — Harbor doesn't expect auto-restart; omit the policy..env directly — always use harbor config set or edit default.env +
harbor config update.If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.