Last updated: 2026-08-09
Moving off Odoo.sh onto a self-hosted VPS is a well-defined project, not a leap into the unknown. Here is the actual sequence, based on real migrations we've run, not a generic "export and import" hand-wave.
Before you start: confirm the reason still holds
Revisit our Odoo.sh vs self-hosted comparison before committing. If your driver is cost, model the actual VPS + engineering time cost against your current Odoo.sh tier honestly, including the ongoing time cost of managing your own infrastructure, before starting the migration project.
Step 1: Export your database from Odoo.sh
Odoo.sh provides a database backup download through its own dashboard (Settings tab of your production branch, or via the Odoo.sh backups interface). Download the most recent automatic backup, or trigger a fresh manual one immediately before your planned migration window to minimise the data gap.
Step 2: Export the filestore
The Odoo.sh backup download includes the filestore alongside the database dump in the same archive, unlike a raw pg_dump, Odoo.sh's backup export is a full snapshot. Confirm this when you download it, extract the archive and verify both a .dump (or .sql) file and a filestore/ directory are present before proceeding.
Step 3: Provision the target server
Follow our Odoo 19 Ubuntu 24.04 install guide (or the equivalent for your target Odoo version) to get PostgreSQL, Python, and a systemd-managed Odoo instance running with an empty database, before touching the actual migration data. Confirm the fresh install works end to end first, this isolates infrastructure problems from data migration problems.
Step 4: Restore the database and filestore
# Restore the database
sudo -u postgres createdb your_production_db
sudo -u postgres pg_restore -d your_production_db /path/to/odoo_sh_export.dump
# Restore the filestore to the exact matching path
sudo mkdir -p /opt/odoo/.local/share/Odoo/filestore/your_production_db
sudo rsync -a /path/to/odoo_sh_export/filestore/ \
/opt/odoo/.local/share/Odoo/filestore/your_production_db/
sudo chown -R odoo:odoo /opt/odoo/.local/share/Odoo/filestore/your_production_db
See our filestore restore troubleshooting post if attachments don't load correctly after this step, the database name and filestore directory name have to match exactly.
Step 5: Migrate custom modules
Odoo.sh manages your custom module code via a Git repository connected to the platform. Clone that same repository onto your new server's custom addons path directly, this is one advantage of Odoo.sh's Git-first model, your custom code was already version-controlled the whole time.
sudo -u odoo git clone your_odoo_sh_repository_url /opt/odoo/custom-addons
Check the requirements.txt at the root of that repository, if it exists, and install those Python dependencies into your new server's virtual environment before starting Odoo against the migrated database:
sudo -u odoo /opt/odoo/odoo19-venv/bin/pip install -r /opt/odoo/custom-addons/requirements.txt
Odoo.sh's managed Python environment includes a specific set of system and Python packages already available, some third-party Odoo modules assume those are present without declaring them explicitly in their own manifest. If a module fails to load on the new server with an import error, check what Odoo.sh's build log historically showed for that module's dependencies rather than assuming your requirements.txt is complete on its own.
Step 6: Test thoroughly on the new server before cutover
Point a temporary subdomain or use a hosts-file override to access the new server directly, and test real workflows, not just "the login page loads." Custom modules, integrations, scheduled actions, and report generation all need verification before this becomes the production system.
Pay specific attention to anything Odoo.sh was handling for you invisibly: outbound email (Odoo.sh provides a mail relay by default, your self-hosted server needs its own SMTP configuration, see the mailcow-style setup pattern in our other deployment posts), scheduled backups (no longer automatic, wire up the pipeline from our backup automation post before go-live, not after), and SSL certificate renewal (Odoo.sh handles this for you; on your own server you need Let's Encrypt with an auto-renewal cron or systemd timer configured and verified before cutover).
Step 7: DNS cutover with minimal downtime
- Lower your domain's DNS TTL a day or two ahead of the planned cutover, so the actual switch propagates fast.
- Freeze writes on Odoo.sh briefly (maintenance mode) at your planned cutover time.
- Take one final database and filestore export covering the gap since your last full export.
- Apply that final incremental data to the new server.
- Update DNS to point at the new server.
- Monitor both the new server's logs and actual user access for the first few hours.
Keep the Odoo.sh instance accessible (even if frozen) for a holdback period, at least a week, in case you need to reference something from it before fully decommissioning it.
How much downtime to actually expect
The honest answer depends almost entirely on database size and how tight your final incremental sync window is. For a database under a few GB, the full sequence in Step 7 (freeze, final export, restore, DNS switch) can realistically complete inside 30-60 minutes if rehearsed beforehand on the same infrastructure. For larger databases, the final incremental export and restore is the long pole, budget proportionally more time, and consider whether your business can tolerate a maintenance window outside business hours rather than treating minimal downtime as an absolute requirement regardless of data size. Rehearsing the full cutover sequence once against a copy of production data, timing each step, is the only reliable way to know your actual number rather than guessing.
What changes for your team day to day after the move
A few operational responsibilities shift from Odoo.sh to whoever manages the new server, worth listing explicitly so nothing falls through the cracks in the weeks after cutover:
- Monitoring and alerting. Odoo.sh monitors instance health for you. Post-migration, you need your own uptime monitoring and alerting on the new server, this isn't automatic just because the server is running.
- Security patching. OS-level and PostgreSQL security patches are now your responsibility on a regular cadence, not applied for you.
- Capacity planning. Odoo.sh scales your instance tier when you upgrade a plan. On your own server, growth in users or data means you're the one watching disk, RAM, and CPU headroom and provisioning ahead of running out.
Common mistakes
- Not testing custom modules before cutover. A module that worked fine under Odoo.sh's managed Python environment can fail on a self-managed server with different system package versions if dependencies weren't pinned carefully.
- Forgetting scheduled actions (cron jobs) configuration. These migrate with the database, but confirm they're actually running post-migration, a systemd service without the right worker configuration can silently fail to process them.
- No rollback plan. Keep Odoo.sh live and unmodified until you're fully confident in the new server, don't decommission it same-day.
- Forgetting outbound email configuration. The most common "everything works except..." gap in these migrations. Test a real outbound email (a password reset, a quotation send) before cutover, not after users start reporting missing emails.
- Assuming SSL is automatic. It isn't, on your own server, unlike on Odoo.sh. Confirm certificate issuance and auto-renewal work before the domain actually points at the new server.
- Underestimating the staging-branch habit gap. Teams used to Odoo.sh's automatic per-branch staging often don't have an equivalent workflow set up on day one of the new server, and end up testing changes against production out of habit, or convenience, in the first weeks after cutover. Set up at least a basic staging database and filestore copy on the new infrastructure before the old Odoo.sh staging branches are gone for good.
- Not verifying scheduled action timezone handling. Odoo.sh's server timezone and your new server's timezone (often UTC by default on a fresh Ubuntu install) can differ from what your cron-based scheduled actions were tuned for. Confirm nightly jobs, report generation, and any time-sensitive automated actions still fire at the intended local time after the move, not just that they fire at all.
Sources: Odoo official documentation.