The problem with self-hosting Odoo in 2026
The textbook way to put Odoo on the internet looks like this: rent a VPS, get a public IPv4 address, open ports 80 and 443, terminate TLS yourself with Let's Encrypt or whichever ACME client you prefer, then hope nothing in front of you drops the connection. It works. It is also a slightly outdated picture of where Odoo actually runs.
In practice, a lot of useful servers do not have a public IP at all:
- Home labs. Most residential ISPs block inbound port 80 and 443, hand you a CGNAT address, or rotate your IP every few days. Port forwarding is not even an option on a lot of fiber and 5G plans.
- Office servers behind a corporate firewall. The IT team is not opening inbound rules for an Odoo box on the LAN, and frankly they are right not to.
- Air-gapped or near-air-gapped on-prem. Manufacturing, healthcare, government, and finance frequently mandate that the database lives on a specific network. Outbound traffic is allowed, inbound is not.
- Small businesses with NAT but no static IP. A single shared IP behind the office router that gets reassigned on every modem reboot.
Historic workarounds are all painful in their own way. Dynamic DNS works until your ISP starts CGNAT'ing you. Port forwarding requires admin access to the edge router and a cooperative ISP. Site-to-site VPNs are fine if everyone in the org is on the VPN, less fine when an external accountant tries to log into Odoo from a coffee shop. Paying for a static IP is real money, and it still leaves TLS, DDoS, and edge caching as your problem.
Cloudflare Tunnel makes most of this go away. The server reaches out to Cloudflare, and Cloudflare reaches back through that connection to deliver traffic. There is nothing to forward, nothing to expose, and no static IP to buy.
What Cloudflare Tunnel actually does
Cloudflare Tunnel is a small daemon called cloudflared that runs on your server. When it starts, it opens a long-lived outbound QUIC connection to Cloudflare's edge. From that point on, any request that hits your hostname on Cloudflare's network gets shipped down that connection and handed to your local Odoo process, typically on 127.0.0.1:8069.
The mental model is roughly this:
User browser | v Cloudflare edge (TLS terminated here, DDoS scrubbed here) | v [ persistent outbound QUIC connection ] | v cloudflared on your private server | v http://127.0.0.1:8069 (Odoo)
Important things this gives you out of the box:
- No inbound ports. Your server only needs outbound 443. That is true on virtually every network on earth, including hotel WiFi.
- TLS at the edge. Cloudflare terminates TLS for you. You do not run certbot, you do not rotate certs, you do not stage Let's Encrypt challenges through a NAT.
- DDoS protection. Cloudflare absorbs Layer 3 and Layer 4 attacks before they ever touch your box. Your home internet does not melt because someone scanned your hostname.
- Edge caching. Static Odoo assets get cached in 300+ POPs, which is genuinely meaningful when your origin is a Beelink in a basement.
- Stable public hostname. You point either a Cloudflare-issued
*.cfargotunnel.comor your own domain at the tunnel. Your underlying IP can change every five minutes and nobody notices.
Cloudflare also offers Zero Trust Access policies on top of tunnels. You can require an email login, an SSO check, a device certificate, or a service token before any traffic reaches Odoo. We use that for our admin endpoints and for customers who want internal tools restricted to specific identity providers.
Self-managed vs platform-managed Tunnel
Both options end up with the same architecture. The difference is who runs the control plane.
Self-managed: you own the tunnel
You log into the Cloudflare Zero Trust dashboard, create a tunnel, copy the install command, run it on your server, then go back to the dashboard to add public hostname routes pointing to http://localhost:8069. The credentials file lives on your server, the tunnel ID lives in your Cloudflare account, and rotation is your job. So is monitoring whether the tunnel is up, fixing it when cloudflared crashes, and cleaning up orphaned tunnels in Cloudflare's API when you reinstall a server.
This is fine if you already use Cloudflare for other infrastructure, your team has Zero Trust admins, and you want full control over routes and Access policies.
Platform-managed: OEC.sh owns the tunnel
This is the mode we shipped in 2026. You connect a server to OEC.sh and pick "Cloudflare Tunnel" as the connection type. The platform calls the Cloudflare API on your behalf, creates a per-org tunnel, generates credentials, pushes them to the server through our keystore, configures the route, and adds the DNS record. cloudflared runs as a systemd service we manage.
After that, the platform keeps an eye on it. There is a periodic health check cron with a circuit breaker so we do not hammer Cloudflare when their API is having a bad day. There is an orphan metric that detects tunnels existing in Cloudflare without a matching server in our database, and the reverse. The whole provisioning step uses dependency-ordered rollback, so if creating the DNS record fails, the tunnel and credentials get cleaned up instead of lingering.
Pick platform-managed if you want a private Odoo server up in three minutes and you do not want to think about Cloudflare at all. Pick self-managed if you already have a Cloudflare account with strong opinions and you want OEC.sh to treat the tunnel as a black box.
Step-by-step: adding a private server to OEC.sh via Tunnel
Here is the actual flow with a fresh Ubuntu 24.04 box that has no public IP and is sitting behind your home router.
Step 1: Sign up and create a project
Create a free OEC.sh account. The Free plan covers one server and is enough to get a home Odoo running. Create a project, give it a name, pick the Odoo version you want (18, 19, or one of the LTS branches).
Step 2: Add a server, choose Cloudflare Tunnel
In Servers, click Add Server. You will see three connection types: SSH with public IP, BYOS one-liner, and Cloudflare Tunnel. Pick Cloudflare Tunnel. The platform will ask whether you want platform-managed (default) or self-managed. Stick with platform-managed for your first server.
Step 3: Run the install one-liner on the private server
OEC.sh will show you a Quick Connect command that looks roughly like this:
curl -fsSL https://oec.sh/install | \
bash -s -- \
--token oecsh_xxxxxxxxxxxxxxxxxxxxxxxx \
--connection cloudflare-tunnel \
--org acme-corpRun that on the private server. The script:
- installs Docker, Traefik, Fail2ban, Netdata
- installs cloudflared as a systemd service
- fetches tunnel credentials from our keystore using the per-org service token
- writes
/etc/cloudflared/config.ymlwith a route tohttp://localhost:8069 - starts
cloudflared.serviceand waits for the tunnel to come up - calls home to register the server with your OEC.sh account
You never paste an SSH key into our dashboard. The server reaches out to us, not the other way around.
Step 4: Auto-qualification kicks in
As soon as the tunnel reports healthy, the server auto-qualifies. That means the platform runs its readiness checks (Docker version, kernel features, disk space, memory, tunnel reachability) and flips the server into a deployable state. You do not manually click anything. By the time you switch back to the dashboard tab, the server is usually green.
Step 5: Deploy Odoo
From here on it is a normal OEC.sh deploy. Pick the project, hit Deploy, choose the environment. Odoo gets pulled, Postgres comes up, the database initializes, and the tunnel routes https://acme.apps.oec.sh (or your own domain) to it. We have HTTP routing through the tunnel that we validated in production with Sentry, so longpolling, websockets, and the usual Odoo session quirks all behave.
What the platform manages for you
The interesting part of platform-managed Tunnel is not the install. It is what happens over the next year. A tunnel is not a one-shot thing, it is a piece of infrastructure that needs continuous care.
- Encrypted credentials in a per-org keystore. Tunnel credentials are JSON files containing secrets that authenticate cloudflared to your tunnel. We store them encrypted at rest, scoped to your org. Our
TunnelCredentialsProviderhas a platform path (we manage everything) and a customer path (you bring your own credentials) so the same code handles both modes. - Per-org isolation. Tunnels created for one org are not visible, callable, or routable by another. Service tokens are minted per org by an idempotent seed script, and Access policies are scoped per org as well.
- Health checks with a circuit breaker. A cron polls every few minutes to confirm each tunnel is reporting connected on Cloudflare's side and answering on the server side. When Cloudflare's API goes through a rough patch, the breaker opens so we stop hitting them and start trusting the last-known good state instead.
- Credential rotation on a schedule. Long-lived secrets are bad. Tunnel credentials get rotated automatically. If a rotation fails, we keep the old credentials live until the new ones validate, then swap.
- Orphan detection. If a tunnel exists in Cloudflare but not in our database (or vice versa), we surface it as an orphan metric and clean it up. This sounds boring, but it is the difference between a clean Cloudflare account after a year and a pile of dead tunnels you cannot identify.
- Admin diagnostics. There is an internal admin dashboard for tunnel VM management with a diagnostics endpoint. When something is off, support can see it without waking up an SRE.
Use cases that justify private hosting
Cloud-only is not always the right answer. Cases where private servers plus Cloudflare Tunnel beat a vanilla VPS:
- Home lab Odoo. You are learning Odoo, building a custom module, or running it for a side project. A spare laptop with 16 GB of RAM is more capable than a $20 VPS, costs nothing per month, and now has a real public URL.
- Family or small business on-prem. The shop has a desktop in the back room running Odoo for invoicing and inventory. The accountant logs in from home. Before tunnels, that meant either a static IP plus port forwarding or a paid hosted plan. Now it is a tunnel.
- Regulated industry where data must stay on a specific network. Healthcare, defense contracting, financial services, or any contract that says "data shall not leave the premises". The tunnel only carries traffic, not data at rest. The Postgres volume lives on your hardware, in your network, under your physical control.
- Cost-sensitive setups. A used desktop running Ubuntu Server is genuinely free after the first purchase. A VPS is $5 to $20 per month forever. Over five years that is several hundred dollars, plus the recurring mental tax of patching and monitoring it.
- Edge deployments. A factory with Odoo Manufacturing wants the database on the shop floor so the MRP and shop floor terminals keep working when the WAN drops. Headquarters still wants to log in. Tunnel handles the headquarters case without making the local LAN dependent on the WAN.
Limitations and gotchas
This setup is not magic. A few things to know:
- Latency is real. Traffic now goes browser then Cloudflare edge then tunnel then your server. That adds roughly 50 to 100 ms compared to direct origin in the same city. For a normal Odoo workload it is unnoticeable. For latency-critical POS work in the same building, run a local LAN URL too.
- Bandwidth limits on free Cloudflare. The free Cloudflare plan has soft fair-use limits and will eventually nudge heavier production tenants toward a paid plan. For typical Odoo traffic on a small or mid-size business, free is plenty. For a content-heavy public website behind the same tunnel, plan for Pro or Business.
- Long-running connections need timeout tuning. Cloudflare has request timeouts that matter for long imports, large report generation, and certain XML-RPC use cases. Use Odoo's longpolling worker properly and break long imports into smaller jobs.
- WebSocket and SSE need care. Both work through tunnels, but check that your reverse proxy inside the server is forwarding upgrade headers correctly. Traefik, which OEC.sh ships, does this out of the box.
- You still need an internet connection. If your home or office WAN is down, the tunnel is down. There is no offline mode here. For truly air-gapped setups, you use the platform for management and ship the database to a network the platform cannot reach, which is a different conversation.
Cost comparison
The interesting math is over a year, not a month.
| Setup | Year 1 | Recurring |
|---|---|---|
| VPS with public IP, you manage it | ~$60 to $240 | $5 to $20 per month, forever |
| Home server + Cloudflare free + OEC.sh Free | $0 (existing hardware) | $0 per month |
| Home server + Cloudflare free + OEC.sh Starter | ~$228 | $19 per month |
| Home server + Cloudflare free + OEC.sh Pro | ~$468 | $39 per month |
For agencies, the calculus is different and arguably better. Each client can host on their own infrastructure with no public IP. You connect the box once, and you get full deploy, backup, and monitoring without ever asking for SSH access or a static IP. The Agency plan at $199 per month covers unlimited servers and projects, so the marginal cost of adding a tunnelled client server is zero.
TL;DR
- You can self-host Odoo on a private network without opening a single port.
- Cloudflare Tunnel runs
cloudflaredon your server and accepts traffic at the edge. - OEC.sh provisions and manages the tunnel, credentials, health checks, and rotation per org.
- Auto-qualification means the server is deployable as soon as the tunnel is healthy.
- Best fit: home labs, on-prem boxes, NAT'd offices, regulated industries, agencies hosting on client infrastructure.
- Mind latency, Cloudflare bandwidth tiers, and request timeouts for very long jobs.
If your only reason for renting a VPS was needing a public IP, you do not need to anymore. The desktop in the closet is a perfectly good Odoo server, and Cloudflare plus OEC.sh make it look professional from the outside.