Blog/Tutorials
Tutorials

Deploying an Open-Source OnlyFans API with Coolify: A Vercel-Like UI on Your Own Server

Coolify gives you git-push deploys, a deployment UI and automatic TLS on hardware you own. Here is how to run the open-source OnlyFans and Fansly CRM API on it — including the Traefik settings that keep the event stream alive.

J
Jordan H. · Co-Founder & CEO, The Only API·Sep 19, 2026·12 min read·Updated Sep 24, 2026
Deploying an Open-Source OnlyFans API with Coolify: A Vercel-Like UI on Your Own Server

The objection to a bare VPS is not that it is hard. It is that ssh and docker compose are a bad interface for a team. Nobody wants to be the only person who can redeploy, and nobody enjoys explaining over Slack which directory the compose file lives in.

Coolify solves that without the usual trade. It is an open-source, self-hosted PaaS that runs on your own server: git-push deploys, a deployment UI, environment variable management, automatic Let's Encrypt certificates, logs and rollbacks — while the box, the disk and the SQLite database stay entirely yours. It consumes the project's docker-compose.yml directly. You get the buttons without handing a vendor your database.

This guide is the full path: server, Coolify, repository, environment, domains, and the proxy settings that decide whether your live event stream works or dies quietly.

What you are actually paying for

Line itemIndicative monthly costNotes
Coolify, self-hostedFreeFull feature set, no restrictions
Coolify Cloud (optional)$5 base for 2 servers, +$3 per extra serverTheir hosted control plane; your servers stay yours
VPS — 4 vCPU / 8 GB (e.g. Hetzner CX33)€8.498 GB is the comfortable floor with Coolify on the same box
VPS — 2 vCPU / 4 GB (e.g. Hetzner CX23)€5.49Workable, but add swap
Residential or mobile proxyVaries by supplier and locationMandatory per connected OnlyFans account; quote separately
2captcha Turnstile solves$1.45 per 1,000One paid solve per login attempt

All checked September 2026, excluding VAT.

Note the sizing difference from a bare VPS install. Coolify itself wants 2 cores, 2 GB RAM and 10 GB of disk as a minimum, and it runs alongside your application on the same machine. Add a Next.js build to that and a 4 GB box is tight. 8 GB is the number to pick if you would rather not think about it.

And the same honesty as everywhere else: below roughly five connected accounts, self-hosting costs more than the hosted plan once proxies, captcha credits and your own hours are counted. The reason to do this is control, not price.

The constraints Coolify has to respect

Before touching the UI, know what cannot be changed, because a PaaS makes two of these very easy to get wrong.

  • One API process, total. gunicorn.conf.py raises at startup if GUNICORN_WORKERS is not 1, because the APScheduler instance, the refresh_state progress store, the sse_hub subscriber registry and the memory-backed rate limiter all hold authoritative state in-process. Coolify's scaling controls are right there in the UI. Do not use them on the api service. Two replicas means every poll, webhook retry and captcha solve runs twice against the same accounts, and live events reach only half your open dashboards. The web service is stateless and could scale; there is no reason to.
  • A persistent volume. SQLite holds both the application data and the scheduler jobstore.
  • The container's working directory is the persistence root. multi_tenant_auth.py builds session paths as the relative string saved_sessions/<crm_id>/<of_user_id>.json, with no environment override. The image sets /data as the working directory so that lands inside the volume. Do not override it.
  • Python and Node in one image. The request signer is a Node subprocess invoked by header_generator.py. This is why there is a single custom Dockerfile and not a Python buildpack.
  • Long-lived HTTP. One SSE connection per open tab, held for hours, and logins that take 20–30 seconds. This is the part Coolify's default proxy needs help with.

Step one: the server

Any VPS works. Ubuntu 24.04 on a Hetzner CX33, a DigitalOcean droplet, whatever you already trust. Coolify supports Debian, Ubuntu LTS, RHEL-family, SUSE, Arch, Alpine and Raspberry Pi OS on amd64 or arm64.

Before installing, open the ports you need at the cloud firewall: 22 for SSH, 80 and 443 for the proxy, and 8000 for the Coolify dashboard. Restrict 8000 to your team's IPs if your provider's firewall allows it — there is no reason for the control panel to be reachable from the whole internet.

Step two: install Coolify

As root:

bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash

Then open http://your-server-ip:8000 and register. Do that immediately. Registration is open until the first account exists, exactly like every other self-hosted panel. If you would rather not race anyone, Coolify supports pre-creating the admin with ROOT_USERNAME, ROOT_USER_EMAIL and ROOT_USER_PASSWORD environment variables at install time.

Back up /data/coolify/source/.env once you are in. It holds Coolify's own encryption keys, and without it a restored Coolify cannot read its stored credentials. That is a separate backup from the application's — you will have two.

Step three: DNS

Create an A record for crm.example.com pointing at the server, and let it resolve before you deploy. Coolify issues Let's Encrypt certificates automatically, and like every ACME client it cannot do that for a name that does not point here yet.

Add a second record, api.example.com, only if you want to call the REST API from scripts or an MCP client. The dashboard alone needs one hostname.

