Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ harbor eject searxng llamacpp > docker-compose.harbor.yml
[SearXNG](https://github.com/av/harbor/wiki/2.3.1-Satellite:-SearXNG) ⦁︎ [Sim Studio](https://github.com/av/harbor/wiki/2.3.58-Satellite-Sim-Studio) ⦁︎ [Solo CLI](https://github.com/av/harbor/wiki/2.3.77-Satellite-Solo-CLI) ⦁︎ [SQL Chat](https://github.com/av/harbor/wiki/2.3.35-Satellite-SQL-Chat)
[SuperGateway](https://github.com/av/harbor/wiki/2.3.44-Satellite-supergateway) ⦁︎ [SurfSense](https://github.com/av/harbor/wiki/2.3.85-Satellite-SurfSense) ⦁︎ [TextGrad](https://github.com/av/harbor/wiki/2.3.12-Satellite:-TextGrad) ⦁︎ [tokscale](https://github.com/av/harbor/wiki/2.3.86-Satellite-Tokscale)
[Traefik](https://github.com/av/harbor/wiki/2.3.37-Satellite-traefik) ⦁︎ [txtai RAG](https://github.com/av/harbor/wiki/2.3.11-Satellite:-txtai-RAG) ⦁︎ [Unsloth](https://github.com/av/harbor/wiki/2.3.51-Satellite-Unsloth) ⦁︎ [Unsloth Studio](https://github.com/av/harbor/wiki/2.3.83-Satellite-Unsloth-Studio)
[Webtop](https://github.com/av/harbor/wiki/2.3.29-Satellite:-Webtop) ⦁︎ [Windmill](https://github.com/av/harbor/wiki/2.3.52-Satellite-Windmill) ⦁︎ [LightRAG](https://github.com/av/harbor/wiki/2.3.93-Satellite-LightRAG) ⦁︎ [Paperless-ngx](https://github.com/av/harbor/wiki/2.3.94-Satellite-Paperless) ⦁︎ [Paperless-GPT](https://github.com/av/harbor/wiki/2.3.95-Satellite-Paperless-GPT) ⦁︎ [Whishper](https://github.com/av/harbor/wiki/2.3.96-Satellite-Whishper) ⦁︎ [Linkwarden](https://github.com/av/harbor/wiki/2.3.97-Satellite-Linkwarden)
[Webtop](https://github.com/av/harbor/wiki/2.3.29-Satellite:-Webtop) ⦁︎ [Windmill](https://github.com/av/harbor/wiki/2.3.52-Satellite-Windmill) ⦁︎ [LightRAG](https://github.com/av/harbor/wiki/2.3.93-Satellite-LightRAG) ⦁︎ [Paperless-ngx](https://github.com/av/harbor/wiki/2.3.94-Satellite-Paperless) ⦁︎ [Paperless-GPT](https://github.com/av/harbor/wiki/2.3.95-Satellite-Paperless-GPT) ⦁︎ [Whishper](https://github.com/av/harbor/wiki/2.3.96-Satellite-Whishper) ⦁︎ [Linkwarden](https://github.com/av/harbor/wiki/2.3.97-Satellite-Linkwarden) ⦁︎ [LinkedGrow](https://github.com/av/harbor/wiki/2.3.98-Satellite-LinkedGrow)

See [services documentation](https://github.com/av/harbor/wiki/2.-Services) for a brief overview of each.

Expand Down
8 changes: 8 additions & 0 deletions app/src/serviceMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1192,4 +1192,12 @@ export const serviceMetadata: Record<string, Partial<HarborService>> = {
wikiUrl: `${wikiUrl}/2.3.95-Satellite-Paperless-GPT`,
tooltip: 'LLM companion for Paperless-ngx: suggests titles, tags and correspondents, and does LLM-based OCR.',
},
linkedgrow: {
name: 'LinkedGrow',
tags: [HST.satellite, HST.tools],
projectUrl: 'https://github.com/DigiHold/LinkedGrow',
logo: 'https://www.google.com/s2/favicons?domain=linkedgrow.ai&sz=128',
wikiUrl: `${wikiUrl}/2.3.98-Satellite-LinkedGrow`,
tooltip: 'AI agents that find leads and clients on LinkedIn, driving a real browser because LinkedIn has no API.',
},
};
3 changes: 3 additions & 0 deletions docs/2.-Services.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ A free and open-source machine translation.
- <a href="https://github.com/av/harbor/wiki/2.3.93-Satellite-LightRAG"><img src="https://github.com/HKUDS.png?size=200" alt="LightRAG logo" width="12" height="12" /> LightRAG</a> <span style="opacity: 0.5;">`Satellite`, `RAG`, `API`</span><br/>
Graph-based RAG server with a web UI, document ingestion and an Ollama-compatible query API.

- <a href="https://github.com/av/harbor/wiki/2.3.98-Satellite-LinkedGrow"><img src="https://www.google.com/s2/favicons?domain=linkedgrow.ai&sz=128" alt="LinkedGrow logo" width="12" height="12" /> LinkedGrow</a> <span style="opacity: 0.5;">`Satellite`, `Tools`</span><br/>
AI agents that find leads and clients on LinkedIn, driving a real browser because LinkedIn has no API.

- <a href="https://github.com/av/harbor/wiki/2.3.97-Satellite-Linkwarden"><img src="https://www.google.com/s2/favicons?domain=linkwarden.app&sz=128" alt="Linkwarden logo" width="12" height="12" /> Linkwarden</a> <span style="opacity: 0.5;">`Satellite`</span><br/>
Self-hosted collaborative bookmark manager with archiving and AI-powered tagging.

Expand Down
107 changes: 107 additions & 0 deletions docs/2.3.98-Satellite-LinkedGrow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
### [LinkedGrow](https://github.com/DigiHold/LinkedGrow)

> Handle: `linkedgrow`<br/>
> URL: [http://localhost:35080](http://localhost:35080)

![LinkedGrow setup wizard, first screen after creating the owner account](./harbor-linkedgrow.png)

LinkedGrow runs AI agents that find leads and clients on LinkedIn. You describe who you sell to, and an agent goes looking for those people where they already are: under a competitor's post, inside a search you trust, among the people who reacted to a topic. It reads each profile, scores the fit, sends an invitation, and once the person accepts it runs a short conversation in your name and hands you the ones who want to talk. The posting side comes with it, including a generator, a calendar and the analytics it reads back off your own posts.

There is no LinkedIn API for any of this. Every action happens in a real Chrome, signed in to the account you connected, driven by the worker container under Xvfb at a human pace. That choice is what the resource footprint below is about.

**Key Features:**
- **Lead discovery**: mines post reactions, comments and searches for people matching a customer profile you describe
- **Scoring and outreach**: reads the profile, scores the fit, sends the invitation, then runs a follow up conversation
- **Publishing**: draft generator, an editor that scores hook and length, and a calendar that hands posts to LinkedIn's own scheduler
- **Analytics**: impressions, reactions, comments and follower count read off your own posts, never estimated
- **MCP server**: 26 tools behind one URL and one key, so an assistant can drive the agents and the leads

#### Starting

```bash
# Pull the images
harbor pull linkedgrow

# Start LinkedGrow
harbor up linkedgrow --open
```

- On the first start, create an account via **Sign Up**. That first account is the administrator. Sign ups stay open until the setup wizard finishes, and after that a new account needs the administrator to reopen them, so finish the wizard before the instance is reachable by anyone else.
- The setup wizard then runs, and it does not finish without an AI key. Pick Anthropic, OpenAI, Google, Grok or Kimi and paste a key of your own. The agents run on it, under the daily and monthly ceilings the same screen sets.
- **The agents cannot use a Harbor backend.** LinkedGrow calls its five providers at their own addresses and has no configurable base URL, so there is no Ollama or llama.cpp cross-file for this service and pointing it at one is not possible today. This is the one integration a Harbor user reasonably expects and does not get.
- LinkedGrow starts with two auxiliary containers: `linkedgrow-db` (libSQL) and `linkedgrow-worker` (the browser side). The worker waits for the app's health check, because the app applies the migrations and writes the secrets both containers read.
- Connecting a LinkedIn account asks for that account's own credentials and, for anything beyond a look around, an address reserved for it. Read the project's documentation before pointing it at an account that matters to you.

#### Configuration

##### Environment Variables

Following options can be set via [`harbor config`](./3.-Harbor-CLI-Reference.md#harbor-config) or in `services/linkedgrow/override.env`:

```bash
# Web UI port
HARBOR_LINKEDGROW_HOST_PORT 35080

# Images and tag. The worker image carries Chrome, so it is the large one
HARBOR_LINKEDGROW_IMAGE ghcr.io/digihold/linkedgrow
HARBOR_LINKEDGROW_WORKER_IMAGE ghcr.io/digihold/linkedgrow-worker
HARBOR_LINKEDGROW_VERSION latest
HARBOR_LINKEDGROW_DB_IMAGE ghcr.io/tursodatabase/libsql-server
HARBOR_LINKEDGROW_DB_VERSION latest


# Empty means the app answers on whatever address the request arrived on.
# Set it behind a reverse proxy, because the links in its emails use it
HARBOR_LINKEDGROW_PUBLIC_URL ""

# Generated into the config workspace on the first start and read back after,
# so empty is the working default
HARBOR_LINKEDGROW_AUTH_SECRET ""
HARBOR_LINKEDGROW_ENCRYPTION_KEY ""

# Concurrent LinkedIn browsers the worker may open
HARBOR_LINKEDGROW_WORKER_SLOTS 2

# Shared memory for the worker's Chrome processes
HARBOR_LINKEDGROW_SHM_SIZE 2gb
```

##### Resources

The worker is where the cost sits, and `HARBOR_LINKEDGROW_WORKER_SLOTS` is the dial. Two slots fit in about 4GB of RAM and twelve want 16GB, so raise it only on a machine that has the room. `HARBOR_LINKEDGROW_SHM_SIZE` exists because Chrome crashes on a full `/dev/shm` and Docker's 64m default is not enough for it.

##### Secrets

`AUTH_SECRET` and `ENCRYPTION_KEY` are written into `secrets.env` inside the `linkedgrow-config` volume on the first start, and read back on every later one. Back that volume up with the database. `ENCRYPTION_KEY` is what every stored LinkedIn password, 2FA secret and API key is encrypted with, so restoring a backup without the original value leaves all of them unreadable. Setting `HARBOR_LINKEDGROW_ENCRYPTION_KEY` overrides the file, which is how you put the original back; it has to be exactly 64 hex characters or the app refuses to boot and says so.

##### Volumes

Both LinkedGrow images run as uid 10001 and refuse to start on a directory they cannot write, so this service uses named volumes rather than a workspace bind mount: Docker gives a fresh named volume the image's own ownership, while a bind mount target is created root owned and cannot be fixed portably from a sidecar.

- `linkedgrow-config` the generated secrets, mounted read only into the worker
- `linkedgrow-uploads` media attached to posts
- `linkedgrow-profiles` one persistent Chrome profile per connected LinkedIn account
- `linkedgrow-db-data` the libSQL database file

Reach any of them with `docker volume inspect harbor_linkedgrow-config` and the equivalent for the others.

#### Troubleshooting

```bash
harbor logs linkedgrow
harbor logs linkedgrow-worker
```

**The app exits complaining about ENCRYPTION_KEY.** `HARBOR_LINKEDGROW_ENCRYPTION_KEY` is set to something that is not 64 hex characters. Either put the original value back or clear it and let the instance generate one.

**The app exits saying a directory is not writable by uid 10001.** Something replaced a named volume with a bind mount, in `docker-compose.override.yml` or by hand. Either put the named volume back or chown the directory to 10001 on the host, which is what the error message suggests.

**The worker logs "waiting for the app".** Normal on a cold start: it blocks until the app answers its health check, which happens once the migrations finish.

**A browser action fails after LinkedIn changes its interface.** Selector maintenance is the standing cost of driving a real browser. Check the project's issues before assuming the install is broken.

#### Links

- [GitHub Repository](https://github.com/DigiHold/LinkedGrow)
- [Self hosting documentation](https://github.com/DigiHold/LinkedGrow/tree/main/src/content/docs/self-hosting)
- [Hosted version](https://linkedgrow.ai)
Binary file added docs/harbor-linkedgrow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions profiles/default.env
Original file line number Diff line number Diff line change
Expand Up @@ -1865,6 +1865,27 @@ HARBOR_LINKWARDEN_ALLOW_PRIVATE_NETWORK_ACCESS="true"
HARBOR_LINKWARDEN_ARCHIVE_TAKE_COUNT=5
HARBOR_LINKWARDEN_BROWSER_TIMEOUT=5

# LinkedGrow
HARBOR_LINKEDGROW_HOST_PORT=35080
HARBOR_LINKEDGROW_IMAGE="ghcr.io/digihold/linkedgrow"
HARBOR_LINKEDGROW_WORKER_IMAGE="ghcr.io/digihold/linkedgrow-worker"
HARBOR_LINKEDGROW_VERSION="latest"
HARBOR_LINKEDGROW_DB_IMAGE="ghcr.io/tursodatabase/libsql-server"
HARBOR_LINKEDGROW_DB_VERSION="latest"
# Leave empty and the app answers on whatever address the request arrived on.
# Set it behind a reverse proxy, because the links in its emails use it.
HARBOR_LINKEDGROW_PUBLIC_URL=""
# Both are generated into the config workspace on the first start and read back
# on every later one, so empty is the working default. Set ENCRYPTION_KEY only
# to restore a backup, with the exact 64 hex characters it was written with: a
# different value makes every stored credential in that backup unreadable.
HARBOR_LINKEDGROW_AUTH_SECRET=""
HARBOR_LINKEDGROW_ENCRYPTION_KEY=""
# Concurrent LinkedIn browsers the worker may open. 2 fit 4GB of RAM, 12 need 16GB
HARBOR_LINKEDGROW_WORKER_SLOTS=2
# Chrome dies on a full /dev/shm; the default 64m is not enough for it
HARBOR_LINKEDGROW_SHM_SIZE="2gb"

# Whishper
HARBOR_WHISHPER_HOST_PORT=35060
HARBOR_WHISHPER_PUBLIC_URL=""
Expand Down
95 changes: 95 additions & 0 deletions services/compose.linkedgrow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
services:
linkedgrow:
container_name: ${HARBOR_CONTAINER_PREFIX}.linkedgrow
image: ${HARBOR_LINKEDGROW_IMAGE}:${HARBOR_LINKEDGROW_VERSION}
# Harbor omits restart policies, but this service owns a queue that the
# worker drains: the app applies migrations at startup and the worker
# blocks on its health check, so an app that stays down strands every
# queued action. Same reasoning as linkwarden and windmill.
restart: unless-stopped
env_file:
- ./.env
- ./services/linkedgrow/override.env
ports:
- ${HARBOR_LINKEDGROW_HOST_PORT}:3000
environment:
# Empty is the supported value: the app then answers on whatever address
# the request arrived on, which is what a plain harbor up wants. Set it
# behind a reverse proxy, because the links in its emails use it.
- APP_URL=${HARBOR_LINKEDGROW_PUBLIC_URL}
- AUTH_SECRET=${HARBOR_LINKEDGROW_AUTH_SECRET}
# 64 hex characters, validated on boot. Changing it makes every stored
# LinkedIn password, 2FA secret and API key unreadable, so it belongs in
# the backup set with the database.
- ENCRYPTION_KEY=${HARBOR_LINKEDGROW_ENCRYPTION_KEY}
- LINKEDGROW_EDITION=self-hosted
- TURSO_DATABASE_URL=http://linkedgrow-db:8080
- TURSO_AUTH_TOKEN=
# Named volumes rather than a workspace bind mount, because both images run
# as uid 10001 and refuse to start on a directory they cannot write. Docker
# gives a fresh named volume the image's own ownership, while a bind mount
# target is created root-owned and cannot be fixed portably from a sidecar.
volumes:
- linkedgrow-config:/data/config
- linkedgrow-uploads:/data/uploads
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 10s
timeout: 5s
retries: 30
start_period: 60s
depends_on:
- linkedgrow-db
networks:
- harbor-network

# One real Chrome per connected LinkedIn account, under Xvfb, because
# LinkedIn has no API for reading a feed or sending a message. This is the
# memory cost of the service: HARBOR_LINKEDGROW_WORKER_SLOTS caps concurrent
# browsers, 2 fit 4GB of RAM and 12 need 16GB.
linkedgrow-worker:
container_name: ${HARBOR_CONTAINER_PREFIX}.linkedgrow-worker
image: ${HARBOR_LINKEDGROW_WORKER_IMAGE}:${HARBOR_LINKEDGROW_VERSION}
restart: unless-stopped
env_file:
- ./.env
- ./services/linkedgrow/override.env
shm_size: ${HARBOR_LINKEDGROW_SHM_SIZE}
environment:
- APP_INTERNAL_URL=http://linkedgrow:3000
- ENCRYPTION_KEY=${HARBOR_LINKEDGROW_ENCRYPTION_KEY}
- LINKEDGROW_EDITION=self-hosted
- TURSO_DATABASE_URL=http://linkedgrow-db:8080
- TURSO_AUTH_TOKEN=
- WORKER_ENV=production
- WORKER_SLOTS=${HARBOR_LINKEDGROW_WORKER_SLOTS}
volumes:
# Read only: the app owns the secrets file and always starts first
- linkedgrow-config:/data/config:ro
- linkedgrow-profiles:/data/profiles
- linkedgrow-uploads:/data/uploads
depends_on:
linkedgrow:
condition: service_healthy
networks:
- harbor-network

linkedgrow-db:
container_name: ${HARBOR_CONTAINER_PREFIX}.linkedgrow-db
image: ${HARBOR_LINKEDGROW_DB_IMAGE}:${HARBOR_LINKEDGROW_DB_VERSION}
restart: unless-stopped
env_file:
- ./.env
environment:
- SQLD_NODE=primary
- SQLD_DB_PATH=/var/lib/sqld/iku.db
volumes:
- linkedgrow-db-data:/var/lib/sqld
networks:
- harbor-network

volumes:
linkedgrow-config:
linkedgrow-uploads:
linkedgrow-profiles:
linkedgrow-db-data:
14 changes: 14 additions & 0 deletions services/compose.x.traefik.linkedgrow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# This file is generated by seed-traefik.ts script,
# any updates will be overwritten.
services:
linkedgrow:
labels:
- "traefik.enable=true"
- "traefik.http.routers.linkedgrow.rule=Host(`linkedgrow.${HARBOR_TRAEFIK_DOMAIN}`)"
- "traefik.http.services.linkedgrow.loadbalancer.server.port=3000"
- "traefik.http.routers.linkedgrow.entrypoints=websecure"
- "traefik.http.routers.linkedgrow.tls=true"
- "traefik.http.routers.linkedgrow.service=linkedgrow"

networks:
- traefik-public
1 change: 1 addition & 0 deletions services/linkedgrow/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# nothing persistent lands here; the service uses named volumes
3 changes: 3 additions & 0 deletions services/linkedgrow/override.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# This file can be used for additional environment variables
# specifically for the 'linkedgrow' service.
# You can also use the "harbor env" command to set these variables.