Bottom line first: port 8090 is still port 9090, but the tenant inside has changed. The original six-container ERPNext v15 stack has been decommissioned entirely — compose stopped, volumes deleted, all images pruned, the instance directory removed, and the corresponding hosts entry wiped. The port now hosts Odoo 18 Community + PostgreSQL. A Chinese database was created with the small-business accounting standards chart of accounts and 13%/9%/6%/3% VAT rates pre-configured. The first test expense bill was confirmed, the journal entries posted instantly, the trial balance printed, and the entire interface is in Chinese.
This post is an execution log. In the previous article, I ran 107 parallel AI agents, made 949 tool calls, and spent 20 minutes dissecting the source code of three open-source bookkeeping foundations. The conclusion: Odoo Community + official l10n_cn. See 《Choosing an Open-Source Bookkeeping Foundation for the Company》 for the research process. Research is done. Now it’s time to build. Every step below includes a verification method — follow along and you can have a Chinese bookkeeping system running on your own server today.
Before diving in, one counterintuitive point worth stating upfront. For a small company’s bookkeeping foundation, the critical decision is the maintenance path, not the software itself. After installing this stack, Odoo has zero integrations for fully digitalized e-invoices or for the Golden Tax Phase IV system. All three open-source options I evaluated are the same. Deep compliance has been bracketed into an external paid tier from the start. Once you internalize this, a lot of the later hesitation simply never arises.
Prerequisites
The material list has only three items.
- A machine that can run Docker, with the Compose plugin installed —
docker compose versionproducing output is sufficient; - A way to reach port 8090 on that machine. Intranet: use the IP directly. For external access, add a reverse proxy;
- Reading the previous article is recommended but not required. The reasoning behind every choice is linked there.
Cost disclosure up front: zero for software. All three components are community editions or GPL-licensed self-hosted solutions, running on the same server that’s already in use. No new spending. A domain name can wait — intranet IP plus port gets the books open. Add the reverse proxy only when external access becomes necessary.
Clean Out the ERPNext Six Containers
Port 8090 was previously running ERPNext v15, domain erp.lingxu.local — the same instance I entered containers and inspected erpnext_china source code in the previous article. I calculated the decommissioning math: a China plugin that hasn’t been updated in over eight months, plus a custom image build pipeline that exists solely to accommodate it, with six containers of daily maintenance overhead and zero tax or localization value delivered. Since we’ve decided on Odoo, keeping this stack around only adds another thing to monitor every day.
Here I focus on the teardown, four actions with verification after each step.
- Run
docker compose down -vin the old instance directory. Docker Compose handles the six-container shutdown order on its own — it understands the dependency graph. The-vflag also removes attached volumes; without it, anonymous volumes linger on disk eating space. - Delete images. Run
docker image ls | grep frappeto find the whole family, thendocker rmieach one. - Delete the directory. ERPNext data lives in MariaDB — first dump a mysqldump backup, rsync the attachments directory, save both to persistent storage, then
rm -rfthe entire instance directory. This step is irreversible; the backup is the only safety net. - Clear hosts. Remove the erp.lingxu.local entry from
/etc/hosts. The domain no longer needs to exist.
After all four steps, verify everything at once.
| |
Only when you reach this checkpoint has port 8090 truly been freed up. The decommission is four commands plus one verification pass — cheaper and easier than expected, yet easy to procrastinate on.
If your old system contains real historical accounts, you need a plan for chart-of-accounts mapping and opening balance import before decommissioning. That belongs to a different article. Non-accounting legacy data — a single dump in your backup is enough. The principle here is simple: back up first, delete second. Never reverse the order.
One Compose File, Raising Odoo 18 in Place
The new foundation goes under ~/odoo-cn, a single docker-compose.yml. Only official images and the official l10n_cn module — no third-party China modules. The reason comes up in the pitfalls section below.
| |
The 8090:8069 in the compose file means external traffic still uses port 8090, mapped inside the container to Odoo’s default 8069. If you ever want to change the entry point, only the left number needs updating — the container internals are untouched.
The three mounts each serve one purpose. The odoo-data volume holds account attachments and session files; pg-data is the PostgreSQL data body. Lose either and your books are gone. ./config holds odoo.conf for parameter tuning without entering the container; ./addons is reserved for future custom modules. The directory tree looks like this.
| |
Data lives in exactly two places — pg-data and odoo-data. Backup exits are pg_dump plus volume copies. No third home for your books. In a single-server deployment, this is the structure least prone to mistakes.
Starting the service takes two commands. The first up pulls both odoo:18 and postgres:16, then brings the containers up automatically.
| |
If it fails to start, run docker compose logs -f web and check the logs. Two common failure modes: db not ready yet while web races ahead (restart the web container), or port 8090 still occupied by an old process (change the left-side port number).
Verification: docker compose ps shows both web and db as Up; opening http://<server_ip>:8090 in a browser displays a Chinese-language database management page. The foundation is in place.
Create the Database, Pick China — Chart and Tax Rates Grow Automatically
Click Create Database. For the Master Password, enter admin — that’s the default for the official Odoo image. Remember to change it before going to production. The database name is arbitrary. Select Chinese (Simplified) as the language, China as the country, and enter your email and password.
When installing the Accounting app, Odoo automatically attaches the l10n_cn localization based on the country you selected at creation, then asks which chart of accounts template to use. The official templates come in two versions: large-enterprise accounting standards and small-business accounting standards. The large-enterprise version has a finer-grained chart; the small-business version is more than sufficient for us. Our company, Hangzhou Lingxu Technology (杭州凌序科技), was established on 2025-09-19 as a small-scale taxpayer. We select the small-business standards, click next, and within a few minutes the chart is laid out.
I installed four apps together: Accounting, Sales, Purchasing, and Inventory. Website and Marketing are left for later. In the Community edition, opening additional apps incurs no extra charge — just toggle them on when needed. There’s no “module unlocks cost money” trap here.
The full contents of l10n_cn were audited in the previous article’s manifest review: account types, chart-of-accounts template, VAT structure, province names in Chinese, and voucher print-format templates. The first four take effect at database creation; the last one applies when printing vouchers, producing documents in a Chinese format.
Taxes don’t need to be created manually. Navigate to Accounting → Configuration → Taxes, and four pre-configured VAT rates — 13%, 9%, 6%, 3% — are already sitting there. Line items on materials and expenses auto-assign via the tax templates with zero manual work. Small-scale taxpayer invoicing follows the 3% collection rate. The 13/9/6 tiers are preloaded in the system; when you eventually upgrade to a general taxpayer, no tax reconfiguration is needed.
Two housekeeping tasks after the database is built. Change admin_passwd in config/odoo.conf and restart the web container for it to take effect. Schedule a nightly pg_dump in crontab, retaining seven rolling copies. This is the cheapest insurance policy for the entire system.
Verification: Accounting → Configuration → Chart of Accounts — the first account is 1001 Cash on Hand, followed by 1002 Bank Deposits. Expenses sit under 6602 Administrative Expenses, revenue under 6001 Main Business Revenue — all in Chinese. Not a single English label on menus, buttons, or report titles.
Also check the Settings page — Accounting → Configuration → Settings. The Financial Localization field shows China, and the currency is automatically set to RMB. With both pages verified, localization acceptance is complete. Every document’s accounts and tax rates downstream inherit from these two settings.
First Expense Bill: Auto-Written Entries, Trial Balance Balanced on the Spot
The hard test for this system is whether it dares to write journal entries for you. I created a test supplier, opened a supplier bill, selected Office Expenses under Administrative Expenses on the line, picked the applicable VAT rate, entered the tax-exclusive amount, and clicked Save then Validate in one smooth motion. After validation, I navigated to Accounting → Accounting → Journal Entries. The same bill number now has three lines:
| |
The system auto-balanced the direction and amounts of all three lines. Not a single field in the entry form was manually filled. The benefit of a chart of accounts grown from the small-business standards is precisely this — account names are all standard Chinese, and the tax line is split out by the system. Pick 3% or 6% on the tax column of a bill line, leave the amount untouched, and the tax amount column calculates itself.
Since the entries are system-authored, the accounts payable automatically posts to the test supplier’s reconciliation statement, and the aging clock starts from this bill. The receivables/payables aging mentioned in the previous article works out of the box — this is what that refers to.
Finally, open Accounting → Reports → Trial Balance, select the period. Three columns — Opening, Debit/Credit Turnover, and Closing — and the debit total equals the credit total in every column. That balance means the sum of all debit occurrences across every account for the period equals the sum of all credits. A single wrong entry breaks the balance.
The Balance Sheet and Income Statement are also worth a quick look — titles in full Chinese. Their numbers share the same foundation as the trial balance. If the entries are correct, these follow correctly, and they’re the raw material you’ll need for quarterly filings. At this point, all key components of the foundation have been validated.
- Chart of Accounts — small-business standards, fully in Chinese
- Journal Entries — expense bills auto-generate debit and credit entries
- Trial Balance — debit total equals credit total
Three Pitfalls Encountered
These pitfalls weren’t discovered fresh in this round — they were dug out in advance by 107 agents and one live run in the previous article, then relisted here in execution order.
- Containers are immutable; custom apps must be baked into the image. In the ERPNext era, installing a custom app like erpnext_china required building it into a custom image and pushing to a registry. Installing apps into a running container isn’t supported by the official setup. Maintaining a build pipeline for a single unused plugin is pure waste for a newly established small company. The same rule applies to Odoo — that’s why ./addons is mounted separately. Drop a module in, restart the container, done. Don’t modify inside the container by hand. A container restart wipes all hand-edits. That’s the direct cost of immutability.
- erpnext_china hasn’t been updated in over 8 months. The last substantive commit landed in January 2026. The community still recommends it as “ERPNext’s China localization solution,” but it’s actually a CRM/HRM localization plugin with no connection to tax or accounting. I inspected its module清单 inside the container in the previous article. Betting a bookkeeping foundation on it is equivalent to betting on an abandoned personal project.
- Third-party Odoo China modules inject ads. Free chart-of-accounts modules in the Odoo app store bundle two dependencies — app_common and app_odoo_customize. Installing them modifies official settings, syncs content from odooapp.cn, and silently injects vendor ads. Some users report the system crashing immediately after installation. That’s why this round uses only the official l10n_cn. For any third-party module, inspect the manifest dependencies first before deciding.
End State: One Foundation, Two Add-ons, All Open-Source and Self-Hosted
With the foundation in place, the periphery rounds it out per the positioning in the previous article. The complete system now consists of three pieces.
| What it does | Solution | License |
|---|---|---|
| Accounting, Inventory, Email Marketing | Odoo 18 Community | LGPL-3 |
| Automated Journeys | Mautic | GPL |
| Social Media Scheduling | Postiz | AGPL-3.0 |
Each piece runs independently, connected via webhooks and APIs for customer lists. Zero subscription fees, no legal gray areas. The key word throughout is self-hosted — accounting data, customer lists, and publishing schedules all sit on your own servers.
I chose Mautic and Postiz to fill the periphery based on the previous article’s cost analysis. The two features exclusive to Odoo Enterprise are Marketing Automation and Social Marketing — the former is drag-and-drop automation (if a customer registers and doesn’t order within three days, auto-send an email), the latter is a dashboard for scheduling multi-platform posts. The Community edition already includes email marketing, UTM tracking, and event registration — sufficient for daily use. The two missing pieces don’t justify upgrading to Enterprise. Mautic’s drip sequences and audience segmentation are finer-grained than the Enterprise version. Postiz’s multi-platform scheduled posting is the open-source equivalent of Social Marketing.
One boundary I repeated in the previous article and reiterate here: fully digitalized e-invoicing, Golden Tax Phase IV, and other deep-compliance interfaces are absent from all three open-source options. Switching to any other open-source software won’t fill this gap — it must go through an external paid tier. This is the ceiling of the entire open-source ecosystem; don’t hold it against Odoo.
Returning to that opening statement, the current maintenance path can be summarized in a few phrases: official images, official modules, plus Mautic and Postiz — two open-source projects with continuous updates. Every link in the chain has an active community behind it. Spread out and examined, the most fragile part of this combination remains human — quarterly filings, e-invoice re-entry — these still require your own attention.
Next Steps
We’re a small-scale taxpayer filing quarterly. Bills, journal entries, and the trial balance align perfectly with quarterly filing requirements. Four items are on the backlog:
- E-invoice issuing and receipt — this will go through an external paid service. Need to lock down a provider before the next quarterly filing.
- The supplier bill form still has blank fields for invoice numbers and invoice dates — waiting for the external e-invoice service to backfill those.
- Put the Inventory module to work — let inbound and outbound stock follow order flow, rather than using it as a bare general ledger.
- Connect Mautic and Postiz to one real use case each. Not installing for the sake of installing — everything deployed must actually be used.
Opening http://localhost:8090 now shows Odoo 18’s Chinese login page, with the first test expense bill and a balanced trial balance sitting inside the database. The six ERPNext containers — images, directories, everything — are gone.
