Skip to content
Merged
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
29 changes: 26 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
# -----------------------------------------------------------------
# 1. Core Application & Database
# -----------------------------------------------------------------
# Port on the host machine to expose the application (default: 3000)
PORT=3000
# Port on the host machine to expose the application (default: 5050)
PORT=5050

# MongoDB Connection String (pre-configured with replica set for transactions)
DATABASE_URL="mongodb://mongo:27017/instagram?replicaSet=rs0&directConnection=true"
Expand All @@ -28,7 +28,30 @@ CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=

# -----------------------------------------------------------------
# 3. Logging & Telemetry (Optional)
# 3. AI Vector Search - Qdrant (Optional, Beta - OFF by default)
# -----------------------------------------------------------------
# Leave these commented out and search stays disabled: the app shows a
# "not enabled" notice on the Search page and never downloads any models.
#
# To turn it on you need a running Qdrant service. With docker compose:
# docker compose -f docker-compose.yml -f docker-compose.search.yml up -d
# then uncomment QDRANT_URL below.
#
# Setting QDRANT_URL is the switch — nothing else enables or disables search.
# See docs/features/ai-vector-search.md
#
# QDRANT_URL="http://qdrant:6333"
#
# Host port for the Qdrant API & Dashboard (default: 6335, loopback only).
# Dashboard UI: http://localhost:6335/dashboard
# QDRANT_PORT=6335
#
# Required if you enable Qdrant's own auth. The app refuses to send this over
# plaintext HTTP to a non-local host.
# QDRANT_API_KEY=

# -----------------------------------------------------------------
# 4. Logging & Telemetry (Optional)
# -----------------------------------------------------------------
# Log level: fatal | error | warn | info | debug | trace (default: info)
LOG_LEVEL=info
Expand Down
17 changes: 14 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,24 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js 20
- name: Set up Node.js 24
uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
node-version: 24

# node_modules is ~1.7 GB across 59k files, dominated by onnxruntime
# (342 MB) and tfjs (277 MB). Caching the tree itself skips npm ci
# outright; setup-node's `cache: npm` only skipped the download, leaving
# the extraction — the actual cost — to run on every single job.
- name: Restore node_modules
id: node-modules
uses: actions/cache@v4
with:
path: node_modules
key: node-modules-${{ runner.os }}-node24-${{ hashFiles('package-lock.json') }}

- name: Install dependencies
if: steps.node-modules.outputs.cache-hit != 'true'
run: npm ci --ignore-scripts

- name: Generate Prisma Client
Expand Down
31 changes: 15 additions & 16 deletions .github/workflows/pr-beta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ jobs:
with:
fetch-depth: 0

- name: Set up Node.js 20
- name: Set up Node.js 24
uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
node-version: 24
# No npm install in this workflow — only the version scripts run here,
# and they use node builtins.

- name: Calculate Beta Version
id: semver
Expand All @@ -36,16 +37,9 @@ jobs:
GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
run: node .github/scripts/calculate-version.mjs --mode=beta

- name: Install dependencies
run: npm ci --ignore-scripts

- name: Generate Prisma Client
run: npx prisma generate

