Turn your Instagram data export into a private, self-hosted photo gallery on Cloudflare. Built with Astro and deployed as a Cloudflare Worker with R2 storage for media.
- Instagram-style grid — responsive thumbnail grid generated from your export
- Post detail pages — full-size images, video playback, carousels, and captions
- Role-based access — three tiers derived from your export's follower/close-friends lists:
- Close friends — full access including pre-2020 posts and the archive page
- Friends — access to posts from 2020 onward
- Guests — see a "private archive" landing page only
- No passwords — visitors log in with their Instagram username; access is granted based on your exported follower lists
- Full archive page — all posts in one grid (close friends only)
- Media served from R2 — full-size images and video load directly from a public R2 bucket (no Worker proxy)
| Requirement | Notes |
|---|---|
| Node.js | ≥ 22.12 (see package.json → engines) |
| mise (recommended) | From igp/, run mise install to install the pinned Node and Yarn versions |
| Yarn | If you do not use mise, install Yarn per the official install guide (this repo uses Yarn 4; package.json → packageManager pins the version) |
ffmpeg on PATH |
Required by yarn prep to extract video poster frames |
| Instagram data export | Request a download from Instagram (Help Center: request export). Use the JSON format (not HTML), then unzip — see Where to put your Instagram export |
| Cloudflare account | With an R2 bucket for media storage |
If you have not downloaded your data yet, start with Instagram’s instructions: Download a copy of your information.
All tooling assumes a fixed location: a directory named instagram sitting next to the igp project folder (sibling of igp/, not inside it).
| Rule | Detail |
|---|---|
| Folder name | Must be exactly instagram (lowercase). Scripts resolve it as ../instagram from igp/. |
| Parent directory | The parent of instagram/ must also be the parent of igp/. Same level on disk. |
| What goes inside | The contents of Meta’s JSON export: you should have paths like instagram/media/posts/…, instagram/connections/followers_and_following/…, and instagram/your_instagram_activity/media/posts_1.json. |
If Meta’s ZIP unpacks to a single dated folder (e.g. instagram-yourname-2024-01-01-…/), rename that folder to instagram and place it next to igp/, or move its contents into an instagram/ folder you create there. Do not add an extra nesting level so that media/ is missing under instagram/media/.
Example if this repository is checked out as ig-static/:
ig-static/ ← repository root (parent of both folders)
├── instagram/ ← YOUR EXPORT LIVES HERE (sibling of igp/)
│ ├── media/posts/
│ ├── connections/followers_and_following/
│ └── your_instagram_activity/media/posts_1.json
└── igp/ ← this app (you run yarn commands from here)
├── package.json
└── ...
The same layout applies under any root: …/instagram/ and …/igp/ must share the same …/ parent.
From the repository root, enter this project, install the toolchain, then dependencies. Put your export in ../instagram (see above) before yarn prep.
cd igp
mise install # skip if you manage Node/Yarn yourself (see Prerequisites)
yarn install
# Local environment
cp .dev.vars.example .dev.vars
# Edit .dev.vars — set R2_PUBLIC_URL and AUTH_SECRET
# One-time (re-run after export changes)
yarn prep
yarn devOpen localhost:4321 and log in with any username from your followers list.
| Command | Action |
|---|---|
yarn prep |
Generate friends list, thumbnails, and verify assets (run once after export changes) |
yarn dev |
Start local dev server at localhost:4321 |
yarn build |
Verify grid assets, then build to ./dist/ |
yarn preview |
Prep, build, and preview locally |
yarn deploy |
Prep, build, and deploy to Cloudflare |
yarn upload-media |
Upload media files to R2 (see Media Upload) |
yarn exec wrangler r2 bucket create igp-mediaMake the bucket publicly accessible (via R2 custom domain or r2.dev subdomain) and note the public URL.
yarn exec wrangler secret put AUTH_SECRET
# Enter a strong random string — used to sign session cookies
yarn exec wrangler secret put R2_PUBLIC_URL
# Enter your bucket's public URL (e.g. https://media.your-domain.com)yarn upload-media --remote # upload all media
yarn upload-media --remote --skip-existing # only upload new filesyarn deployThe upload-media script reads files from ../instagram (same export location as above) and uploads them to R2 with keys matching the export paths (e.g. media/posts/202301/12345.jpg).
| Flag | Effect |
|---|---|
--remote |
Upload to production R2 (default is local dev bucket) |
--skip-existing |
Skip files that already exist in the bucket |
Use POST_LIMIT=N to limit processing to the N most recent posts (applies to prep, build, and upload).
| Variable | Where | Purpose |
|---|---|---|
AUTH_SECRET |
.dev.vars locally; in production use yarn exec wrangler secret put AUTH_SECRET |
Signs session cookies — any string, keep it secret |
R2_PUBLIC_URL |
.dev.vars locally; in production use yarn exec wrangler secret put R2_PUBLIC_URL |
Public base URL for your R2 bucket |
POST_LIMIT |
Shell env (optional) | Limit to N most recent posts (useful for dev/testing) |
There are no passwords. The login page asks for an Instagram username, which is checked against your exported follower and close-friends lists (generated into src/data/friends.json by yarn prep).
- Usernames in your close friends list →
close_friendrole (full access) - Other followers →
friendrole (2020+ posts only) guestordemo→guestrole (welcome page only)- Unknown usernames → rejected
Sessions are HMAC-signed cookies (30-day expiry).