Step four: add the project as a Docker Compose resource

In Coolify: Projects → New → Docker Compose, then point it at the repository and select the Docker Compose build pack. Coolify parses docker-compose.yml, adds its own proxy labels and networking, and manages the services it finds.

Two settings to get right on the way in:

  • Leave the tls compose profile disabled. The shipped Caddy service is profile-gated and off unless you explicitly enable it. Coolify fronts everything with its own Traefik instance; two proxies fighting over ports 80 and 443 is a bad afternoon.
  • Do not publish 5000 and 3000 on the host. The compose file publishes both by default for the bare-VPS case. Behind Coolify, the proxy reaches containers over the Docker network — bind them to 127.0.0.1 or drop the ports entries entirely.

The first build compiles the whole Next.js dashboard and takes a while — five to fifteen minutes on a modest box. If it dies with a bare exit code 137, that is the OOM killer during next build, not a Coolify problem. Add swap or move up a plan size.

Step five: environment variables

Set these in Coolify's environment editor before the first deploy, because three of them are read at import and the container will not boot without them.

SECRET_KEY=            # 32+ chars
ENCRYPTION_KEY=        # 32+ chars — see the warning below
TWOCAPTCHA_API_KEY=    # from 2captcha.com
NEXTAUTH_SECRET=       # 32+ chars, different from SECRET_KEY
NEXTAUTH_URL=https://crm.example.com
BACKEND_URL=http://api:5000
CORS_ORIGINS=https://crm.example.com
GUNICORN_WORKERS=1

Generate the three secrets with openssl rand -base64 48 | tr -d '\n=' and paste them in. Any sufficiently long random string works — ENCRYPTION_KEY is SHA-256'd into a Fernet key by crm_database.get_encryption_key.

Then copy ENCRYPTION_KEY somewhere off this server. Stored platform passwords and custom bot tokens are Fernet-encrypted with it and nothing else. There is no reset flow, no escrow, and no way to derive it back from the ciphertext. Lose it and every connected account has to be re-entered by hand. A password manager entry takes ten seconds and is the single highest-value thing in this guide.

TWOCAPTCHA_API_KEY is not optional either: OnlyFans gates login behind a Cloudflare Turnstile challenge and the flow pays for a solve every time, so config.py raises at import without it.

One more, and it is the one that catches people on a PaaS: every NEXT_PUBLIC variable is compiled into the browser bundle at build time. Changing them in Coolify's environment editor afterwards does nothing for client code. Set NEXT_PUBLIC_SITE_URL and NEXT_PUBLIC_APP_URL to your real dashboard URL as build variables before the first build, and if you change one later, trigger a rebuild rather than a restart.

Step six: domains, and the route that must not exist

Assign https://crm.example.com to the web service. Coolify handles the certificate.

Assign nothing to api unless you genuinely need external REST access, in which case give it https://api.example.com — its own hostname, not a path on the dashboard's.

