Outreach automation is useful precisely because it is easy to misuse.

That is the tension with GrowChief .

It is an open-source social media automation and outreach tool for running workflows against social accounts, but it sits in a category where platform terms, account health, consent, spam, and brand risk matter.

Used carefully, it can be a self-hosted control plane for lead workflows, account scheduling, enrichment lookups, and API-driven automations.

Used carelessly, it can get accounts restricted and annoy real people.

Treat it like infrastructure for careful outreach, not a spam cannon.

What is GrowChief?

GrowChief is an AGPL-licensed automation platform for social outreach workflows.

The project positions itself as an open-source alternative to hosted outreach tools such as PhantomBuster, Expandi, Zopto, LinkedIn Helper, and Meet Alfred.

The practical idea is familiar: connect accounts, define workflows, feed leads into those workflows, and let the system execute browser-like actions on a schedule.

The interesting part for self-hosters is the implementation shape:

  • A React and Vite frontend.
  • A NestJS backend.
  • A separate orchestrator worker.
  • PostgreSQL through Prisma.
  • Temporal for workflow execution.
  • Playwright/Patchright-style browser automation.
  • Nginx and PM2 inside the published container image.
  • Local uploads or provider-backed storage.
  • Optional enrichment, proxy, email, OAuth, billing, and LLM configuration through environment variables.

GrowChief is also designed to be called from automation tools.

If you already use n8n, Make, Zapier, or custom scripts for lead routing, GrowChief is the component that can sit behind those flows and execute the outreach side.

Why Self-Host It?

Self-hosting GrowChief makes sense when you want the outreach logic close to your own infrastructure.

You get direct control over the database, logs, proxy settings, environment variables, storage backend, and workflow engine.

That is especially useful if the rest of your stack is already self-hosted: Mautic for marketing automation, listmonk for newsletters, n8n for glue workflows, and perhaps a CRM or internal lead database.

The tradeoff is operational responsibility.

Browser automation is heavier than a normal API app. Each connected account can consume memory, network, proxy capacity, and platform risk budget.

GrowChief’s own README warns that social automation may violate platform terms and can result in bans, so the sober default is to run small, rate-limited, intentional campaigns.

Docker Compose

I kept the reusable Compose file in the public Home-Lab layout:

Home-Lab GrowChief config

The site snippet uses the same compose file:

