This is a noob-friendly guide to standing up your own ipfs-gate server. It assumes you can SSH into a Linux box and follow recipes — no programming background needed.
> Status: v0.1 first build. Working software but expect rough edges. Bugs found during real deployment get folded back into the Common Problems section below.
> Reference OS: Ubuntu Server 24.04 LTS (Noble). The recipes are tested on this OS specifically. Other distros (Ubuntu 22.04, Debian 12, etc.) will likely work but might have subtle differences. For reproducibility, stick to Ubuntu 24.04 LTS on a fresh VPS.
> Reference user: This guide assumes you SSH in as root on a fresh VPS (the default for most providers — Vultr, Hetzner, Linode, DigitalOcean, etc.). If you operate as a non-root user with sudo, swap sudo in where shown.
A server that:
- Accepts file uploads from web browsers
- Charges users in CNOOBS (a Hive-Engine token) for hosting
- Stores the files on IPFS (the InterPlanetary File System — a peer-to-peer storage network)
- Hands users back a "CID" (a fingerprint of their file) that they share with whoever they want to give the file to
- Auto-deletes files after 7 days (configurable)
- A fresh Ubuntu 24.04 LTS VPS with at least:
- 2 CPU cores, 2GB RAM, 20GB disk (more disk = more pinning capacity)
- A public IP address and the ability to open ports 80 + 443
- A domain name you control (e.g.
ipfs.v4call.com). It must point at your VPS's IP via an A record. Set this up at your DNS provider before starting — Let's Encrypt needs the domain to resolve. - A dedicated Hive account for ipfs-gate (separate from any v4call account). Create one if needed. Fund it with about 2 HBD for refunds and Resource Credits.
- The active private key for that Hive account (NOT the owner key, NOT the posting key — the active key). You will paste it into the server's
.envfile. Keep it secret.
This guide installs everything on one VPS (kubo, ipfs-gate, nginx). For v0.1 scale this is fine; splitting onto separate boxes can come later.
SSH into the VPS as root:
ssh root@your-vps-ip
Update + install Docker (the official one-liner installer):
apt update && apt upgrade -y curl -fsSL https://get.docker.com | sh
Verify:
docker --version docker compose version
You should see Docker 27.x or newer and Docker Compose v2.x or newer.
(If you'd rather operate as a non-root user, also run usermod -aG docker your-user then log out and back in. The rest of this guide uses root; adjust paths under /root/... to /home/your-user/... as needed.)
We'll deploy from /opt/IPFS-Gate (a standard location for operator-managed services):
cd /opt git clone https://github.com/CompleteNoobs/IPFS-Gate.git cd /opt/IPFS-Gate
Critical permission step. The ipfs-gate container runs as UID 1000 (the node user inside the container). Because we're running docker as root, the host data directories get created owned by root, and the container can't write to them. Pre-create them and chown:
mkdir -p data/kubo data/ipfs-gate data/letsencrypt data/letsencrypt-webroot chown -R 1000:1000 data/
Skipping this leads to SQLITE_CANTOPEN errors in the ipfs-gate logs (see Common Problems #2).
cp .env.example .env nano .env
Fill in at minimum (the only three required fields):
IPFS_GATE_HIVE_ACCOUNT— your dedicated Hive account name (e.g.v4call-ipfs)IPFS_GATE_ACTIVE_KEY— its active private key (starts with5...)ADMIN_KEY— pick a long random string. Generate one withopenssl rand -hex 32from another terminal.
BIND_HOST=0.0.0.0— required for docker; do not change to 127.0.0.1DISK_LIMIT_GB=5— increase as your VPS allowsDEFAULT_TTL_DAYS=7— accepts fractions for testing (e.g.0.001≈ 1.4 min)MAX_FILE_SIZE_MB=10— per-upload capKUBO_DHT_MODE=none— private hosting; CID only servable from your own gateway. Set toclientfor public IPFS network availability
nano nginx/ipfs-gate.conf
Replace every instance of ipfs.example.com with your actual domain (e.g. ipfs.v4call.com).
Leave the HTTPS server block commented out for now — we'll enable it after the cert is in place in Step 6.
Bring up the stack with HTTP only:
docker compose up -d
Wait ~30 seconds for Kubo's first-boot identity generation. Then verify:
docker compose ps docker compose logs ipfs-gate | tail -20
You should see lines like:
[quota] schema_version = 1 [server] kubo backend OK, version 0.41.0, used <N> bytes [sweeper] starting, interval = 60000ms ipfs-gate v0.1 listening on 0.0.0.0:3001 operator account: @your-hive-account payment: 1 CNOOBS per upload (≤10MB, 7-day TTL) CORS origin: *
If you see listening on 127.0.0.1:3001 instead, your .env has the old BIND_HOST. Fix it (Common Problems #6) before proceeding — the cert step needs HTTP serving to work.
Now request the cert from Let's Encrypt:
# Replace ipfs.your-domain.com with your actual domain in both places below. docker run --rm -it \ -v "$PWD/data/letsencrypt:/etc/letsencrypt" \ -v "$PWD/data/letsencrypt-webroot:/var/www/certbot" \ --entrypoint certbot \ certbot/certbot \ certonly --webroot --webroot-path=/var/www/certbot \ -d ipfs.your-domain.com \ --email you@your-domain.com --agree-tos --no-eff-email
On success: Successfully received certificate. The cert files land in data/letsencrypt/live/ipfs.your-domain.com/.
If this fails with a DNS error, double-check that your domain's A record actually points at this VPS's IP (try dig ipfs.your-domain.com from a different machine).
nano nginx/ipfs-gate.conf
Uncomment the entire server { listen 443 ssl; ... } block. Make sure every ipfs.example.com in that block is also replaced with your domain (it should already match what you set in Step 4).
Reload nginx:
docker compose restart nginx docker compose logs nginx | tail -10
From your laptop (NOT the VPS):
curl https://ipfs.your-domain.com/
Expected JSON response:
{"service":"ipfs-gate","version":"0.1.0-dev","operator":"your-hive-account","payment":{"currency":"CNOOBS","amount":"1","max_size_mb":10,"ttl_days":7}}
Test the admin endpoint too (substitute your real ADMIN_KEY):
curl -H 'Authorization: Bearer YOUR_ADMIN_KEY' https://ipfs.your-domain.com/admin/stats
You should see a JSON response with disk, pin, payment, moderation, and kubo stats.
Note: an HTTP request (http://...) returns a 301 redirect to HTTPS. That's expected — nginx forces HTTPS for all real traffic. Use curl -L to follow the redirect or hit https:// directly.
Let's Encrypt certs expire after 90 days. Add a cron job for root:
crontab -e
Add this single line (adjust nothing — uses /opt/IPFS-Gate from this walkthrough):
0 3 * * 1 cd /opt/IPFS-Gate && docker run --rm -v "$PWD/data/letsencrypt:/etc/letsencrypt" -v "$PWD/data/letsencrypt-webroot:/var/www/certbot" --entrypoint certbot certbot/certbot renew --quiet && docker compose restart nginx
Runs every Monday at 03:00 UTC. Certbot only actually renews when within 30 days of expiry — earlier runs are no-ops.
IPFS_GATE_DIR=/opt/IPFS-Gate cd "$IPFS_GATE_DIR" # 1. Back up your config + data BEFORE pulling cp .env .env.bk-$(date +%Y%m%d) cp -r data data.bk-$(date +%Y%m%d) cp nginx/ipfs-gate.conf nginx/ipfs-gate.conf.bk-$(date +%Y%m%d) # 2. Pull latest code (this WILL clobber .env and nginx/ipfs-gate.conf if you've edited them) git fetch --all && git reset --hard origin/main # 3. Restore your operator-specific files cp .env.bk-$(date +%Y%m%d) .env cp nginx/ipfs-gate.conf.bk-$(date +%Y%m%d) nginx/ipfs-gate.conf # 4. Re-apply data ownership (in case new directories appeared) chown -R 1000:1000 data/ # 5. Rebuild and restart (env-change or code-change BOTH need down/up, not restart) docker compose down docker compose build --no-cache docker compose up -d # 6. Watch logs to confirm clean start docker compose logs -f ipfs-gate
Always back up .env, nginx/ipfs-gate.conf, and the data/ directory before any update. A git reset --hard will wipe any operator-edited files in the repo.
All admin endpoints need Authorization: Bearer YOUR_ADMIN_KEY from your .env.
curl -H 'Authorization: Bearer YOUR_ADMIN_KEY' https://ipfs.your-domain.com/admin/stats
Shows disk usage, pin counts, payment counts, recent moderation actions, Kubo health.
curl -H 'Authorization: Bearer YOUR_ADMIN_KEY' \ "https://ipfs.your-domain.com/admin/uploads?account=guest33"
curl -X POST -H 'Authorization: Bearer YOUR_ADMIN_KEY' -H 'Content-Type: application/json' \
-d '{"hive_account":"badguy","reason":"Spam","refund_policy":"none"}' \
https://ipfs.your-domain.com/admin/ban
refund_policy can be "none" (default — banned user forfeits unused TTL) or "prorata" (calculate per-pin remaining-time refund; v0.1 records this intent but does NOT auto-issue the on-chain refund. The operator must transfer manually and then /admin/log-refund).
curl -X POST -H 'Authorization: Bearer YOUR_ADMIN_KEY' -H 'Content-Type: application/json' \
-d '{"hive_account":"badguy"}' \
https://ipfs.your-domain.com/admin/unban
Note: unban does NOT restore content that was unpinned. The user must re-upload + re-pay.
curl -X POST -H 'Authorization: Bearer YOUR_ADMIN_KEY' -H 'Content-Type: application/json' \
-d '{"cid":"bafkreig...","reason":"DMCA #12345"}' \
https://ipfs.your-domain.com/admin/takedown
Adds the CID to the blocklist (can never be re-uploaded) and unpins from Kubo immediately.
curl -H 'Authorization: Bearer YOUR_ADMIN_KEY' https://ipfs.your-domain.com/admin/takedowns > takedowns.json
curl -X POST -H 'Authorization: Bearer YOUR_ADMIN_KEY' -H 'Content-Type: application/json' \
-d '{"takedowns":[{"cid":"bafk1...","reason":"From peer X"}]}' \
https://ipfs.your-domain.com/admin/takedowns/import
curl -H 'Authorization: Bearer YOUR_ADMIN_KEY' \ "https://ipfs.your-domain.com/admin/moderation/log?limit=50"
Append-only record of every moderation action with admin attribution and timestamps.
If a user paid in wrong currency, wrong amount, or the Hive-Engine sidechain hadn't confirmed the transfer at upload time, the payment ends up in the orphan list:
curl -H 'Authorization: Bearer YOUR_ADMIN_KEY' https://ipfs.your-domain.com/admin/orphan-payments
Operator reviews, decides whether to manually refund (out-of-band Hive transfer), and logs the action:
# After manually transferring back to the user on Hive:
curl -X POST -H 'Authorization: Bearer YOUR_ADMIN_KEY' -H 'Content-Type: application/json' \
-d '{"payment_id":88,"refund_tx_id":"ef56gh78...","reason":"Wrong amount"}' \
https://ipfs.your-domain.com/admin/log-refund
ipfs-gate is currently standalone. Each operator runs their own server with their own ban list, their own pricing, their own disk allocation. v0.3+ will add optional Nostr-based discovery and opt-in cross-operator banlist sharing — same architectural pattern as v4call's Nostr federation work.
(This section grows as bugs are found and fixed during development. Ordered by first-boot likelihood.)
- Q: I ran
docker compose upand Kubo doesn't respond for the first ~30 seconds. Is it broken?- A: No. Kubo does a one-time identity generation on first boot. Takes 10–30s on a small VPS. Watch
docker compose logs kuboto see when it's ready. The healthcheck in docker-compose.yml gates ipfs-gate startup on Kubo being healthy.
- A: No. Kubo does a one-time identity generation on first boot. Takes 10–30s on a small VPS. Watch
- Q: My ipfs-gate container won't start. I see one of:
EACCES: permission denied, mkdir '/app/data'SqliteError: unable to open database filewithcode: 'SQLITE_CANTOPEN'- Container in restart loop with errors mentioning
/app/data/ipfs-gate.db
- A: Same root cause: the container runs as UID 1000 (node user), but if you ran
docker compose upas root, Docker created the host./data/directories owned by root. The container can't write there. Fix: chown -R 1000:1000 ./data/
- Confirmed during the v0.1 first-VPS test (2026-05-24). The Dockerfile correctly
chowns/app/dataat build, but the host volume mount overlays that with the host's ownership, which wins. Step 2 of this walkthrough does the chown proactively to avoid hitting this.
- Confirmed during the v0.1 first-VPS test (2026-05-24). The Dockerfile correctly
- Q: Boot logs say
FATAL: IPFS_GATE_HIVE_ACCOUNT not set. Refusing to start.- A: Your
.envis missing or doesn't haveIPFS_GATE_HIVE_ACCOUNT. Copy.env.exampleto.envand fill in the values (Step 3).
- A: Your
- Q: I changed something in
.envand randocker compose restart ipfs-gate, but the change didn't take effect (e.g. logs still show the old BIND_HOST or old TTL).- A:
docker compose restartreuses the existing container (and the env vars baked in at create-time). To pick up.envchanges you must recreate the container: # Or force-recreate just one service:
- A:
- This is parallel to v4call's "
docker compose downrequired before rebuilding" gotcha — different cause (env change vs code change) but same surprise. Confirmed during v0.1 first-VPS test (2026-05-24).
- This is parallel to v4call's "
- Q: My response JSON shows
"ttl_days":0even though I setDEFAULT_TTL_DAYS=0.001for testing fast sweeper expiry. Or pins expire immediately.- A: Pre-v0.1.1 code used
parseIntwhich truncated0.001to0. Fixed in v0.1.1: now usesparseFloat, so fractional days work. If you're on an old binary,git pull+ rebuild. Reference:0.001 day ≈ 1.44 minutes— handy for testing the sweeper.
- A: Pre-v0.1.1 code used
- Q: Nginx returns
502 Bad Gatewaywhen I curl my domain. The ipfs-gate container is running fine and logs show it's listening.- A: Your
.envhasBIND_HOST=127.0.0.1. That binds ipfs-gate to the container's loopback only, which is unreachable from the nginx container. For Docker deployment, setBIND_HOST=0.0.0.0. The container is still isolated by the docker network — only nginx can reach it from inside the stack; the public only sees ports 80/443 on nginx. The shipped.env.exampledefaults to0.0.0.0— but pre-existing.envfiles from earlier installs still need the manual edit, then a fulldocker compose down && up -d(Common Problems #4).
- A: Your
- Q: Uploads succeed but recipients see "file not found" when fetching from a public IPFS gateway (e.g. ipfs.io).
- A: Check
KUBO_DHT_MODEin your.env. If set tonone(the v0.1 default), only your own ipfs-gate's gateway serves the file. Other public IPFS gateways won't know about it. This is intentional for privacy; switch toclientif you want public-IPFS-network availability. Recipients fetching viahttps://ipfs.your-domain.com/ipfs/<cid>always work regardless of this setting.
- A: Check
- Q: Payment verification fails with
memo mismatch.- A: The browser must send the exact memo string returned by
/reserve. Format isipfs-gate:upload:<16-hex-id>. If the client edited it, this will fail. The reservation_id in the memo also must match the URL parameter.
- A: The browser must send the exact memo string returned by
- Q: Upload returns
"payload.from undefined doesn't match sender <username>"even though the CNOOBS transfer landed on-chain.- A: Pre-v0.1.2 had a redundant
payload.fromcheck inhive-verify.jsthat always failed: Hive-Engine'stokens/transfercontractPayload has nofromfield (the sender is implicit in the wrapping custom_json'srequired_auths, whichextractTokenTransferOpalready validates). Fixed in v0.1.2 by removing the check. If you're on an old binary,git pull+docker compose down && docker compose up -d --build. Confirmed during v0.1 first-client integration test (2026-05-25).
- A: Pre-v0.1.2 had a redundant
- Q: Uploads "succeed" with a CID but the gateway returns
404 / "CID not pinned here"a few minutes later (or even within seconds during testing).- A: Almost always
DEFAULT_TTL_DAYSstill set to a sweeper-test value like0.001. That's ~1.44 minutes; the sweeper unpins the file very fast. Check the live config:curl -s https://ipfs.your-domain.com/ | grep ttl_days
If it's not your production value (default7), edit.env, thendocker compose down && docker compose up -d(env changes need recreate per Common Problems #4). Confirmed during v0.1 first-client integration test (2026-05-25) — sweeper-test TTL from earlier session lingered.
- A: Almost always
- Q: Upload returned 2xx with a CID even though my account didn't have enough CNOOBS to pay (so the Hive-Engine sidechain should have rejected the transfer).
- A: Pre-v0.1.3 had a soft balance check: it compared the escrow's current balance to the claimed payment amount. Since the escrow's existing balance always exceeds a single payment, the check passed even when 0 actually landed — and the file got pinned for free. Fixed in v0.1.3:
/uploadnow polls Hive-Engine'sgetTransactionInfoRPC (atapi.hive-engine.com/rpc/blockchain) for an authoritative success/fail signal. If the sidechain rejected the transfer, the upload is now hard-rejected with the actual sidechain error message and the reservation is cancelled. Ifgit pull && docker compose up -d --builddoesn't fix, check container logs forsidechain rejected tx ...entries. Confirmed during the same 2026-05-25 session when a user with 0.9 CNOOBS triggered a "successful" upload that was actually unpaid.
- A: Pre-v0.1.3 had a soft balance check: it compared the escrow's current balance to the claimed payment amount. Since the escrow's existing balance always exceeds a single payment, the check passed even when 0 actually landed — and the file got pinned for free. Fixed in v0.1.3:
- Q: I ran
git fetch --all && git reset --hard origin/main && docker compose ... --build && docker compose up -dand now the HTTPS site is unreachable (port 443 connection refused; HTTP 80 still works).- A:
git reset --hardclobbered your operator-specificnginx/ipfs-gate.conf(which had the real domain + cert path) back to the placeholder shipped in the repo. Nginx then can't load the cert, fails to bind 443, and the container restart-loops. Always back up nginx config before a hard reset. Working recipe:cp nginx/ipfs-gate.conf nginx/bk.ipfs-gate.conf # one-time backup
- A:
nginx/ipfs-gate.conf.example (or the wiki Step 4 instructions) — replace v4call.com/placeholder with your real domain. Same gotcha exists on v4call. Confirmed during 2026-05-25 testing.
- Q: I deleted a CID (admin takedown / expiry) but my browser still serves it from cache for hours.
- A: The gateway sets
Cache-Control: public, max-age=86400(1 day) on/ipfs/:cidresponses by default. Browsers respect this and skip the network entirely until the cache expires. Use/status/:cidfromcurlor an incognito window to see the gate's real state. For dev/testing, lower the cache window by settingGATEWAY_CACHE_MAX_AGE=3600(1h) or even60(1min) in.env, thendocker compose down && docker compose up -d(env changes need recreate per Common Problems #4). Pin state in the DB is independent of browser cache — the sweeper expires pins on schedule regardless. Documented in v0.1.4.
- A: The gateway sets
- Q: Uploads work but the gateway URL in the client bubble shows
https://ipfs.localhost/ipfs/...instead of my real domain.- A: You haven't set
PUBLIC_GATEWAY_BASEin.env. The server falls back tohttps://ipfs.localhostwhen neitherPUBLIC_GATEWAY_BASEnorSERVER_DOMAINis set. Add this to your.env:PUBLIC_GATEWAY_BASE=https://ipfs.your-domain.com
Thendocker compose down && docker compose up -d(env changes need recreate per Common Problems #4). v4call's client has a safety net that substitutes the user's chosen gateway URL when the server returns a localhost one, so end-users can still fetch — but new clients won't have that fallback, so fix at the source. Was missing from.env.examplepre-v0.1.2; documented in v0.1.2.
- A: You haven't set
- Q: Server logs say
kubo backend status: unreachable (fetch failed).- A: Kubo container isn't healthy yet, or the
KUBO_API_URLin.envdoesn't match the docker-compose service name. Default ishttp://kubo:5001; only change if you renamed the kubo service.
- A: Kubo container isn't healthy yet, or the
- Q: The cert renewal cron fires but nginx still serves the old cert.
- A: Certbot writes new files but nginx doesn't auto-reload. The cron line in Step 8 includes
docker compose restart nginxafter renew — make sure you copied that part too.
- A: Certbot writes new files but nginx doesn't auto-reload. The cron line in Step 8 includes
- Q: Let's Encrypt certbot fails with "DNS problem" or "No A/AAAA record for host".
- A: Your domain's A record isn't pointing at this VPS, or DNS hasn't propagated yet. Verify with
dig ipfs.your-domain.comfrom any machine. If it returns the wrong IP, fix at your DNS provider and wait 5-10 minutes for propagation, then retry the certbot command from Step 5.
- A: Your domain's A record isn't pointing at this VPS, or DNS hasn't propagated yet. Verify with
- Repo: https://github.com/CompleteNoobs/IPFS-Gate
- Sister project v4call: https://github.com/CompleteNoobs/v4call (first ipfs-gate client)
- Hive signup: https://signup.hive.io
- Hive Keychain (browser extension users will need): https://hive-keychain.com
- Hive-Engine (where CNOOBS lives): https://hive-engine.com
- Roadmap: see
roadmap_status.mdin the repo for current dev state. - Deep technical context: see
CLAUDE.mdin the repo.