This is the important part, and it is the single most common way to break this deployment: never point the dashboard hostname's /api routes at the Flask container. That whole path belongs to Next.js.

  • /api/auth/* is NextAuth — sign-in, session, callbacks
  • /api/crm/[...path] is a server-side proxy that attaches X-API-Key from the signed JWT, so the browser never holds the key
  • /api/events/stream is the SSE proxy, which exists because an EventSource cannot set headers

The browser never talks to Flask directly. Every dashboard request goes to Next.js, which calls Flask over the internal network at http://api:5000. Route /api/* to Flask and NextAuth's own endpoints land in an application that has never heard of them. The symptom is "login does nothing", an empty dashboard, and nothing useful in any log.

Step seven: stop Traefik killing your event stream

Coolify's default proxy is Traefik, and this is where a PaaS install differs most from the bare-VPS one — the shipped Caddyfile already solves this, and Traefik needs telling.

Two things matter. Response buffering must be off, or the SSE stream never reaches the browser: the dashboard opens one connection, the server writes a keep-alive comment every 15 seconds, and with buffering anywhere in the path the browser receives nothing until the buffer fills, which for a low-volume event stream can be never. The UI just stops updating, with no error. Traefik streams responses without buffering by default, so the usual culprit is something you added — a CDN in front, or a compression middleware applied to text/event-stream.

Second, the timeouts. Traefik's entrypoint responding timeouts are far below what a 20–30 second login and an hours-long stream need. In Servers → your server → Proxy → Configuration, add generous responding timeouts to the HTTPS entrypoint:

--entrypoints.https.transport.respondingTimeouts.readTimeout=300s
--entrypoints.https.transport.respondingTimeouts.writeTimeout=300s
--entrypoints.https.transport.respondingTimeouts.idleTimeout=300s

Restart the proxy afterwards. Coolify's proxy configuration surface has moved between releases, so check the layout in the version you installed rather than assuming this path; the community has a long-running thread on SSE and WebSocket behaviour behind Coolify's Traefik that is worth reading if yours misbehaves.

Verify it properly rather than trusting the UI. Open the dashboard, leave a tab on the overview page, and watch a counter move after an event fires. If nothing ever moves but a page refresh shows the new data, your stream is being buffered or cut.

If you would rather not fight a proxy at all, Coolify can run Caddy instead of Traefik — and the project already ships a Caddyfile tuned for exactly this workload.

Step eight: persistent storage

Coolify reads the volumes: block from the compose file and shows api_data under Persistent Storage. Confirm it is there and mounted at /data before the first deploy, and leave it alone afterwards. That volume holds everything:

/data/crm_data.db          accounts, fans, events, automations, webhooks
/data/scheduler_jobs.db    APScheduler jobstore — jobs survive restarts
/data/saved_sessions/      one JSON session file per connected account
/data/oauth_keys/          OAuth 2.1 signing keys
/data/exports/             generated "download your data" ZIPs
/data/rate-budgets/        upstream rate-limit state

Deleting a resource in Coolify can offer to delete its volumes with it. That prompt is asking whether to destroy your CRM.

Step nine: owner account and first connected account

Open the dashboard and register. That calls POST /api/auth/register, which mints your crm_id and API key.

Do it immediately, for the same reason as Coolify's own panel. The endpoint is unauthenticated by design and gated behind Cloudflare Turnstile — but turnstile_verify.verify() fails open when TURNSTILE_SECRET_KEY is unset, which is the state of a fresh install. Until you configure it, anyone who finds your hostname can create a panel on your server. Either set TURNSTILE_SECRET_KEY and NEXT_PUBLIC_TURNSTILE_SITE_KEY (and rebuild web, since the public half is compiled in), or restrict the hostname at your firewall. Never set CAPTCHA_BYPASS_TOKEN on a public box.

Then go to Accounts, add an account, and give an OnlyFans account its proxy: http://user:pass@host:port or socks5://user:pass@host:port. The connection flow requires a proxy for OnlyFans and keeps that account assigned to it; Fansly can connect without one. Proxy isolation reduces unnecessary session and location changes, but it does not guarantee an account outcome. The first credential login can run a Cloudflare init and a paid Turnstile solve, so allow time for the challenge.

Backups and upgrades

You now have two backup obligations, which is the honest cost of the nicer interface.

  • Coolify's own /data/coolify/source/.env — it holds Coolify's encryption keys.
  • The application's ENCRYPTION_KEY and the api_data volume. Find the volume with docker volume ls on the host and archive it: docker run --rm -v <volume>:/data -v "$PWD:/backup" alpine tar czf /backup/onlyapi-backup-$(date +%F).tar.gz -C /data .

Ship both off the box. A restored database without the key is undecryptable ciphertext.

Upgrades are the one place Coolify earns its keep: push to the tracked branch, or press Redeploy. Schema changes are additive and applied by _ensure_column in init_database() on startup, and the volume is untouched. Take a volume snapshot first regardless. Coolify itself updates from its settings page — do that on its own, not in the same sitting as an application deploy, so you know which change broke something.

Troubleshooting

SymptomWhat it actually is
api container restarts in a loopA missing or too-short SECRET_KEY (32+), ENCRYPTION_KEY (32+) or TWOCAPTCHA_API_KEY (10+). config.py raises at import, before Flask binds a port. The deploy log names which one
exit code 137 during the buildThe OOM killer during next build. Coolify and a Next.js build on a 4 GB box is too tight — add swap or resize
Every signed platform call starts returning 4xxA stale signing revision. X_OF_REV and APP_TOKEN track OnlyFans' deployed frontend build and go stale when OF ships. Redeploy from the latest release, or set both as environment variables and restart api. /health stays green, which is what makes this one confusing
Live counters never move, refresh shows new dataSSE is buffered or cut. Check compression middleware, any CDN in front, and the entrypoint timeouts above
Login hangs, then failsProxy timeout below 300s, or a 2captcha balance of zero
Login does nothing at all, no errorsAn /api/* route pointed at the Flask container. Remove it
Dashboard shows the wrong API URLNEXT_PUBLIC_* is compiled in. Rebuild web; a restart will not do it
RuntimeError: GUNICORN_WORKERS=…Scaling got switched on. Set it back to 1
/ready returns 503The signer or scheduler is unavailable; the body says which. /health still answers, deliberately, so a degraded container is not restart-looped

When Coolify is the wrong choice

If one person deploys and that person is comfortable with ssh, Coolify is a second system to keep patched for no gain — take the bare Hetzner route instead. Coolify pays off when a team shares the deploy button, when you want environment variables managed somewhere other than a .env file on a server, or when you are running several projects on one box.

And if none of this appeals — if you would rather not own a proxy, a captcha balance and two backup jobs — the hosted cloud is the same software with the operations attached. Most teams pick it, and that is a perfectly reasonable answer. The open-source overview lays out both paths honestly, including the platforms that cannot run this at all.

Sources · last verified Sep 24, 2026

Prices are indicative, exclude VAT, and were rechecked against Coolify and Hetzner's official pages on 24 September 2026. Coolify moves quickly and its Traefik defaults have changed between releases — verify the proxy settings described here against the version you install rather than assuming they still match.