- name: Verify Lint & TypeScript
run: |
npm run lint
npx tsc --noEmit
# Lint, typecheck and build verification run in ci.yml on this same
# pull_request trigger — repeating them here meant a third full npm ci
# (~1.7 GB) per PR for no extra signal.

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
Expand All @@ -71,8 +65,13 @@ jobs:
${{ env.REGISTRY }}/${{ steps.semver.outputs.image_name }}:${{ steps.semver.outputs.tag }}
build-args: |
APP_VERSION=${{ steps.semver.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Separate scopes: sharing the default one meant beta and release
# exports kept overwriting each other's manifest. Beta also reads the
# release scope so a PR starts warm from the last master build.
cache-from: |
type=gha,scope=beta
type=gha,scope=release
cache-to: type=gha,mode=max,scope=beta

- name: Beta Release Summary
run: |
Expand All @@ -84,7 +83,7 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
echo "# Pull and run this beta container locally:" >> $GITHUB_STEP_SUMMARY
echo "docker run -d -p 3000:3000 --name test-beta \\" >> $GITHUB_STEP_SUMMARY
echo "docker run -d -p 5050:3000 --name test-beta \\" >> $GITHUB_STEP_SUMMARY
echo " -e DATABASE_URL=\"mongodb://...\" \\" >> $GITHUB_STEP_SUMMARY
echo " ${{ env.REGISTRY }}/${{ steps.semver.outputs.image_name }}:beta-pr-${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
Expand Down
27 changes: 9 additions & 18 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ jobs:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Node.js 20
- name: Set up Node.js 24
uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
node-version: 24
# No npm install in this workflow — only the version scripts run here,
# and they use node builtins.

- name: Calculate Release Version
id: semver
Expand All @@ -65,19 +66,9 @@ jobs:
IS_TAG_PUSH: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: node .github/scripts/calculate-version.mjs --mode=release

- name: Install dependencies
if: steps.semver.outputs.should_release == 'true'
run: npm ci --ignore-scripts

- name: Generate Prisma Client
if: steps.semver.outputs.should_release == 'true'
run: npx prisma generate

- name: Verify Lint & TypeScript
if: steps.semver.outputs.should_release == 'true'
run: |
npm run lint
npx tsc --noEmit
# Lint and typecheck already ran in ci.yml for this commit, and the Docker
# build below runs `npm run build` — a broken tree cannot publish an image.
# Installing here just to re-run them cost a full ~1.7 GB npm ci.

- name: Update package.json Version
if: steps.semver.outputs.should_release == 'true' && steps.semver.outputs.is_tag_push == 'false'
Expand Down Expand Up @@ -122,8 +113,8 @@ jobs:
${{ env.REGISTRY }}/${{ steps.semver.outputs.image_name }}:${{ github.sha }}
build-args: |
APP_VERSION=${{ steps.semver.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=gha,scope=release
cache-to: type=gha,mode=max,scope=release

- name: Create GitHub Release with Auto-Changelog
if: steps.semver.outputs.should_release == 'true' && steps.semver.outputs.dry_run == 'false'
Expand Down
21 changes: 19 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1

# ── Builder ───────────────────────────────────────────────────
FROM node:20-bookworm-slim AS builder
FROM node:24-bookworm-slim AS builder
WORKDIR /app

# OpenSSL is required by the Prisma query engine
Expand All @@ -12,9 +12,19 @@ COPY package.json package-lock.json ./
# --ignore-scripts skips native builds and the postinstall prisma generate (run explicitly below).
RUN npm ci --ignore-scripts

# onnxruntime-node ships prebuilt binaries for win32/darwin/linux in one tarball.
# Only linux is reachable from this image, and the other two (~159 MB) otherwise
# ride through npm prune, the runner COPY, and the registry layer cache.
RUN rm -rf node_modules/onnxruntime-node/bin/napi-v*/win32 node_modules/onnxruntime-node/bin/napi-v*/darwin

COPY prisma ./prisma
RUN npx prisma generate

# CLIP and face-recognition weights are deliberately NOT baked in. Vector search
# is an optional beta add-on, so shipping ~600 MB of weights to every install
# would tax the majority that never enables it. They are downloaded on the first
# index run instead, into the model_cache volume from docker-compose.search.yml.

COPY . .

ARG APP_VERSION=1.0.1
Expand All @@ -25,7 +35,7 @@ RUN npm run build
RUN npm prune --omit=dev

# ── Runner ────────────────────────────────────────────────────
FROM node:20-bookworm-slim AS runner
FROM node:24-bookworm-slim AS runner
WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates curl \
Expand All @@ -51,6 +61,13 @@ COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma

# Transformers.js writes downloaded weights here. The path must exist and be
# owned by nextjs in the image: Docker seeds a fresh named volume from the image
# directory, so without this the search add-on's model_cache volume would come
# up root-owned and the non-root app could not write to it.
RUN mkdir -p /app/node_modules/@huggingface/transformers/.cache \
&& chown -R nextjs:nodejs /app/node_modules/@huggingface/transformers/.cache

USER nextjs
EXPOSE 3000

Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ services:
pull_policy: always
restart: unless-stopped
ports:
- "3000:3000"
- "5050:3000"
environment:
- DATABASE_URL=mongodb://mongo:27017/instagram?replicaSet=rs0&directConnection=true
depends_on:
Expand Down Expand Up @@ -90,7 +90,18 @@ volumes:
docker compose up -d
```

🎉 Open **`http://localhost:3000`** in your browser and complete the 60-second onboarding wizard!
🎉 Open **`http://localhost:5050`** in your browser and complete the 60-second onboarding wizard!

> **Optional: AI Vector Search (Beta).** Semantic prompt, image and face search is
> **not enabled by default** — it adds a Qdrant service and downloads ~600 MB of
> model weights on first index. Everything else works fine without it. To opt in,
> grab [`docker-compose.search.yml`](docker-compose.search.yml) and run:
>
> ```bash
> docker compose -f docker-compose.yml -f docker-compose.search.yml up -d
> ```
>
> Full setup and trade-offs: [AI Vector Search](docs/features/ai-vector-search.md).

---

Expand Down Expand Up @@ -129,6 +140,7 @@ This project follows an automated semantic CI/CD versioning lifecycle directly c

| Feature | Description |
| :--- | :--- |
| 🔍 **Multimodal Vector Search** *(Beta, optional)* | Natural-language prompt search (CLIP), visual similarity image search, and facial recognition search with Qdrant. **Off by default** — [how to enable](docs/features/ai-vector-search.md). |
| 🧙‍♂️ **Interactive Onboarding** | Built-in setup wizard with real-time Instagram cookie testing and avatar preview. |
| 👥 **Multi-Profile Support** | Manage multiple Instagram accounts with separate sessions and isolated bookmarks. |
| 📈 **Account Timelines** | Automatically records username changes, bio updates, verification changes, and lost accounts. |
Expand Down Expand Up @@ -161,7 +173,9 @@ When running via Docker Compose, **zero environment variables are required**. Op

| Variable | Default | Description |
| :--- | :--- | :--- |
| `PORT` | `3000` | Port exposed on host |
| `PORT` | `5050` | Port exposed on host for web app |
| `QDRANT_URL` | _(unset)_ | **Optional.** Setting this switches on the beta vector search. Unset = disabled |
| `QDRANT_PORT` | `6335` | Host port for the Qdrant API & Dashboard, only used with search enabled |
| `DATABASE_URL` | `mongodb://mongo:27017/instagram?replicaSet=rs0&directConnection=true` | MongoDB connection string (replica set enabled) |
| `CLOUDINARY_CLOUD_NAME` | `""` | Optional Cloudinary cloud name for permanent media |
| `CLOUDINARY_API_KEY` | `""` | Optional Cloudinary API key |
Expand Down
4 changes: 4 additions & 0 deletions coolify-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ services:
restart: unless-stopped
expose:
- "3000"
ports:
- "${PORT:-5050}:3000"
environment:
- DATABASE_URL=mongodb://mongo:27017/instagram?replicaSet=rs0&directConnection=true
- NODE_ENV=production
- PORT=3000
- HOSTNAME=0.0.0.0
# AI Vector Search is an optional beta add-on, off by default.
# To enable it, see docs/features/ai-vector-search.md
# Optional Cloudinary CDN Configuration
- CLOUDINARY_CLOUD_NAME=${CLOUDINARY_CLOUD_NAME:-}
- CLOUDINARY_API_KEY=${CLOUDINARY_API_KEY:-}
Expand Down
55 changes: 55 additions & 0 deletions docker-compose.search.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Optional add-on: AI Vector Search (Beta)
#
# Search is NOT part of the default deployment. It adds a Qdrant service and
# makes the app download ~600 MB of CLIP and face-recognition model weights on
# the first index run, which most archives do not need.
#
# Enable it by layering this file over the base compose:
# docker compose -f docker-compose.yml -f docker-compose.search.yml up -d
#
# Disable it again by dropping the second -f and recreating:
# docker compose -f docker-compose.yml up -d --remove-orphans
#
# See docs/features/ai-vector-search.md

services:
app:
environment:
# Presence of QDRANT_URL is what switches the feature on.
- QDRANT_URL=http://qdrant:6333
- QDRANT_API_KEY=${QDRANT_API_KEY:-}
- QDRANT_PORT=${QDRANT_PORT:-6335}
volumes:
# Model weights are downloaded once and cached here, so restarts and
# image upgrades do not re-fetch them.
- model_cache:/app/node_modules/@huggingface/transformers/.cache
depends_on:
qdrant:
condition: service_healthy

qdrant:
image: qdrant/qdrant:v1.13.4
container_name: instagram_saved_posts_qdrant
restart: unless-stopped
ports:
# Bound to loopback: the API has no authentication unless you set
# QDRANT__SERVICE__API_KEY, so it must never be published on 0.0.0.0.
- "127.0.0.1:${QDRANT_PORT:-6335}:6333"
volumes:
- qdrant_data:/qdrant/storage
ulimits:
nofile:
soft: 65535
hard: 65535
healthcheck:
test: ["CMD-SHELL", "bash -c ': >/dev/tcp/127.0.0.1/6333' || exit 1"]
interval: 5s
timeout: 5s
retries: 10
start_period: 2s

volumes:
qdrant_data:
name: instagram_saved_posts_qdrant_data
model_cache:
name: instagram_saved_posts_model_cache
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ services:
container_name: instagram_saved_posts_app
restart: unless-stopped
ports:
- "${PORT:-3000}:3000"
- "${PORT:-5050}:3000"
environment:
- DATABASE_URL=mongodb://mongo:27017/instagram?replicaSet=rs0&directConnection=true
- NODE_ENV=production
- PORT=3000
- HOSTNAME=0.0.0.0
# AI Vector Search is an optional beta add-on and is off by default.
# To enable: docker compose -f docker-compose.yml -f docker-compose.search.yml up -d
# Optional: Cloudinary for permanent media CDN
- CLOUDINARY_CLOUD_NAME=${CLOUDINARY_CLOUD_NAME:-}
- CLOUDINARY_API_KEY=${CLOUDINARY_API_KEY:-}
Expand Down
8 changes: 8 additions & 0 deletions docs/deployment/coolify.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@ services:
restart: unless-stopped
expose:
- "3000"
ports:
- "${PORT:-5050}:3000"
environment:
- DATABASE_URL=mongodb://mongo:27017/instagram?replicaSet=rs0&directConnection=true
- NODE_ENV=production
- PORT=3000
- HOSTNAME=0.0.0.0
# AI Vector Search is an optional beta add-on, off by default.
# To enable it, see ../features/ai-vector-search.md
- CLOUDINARY_CLOUD_NAME=${CLOUDINARY_CLOUD_NAME:-}
- CLOUDINARY_API_KEY=${CLOUDINARY_API_KEY:-}
- CLOUDINARY_API_SECRET=${CLOUDINARY_API_SECRET:-}
Expand Down Expand Up @@ -73,4 +77,8 @@ volumes:
2. Set the destination port to `3000`.

### Step 4: Deploy
> Want semantic image and face search? It is an optional beta add-on that is off
> by default — see [AI Vector Search](../features/ai-vector-search.md) for the
> extra service and environment variable to add here.

Click **Deploy**. Coolify will orchestrate the containers, provision Traefik routing, issue Let's Encrypt certificates, and make your app accessible securely over HTTPS!
Loading
Loading