services:
  growchief:
    image: ghcr.io/growchief/growchief:${GROWCHIEF_VERSION:-v0.3.0}
    container_name: growchief
    restart: unless-stopped
    command:
      - bash
      - -lc
      - |
        sed -i 's/pnpm dlx prisma db push/pnpm dlx [email protected] db push/' package.json
        nginx && pnpm run pm2
    environment:
      DATABASE_URL: "postgresql://${GROWCHIEF_DB_USER:-growchief}:${GROWCHIEF_DB_PASSWORD:?Set GROWCHIEF_DB_PASSWORD in .env}@growchief-postgres:5432/${GROWCHIEF_DB_NAME:-growchief}"
      STORAGE_PROVIDER: "${STORAGE_PROVIDER:-local}"
      UPLOAD_DIRECTORY: "/uploads"
      NEXT_PUBLIC_UPLOAD_DIRECTORY: "/uploads"
      AUTH_SECRET: "${AUTH_SECRET:?Set AUTH_SECRET in .env}"
      FRONTEND_URL: "${FRONTEND_URL:-http://localhost:5002}"
      VITE_BACKEND_URL: "${VITE_BACKEND_URL:-/api}"
      VITE_PUBLIC_WS: "${VITE_PUBLIC_WS:-http://localhost:5002}"
      TEMPORAL_ADDRESS: "temporal:7233"
      TEMPORAL_NAMESPACE: "${TEMPORAL_NAMESPACE:-default}"
      GOOGLE_CLIENT_ID: "${GOOGLE_CLIENT_ID:-}"
      GOOGLE_CLIENT_SECRET: "${GOOGLE_CLIENT_SECRET:-}"
      APOLLO_API_KEY: "${APOLLO_API_KEY:-}"
      DATAGMA_API_KEY: "${DATAGMA_API_KEY:-}"
      BRIGHTDATA_CUSTOMER: "${BRIGHTDATA_CUSTOMER:-}"
      BRIGHTDATA_API_KEY: "${BRIGHTDATA_API_KEY:-}"
      OPENAI_KEY: "${OPENAI_KEY:-}"
      DISABLE_REGISTRATION: "${DISABLE_REGISTRATION:-}"
    volumes:
      - growchief-config:/config/
      - growchief-uploads:/uploads/
    ports:
      - "${GROWCHIEF_HTTP_PORT:-5002}:5000"
    networks:
      - growchief-network
      - temporal-network
    depends_on:
      growchief-postgres:
        condition: service_healthy
      temporal:
        condition: service_started

  growchief-postgres:
    image: postgres:16
    container_name: growchief-postgres
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: "${GROWCHIEF_DB_PASSWORD:?Set GROWCHIEF_DB_PASSWORD in .env}"
      POSTGRES_USER: "${GROWCHIEF_DB_USER:-growchief}"
      POSTGRES_DB: "${GROWCHIEF_DB_NAME:-growchief}"
    volumes:
      - postgres-volume:/var/lib/postgresql/data
    networks:
      - growchief-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${GROWCHIEF_DB_USER:-growchief} -d ${GROWCHIEF_DB_NAME:-growchief}"]
      interval: 10s
      timeout: 3s
      retries: 5

  temporal-elasticsearch:
    image: elasticsearch:7.17.27
    container_name: temporal-elasticsearch
    restart: unless-stopped
    environment:
      cluster.routing.allocation.disk.threshold_enabled: "true"
      cluster.routing.allocation.disk.watermark.low: "512mb"
      cluster.routing.allocation.disk.watermark.high: "256mb"
      cluster.routing.allocation.disk.watermark.flood_stage: "128mb"
      discovery.type: "single-node"
      ES_JAVA_OPTS: "-Xms256m -Xmx256m"
      xpack.security.enabled: "false"
    networks:
      - temporal-network
    expose:
      - "9200"
    volumes:
      - temporal-elasticsearch-data:/usr/share/elasticsearch/data

  temporal-postgresql:
    image: postgres:16
    container_name: temporal-postgresql
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: "${TEMPORAL_POSTGRES_PASSWORD:?Set TEMPORAL_POSTGRES_PASSWORD in .env}"
      POSTGRES_USER: "${TEMPORAL_POSTGRES_USER:-temporal}"
    networks:
      - temporal-network
    expose:
      - "5432"
    volumes:
      - temporal-postgresql-data:/var/lib/postgresql/data

  temporal:
    image: temporalio/auto-setup:1.28.1
    container_name: temporal
    restart: unless-stopped
    depends_on:
      - temporal-postgresql
      - temporal-elasticsearch
    environment:
      DB: "postgres12"
      DB_PORT: "5432"
      POSTGRES_USER: "${TEMPORAL_POSTGRES_USER:-temporal}"
      POSTGRES_PWD: "${TEMPORAL_POSTGRES_PASSWORD:?Set TEMPORAL_POSTGRES_PASSWORD in .env}"
      POSTGRES_SEEDS: "temporal-postgresql"
      DYNAMIC_CONFIG_FILE_PATH: "config/dynamicconfig/development-sql.yaml"
      ENABLE_ES: "true"
      ES_SEEDS: "temporal-elasticsearch"
      ES_VERSION: "v7"
      TEMPORAL_NAMESPACE: "${TEMPORAL_NAMESPACE:-default}"
    networks:
      - temporal-network
    ports:
      - "${TEMPORAL_GRPC_PORT:-7233}:7233"
    volumes:
      - ./dynamicconfig:/etc/temporal/config/dynamicconfig:ro

  temporal-admin-tools:
    image: temporalio/admin-tools:1.28.1-tctl-1.18.4-cli-1.4.1
    container_name: temporal-admin-tools
    restart: unless-stopped
    environment:
      TEMPORAL_ADDRESS: "temporal:7233"
      TEMPORAL_CLI_ADDRESS: "temporal:7233"
    networks:
      - temporal-network
    stdin_open: true
    depends_on:
      - temporal
    tty: true

  temporal-ui:
    image: temporalio/ui:2.34.0
    container_name: temporal-ui
    restart: unless-stopped
    environment:
      TEMPORAL_ADDRESS: "temporal:7233"
      TEMPORAL_CORS_ORIGINS: "${TEMPORAL_CORS_ORIGINS:-http://127.0.0.1:3000,http://localhost:5002}"
    networks:
      - temporal-network
    ports:
      - "${TEMPORAL_UI_PORT:-8080}:8080"

volumes:
  postgres-volume:
  growchief-config:
  growchief-uploads:
  temporal-elasticsearch-data:
  temporal-postgresql-data:

networks:
  growchief-network:
  temporal-network:
    driver: bridge

Create the .env file:

cp .env.sample .env

Then edit the required secrets:

GROWCHIEF_DB_PASSWORD=replace-me-growchief-db-password
AUTH_SECRET=replace-me-long-random-auth-secret
TEMPORAL_POSTGRES_PASSWORD=replace-me-temporal-db-password

Start the stack:

docker compose up -d

Open GrowChief:

http://localhost:5002

alt text

Open the Temporal UI:

http://localhost:8080

Do not expose the Temporal UI publicly.

alt text

Keep it on a private network, behind a VPN, or behind strong access controls.

Field Note: Local Docker Trial

I tested GrowChief from the Home-Lab compose on this machine on 2026-08-29.

The initial upstream v0.3.0 image did not fully start for me.

The frontend HTML returned 200 OK, but the app process restarted because the image boot script runs:

pnpm dlx prisma db push --schema ./schema.prisma

That command currently resolves a newer Prisma CLI where db push is not registered, so the boot log ended with:

No command registered for `push`, did you mean `update`?

The Compose snippet above includes a narrow startup workaround: it patches the container’s package.json at boot so GrowChief downloads [email protected], matching the Prisma line used by the repository I inspected.

With that workaround, the stack stayed up:

  • growchief served the UI on http://localhost:5002.
  • growchief-postgres reported healthy.
  • temporal, temporal-postgresql, temporal-elasticsearch, temporal-admin-tools, and temporal-ui started.
  • The orchestrator worker logged Worker state changed ... RUNNING.
  • The NestJS backend logged Nest application successfully started and listening on port 3000.

This is a smoke test, not a production benchmark.

I verified boot, HTTP serving, schema setup, backend startup, Temporal connectivity, and worker startup.

Configuration Notes

The minimum local setup needs:

  • PostgreSQL credentials for GrowChief.
  • A strong AUTH_SECRET.
  • PostgreSQL credentials for Temporal.
  • FRONTEND_URL and websocket URL values that match your public URL.
  • Persistent Docker volumes for app data, uploads, GrowChief config, Temporal PostgreSQL, and Temporal Elasticsearch.

Optional variables unlock integrations:

  • GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET for Google OAuth.
  • APOLLO_API_KEY, DATAGMA_API_KEY, and Bright Data variables for enrichment and proxy flows.
  • OPENAI_KEY for LLM-backed helper behavior where the app uses it.
  • Provider storage variables if you move uploads away from local disk.

For a public deployment, put GrowChief behind HTTPS. The auth code uses secure cookie behavior, so a real domain with TLS is the path I would use before inviting users or connecting accounts.

Where GrowChief Fits

GrowChief does not replace every marketing tool.

Mautic is broader marketing automation: contacts, segments, forms, landing pages, email campaigns, tracking, reports, and lead scoring.

listmonk is a focused mailing-list and newsletter system.

n8n is the workflow glue that can move leads between APIs, databases, webhooks, and internal systems.

GrowChief is closer to the outreach execution layer.

It is the place where a lead workflow becomes scheduled browser/account activity, with rate limits, working hours, proxies, and social-account state.

If your goal is email newsletters, start with listmonk.

If your goal is multi-channel marketing operations, look at Mautic.

If your goal is social outreach execution from API-triggered workflows, GrowChief is the more relevant experiment.

Operational Cautions

Keep these constraints in mind:

  • Start with one or two accounts. Browser automation is heavier than normal HTTP APIs.
  • Use working hours and conservative delays.
  • Do not import cold scraped lists and blast people.
  • Use proxies only when you understand the account-risk tradeoffs.
  • Keep Temporal private.
  • Keep provider API keys out of Git.
  • Back up both GrowChief PostgreSQL and Temporal PostgreSQL if workflows matter.
  • Test account connection and login behavior on a disposable environment before putting a real account at risk.

Conclusion

GrowChief is a serious self-hosted outreach automation project, but it belongs in the “handle with care” category.

alt text

The architecture is more substantial than a small dashboard: React frontend, NestJS backend, PostgreSQL, Prisma, Temporal, browser automation, and a worker runtime.

That complexity makes sense for scheduled outreach workflows, but it also means the Docker stack deserves proper secrets, HTTPS, private Temporal access, backups, and conservative usage.

The local Docker trial was useful because it caught a real packaging issue in the current image.

With the Prisma CLI pinned at startup, the stack booted cleanly and the worker reached a running state.

FAQ