Where a website is served
A CoreCP website does not live on "a server". It has five services — its web server, its database, its mail store, its nameservers and its backup target — and each of them is placed on a machine independently. Most of the time all five land
Written for: Administrator
A CoreCP website does not live on "a server". It has five services — its web server, its database, its mail store, its nameservers and its backup target — and each of them is placed on a machine independently. Most of the time all five land on the same one, and you never think about it. This page is about the times you do.
Only administrators see any of this. Resellers and end users never see, and never need to see, which machine serves them. That is deliberate: which customer sits on which machine is a map of the platform.
Looking at a placement
Panel — Accounts → an account → Placement. One line per service: the role, the machine that answers for it, what decided that, and the state. Only a server administrator sees this screen; a reseller and a customer do not have the tab.
Below the five lines is where a service nobody pinned comes from: the account's own machine, the plan, the placement pool that plan schedules into, and the policy of that pool.
Shell — the same question, and the answer the screen draws:
corecp-panel bindings list --account demo --config /etc/corecp-panel/panel.yamlACCOUNT WEBSITE ROLE NODE STATE SINCE
demo (account default) web stck1.corecp.dev active 2026-08-08 21:07
demo (account default) db stck1.corecp.dev active 2026-08-08 21:07
demo shop.example.com web stck2.corecp.dev active 2026-08-08 21:12(account default) is the answer for every website of that account that has no line of its own. shop.example.com has one, so its web server is on the other machine and everything else about it still follows the account.
What the screen lets you do
Three actions, and all three are there because they existed on the command line and nowhere else.
Move a service. Every line carries Move…. The panel only offers machines that carry the role — a machine without it is refused by the server, and offering a choice that gets refused is offering a mistake. The button stays off until you confirm what it costs; that confirmation is not a formality, the API requires it too (confirm). The move then appears under Moves with every step and its state, and it keeps running while you watch. When it lands, the line above changes with it.
A service that is not recorded anywhere. An account from before §D1 has no bindings at all: all five services answer from its own machine and the state column says Not recorded. Move one of those and the panel first writes down where it is now, and only then starts the move. The screen says so, in the panel that opens.
Pinning per website. At the top is a chooser: Account default, or one of the websites. Pick a website and you see what holds for that website — Pinned for this website on the lines that have a row of their own, and Inherited from the account on the rest. Unpin takes the row away and moves nothing; the website follows the account again afterwards. If the service is on a different machine than the default, move it first.
There is one API underneath, and this is it:
curl -s -b jar 'https://panel1.corecp.dev/api/v1/accounts/demo/placement' \
| python3 -m json.tool | head -20{
"account": "demo",
"home_node": "stck1.corecp.dev",
"package": "Basic",
"server_group": "eu-west",
"strategy": "least_websites",
"roles": [
{
"role": "web",
"source": "account",
"state": "active",
"node": {"fqdn": "stck1.corecp.dev", "roles": ["web", "db", "dns"]},
"pinned": true
}
]
}source is the resolver's own word — domain, account or account_node — not a second vocabulary invented for the screen: what the panel shows and what the command line says have to be the same claim. Add ?domain=shop.example.com and you get the same answer for one website.
What one machine carries, and what it leans on
The account page answers "where does this customer live". A server's Placement tab (Servers → the server → Placement) reads the same records from the other end, and that is the end you stand at as an operator: what is on this machine, what leans on it, and what does it lean on itself?
What it carries. One row per service, with three numbers, because they are three different facts and you use all three. Placements is the record: how many rows name this machine. Accounts is who is behind them. Websites is the resolved answer: what actually comes out here once the account default and the per-website pins have been applied. That last one is not the row count, and it is the number somebody means when they say "how many sites are on stck1". Behind each number is the list itself.
What it leans on. Under that is the reverse question: which other machine serves the database, the mail or the backup of the accounts that live here. A web machine whose customers' databases are on db1 is a machine you cannot reboot on its own — and that was visible on no other screen in the panel.
Connections. The four relations the panel records at machine height: which pool it schedules out of, which nameserver takes over from it, where its backups go and which mail gateway it runs through. The same four the fleet diagram draws, out of the same read and in the same words — see The fleet as one picture.
If the machine is draining, that is a banner above everything. It is the one property that changes what every number below it means: a draining machine still serves all of this and will never be given more. Evacuating it is done per service, from the account — there is deliberately no button that does it for you in one go.
Both views are for server administrators only: a reseller and an end customer do not get them.
Where an operation lands
A placement is not a description. Every account-bound operation the panel performs asks it which machine to go to, and it is the same question for the browser, the API, the CLI and the assistant:
| The operation | The role that decides |
|---|---|
| mailboxes, forwarders, spam settings, mail lists, webmail | mail |
| DNS zones and records, DNSSEC | dns |
| databases, database logins, phpMyAdmin | db |
| the platform's own backups of an account | backup |
| files, FTP, SSH, PHP, the website itself | web — the account's own machine |
Files and FTP are not an omission: they act on the account's home directory, and the home directory is on the machine the account is on. That is the web role.
An account with no placement resolves to its own machine. That is the whole compatibility guarantee, and it is why a one-machine platform behaves exactly as it did before any of this existed: nothing is bound, so account.node_id is the answer, which is what it was.
corecp-panel bindings list --account demo --role mail --config …ACCOUNT WEBSITE ROLE NODE STATE SINCE
demo (account default) mail stck2.corecp.dev active 2026-08-16 04:12From that moment, creating a mailbox for demo reaches stck2 — the panel's mail screen says so in its own answer, and the mailbox really is in the Dovecot there:
curl -s -b jar 'https://panel1.corecp.dev/api/v1/accounts/demo/domains/test100.nl/mail?refresh=0' \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["node"])'stck2.corecp.devNothing else moved. The same account's file manager still answers from stck1.corecp.dev, because files ride the web role.
How a new account is placed
- If the website has its own placement for that service, that wins.
- Otherwise the account's default.
- Otherwise the policy of the placement pool the customer's package schedules into.
There are three policies. Fewest websites is the default and the one to keep while you are growing: it is the number the panel knows exactly, and it is the one that predicts next year. Lowest load ranks on the machines' last metrics sample, and a machine with no recent sample sorts after every measured one — "we do not know" should not beat "we measured". Random exists for when you want no relationship at all between the order customers arrive in and where they land.
You can always ask before you commit:
corecp-panel bindings plan --account demo --roles web,db --config …ROLE NODE SOURCE WHY
web stck2.corecp.dev policy fewest websites (14) in server group eu-west
db stck2.corecp.dev policy fewest websites (14) in server group eu-westThis changes nothing. The answer also lists every machine that was considered and why each was ruled out, so "why not stck3" is answerable without guessing.
Naming a machine, or naming a group
Creating an account on a machine you name pins to it everything that machine can lawfully carry — its web server, its database, its mail store. That is what /api/v1/accounts has always done and what it still does.
Creating an account in a group pins nothing but the home. Each role then goes to the machine in the pool that carries it, which is what makes a group of single-purpose machines usable: web here, database there, mail there.
Panel — Accounts → Create account asks for a node group, not for a machine. Naming a machine is still possible, under Advanced: that is the right answer for a rebuild or a migration, and it was only the wrong default. Once the account exists the screen says which machine it landed on and why — "fewest websites (14) in server group eu-west" — so that "why there" is still answerable a year later.
curl -s -b jar -X POST https://panel1.corecp.dev/api/v1/fleet/accounts \
-H 'Content-Type: application/json' \
-d '{"username":"acme","organization_id":"<node-group>","package_id":"<plan>"}'ACCOUNT WEBSITE ROLE NODE STATE SINCE
acme (account default) web stck1.corecp.dev active 2026-08-16 05:40
acme (account default) db stck2.corecp.dev active 2026-08-16 05:40
acme (account default) mail stck2.corecp.dev active 2026-08-16 05:40
acme (account default) dns ns2.corecp.dev active 2026-08-16 05:40
acme (account default) dns stck1.corecp.dev active 2026-08-16 05:40Both paths place all five roles, and two of them are never pinned to the machine you named, because the rules below forbid the obvious answer:
- dns is enumerated, not chosen. Every machine in the pool that carries the role joins the set, because that is what a nameserver set is, and the floor of two applies. A pool with one nameserver places no dns at all — which resolves to the account's own machine, exactly as before.
- backup goes to a machine in the pool that does not already hold this account's data. A platform with no separate backup machine places none, and that too resolves to the account's own machine.
A machine that takes a role for an account is given the account first: a mail node cannot hold a mailbox for a customer it has never heard of. Nameservers are the exception and stay clean — a zone is not owned by a unix account.
Placement pools
A pool is the set of machines a package may schedule onto — a location, a tier, a generation of hardware. It is not the same thing as a node group: a node group decides who may see a machine, a pool decides what lands on it.
Panel — Servers → Placement pools. One line per pool with its policy, how many machines are in it and how many plans schedule into it. Open one and you can rename it, choose the policy, drain it, put machines in and take them out, and see which plans use it. At the bottom of the screen the difference between a node group and a pool is written out again, because that confusion is what this screen exists to prevent.
Without a pool every plan schedules across the whole node group. That is fine while every machine does the same thing; make a pool the moment it stops being.
Shell — the same thing, and still the fastest way when you are making three in a row:
corecp-panel bindings groups create eu-west --org corecp --strategy least_websites
corecp-panel bindings groups list --config …To empty a machine slowly, drain it. Everything on it keeps being served; nothing new is placed there.
corecp-panel bindings groups set eu-west --draining --config …Putting a machine in a pool
When you join a machine you pick the server group in step 1 of the wizard. If you did not, the machine stays outside every pool and nothing schedules onto it — which is exactly the state the wizard points at after a successful join (see Adding a server). There is one button for it, in two places: the wizard's follow-up step, and the pools screen.
curl -s -b jar -X POST https://panel1.corecp.dev/api/v1/nodes/stck2.corecp.dev/pool \
-H 'Content-Type: application/json' -d '{"server_group_id":"<pool>"}'{"fqdn":"stck2.corecp.dev","server_group_id":"5a1c…","roles":["db","mail"]}An empty server_group_id takes the machine back out of every pool. A pool of another node group is refused, naming both: a machine schedules inside its own group, or accounts would land on a machine nobody in that group may touch.
This is not the same as moving a machine to another node group (node.group.set). That one is a permission change wearing the clothes of an inventory edit; this is a scheduling decision.
Which plan schedules into which pool
A pool is only half a relationship. The other half is the plan that schedules into it, and until you set it every plan uses its whole node group.
corecp-panel bindings pool --config …PLAN KIND SCHEDULES INTO
Basic account the whole node group
Pro account eu-west (least_websites)corecp-panel bindings pool Basic --group eu-west --config …
corecp-panel bindings pool Basic --none --config … # back to the groupThe same over the API, which is what the plan builder uses:
curl -s -b jar -X POST https://panel1.corecp.dev/api/v1/packages/<plan>/pool \
-H 'Content-Type: application/json' -d '{"server_group_id":"<pool>"}'{"package":"Basic","server_group":"eu-west","strategy":"least_websites"}An empty server_group_id detaches the plan again. This is an administrator's call and not a reseller's: a reseller shapes a plan, and choosing the hardware it lands on is choosing the platform's shape.
Panel — Packages → a plan → Placement. The chooser sits next to the explanation that belongs with it: new accounts on this plan land on a machine from this pool, and existing accounts do not move — they stay where they are until you move a service. That sentence is there because it is the assumption that goes wrong: point a plan at a new pool and wait for a migration, and you wait for nothing.
The section appears only for a server administrator and only on an account plan. It also saves on the spot — separately from the rest of the form's Save button, because it is a different route at a different level, and one button that half-succeeds for half the people who press it is not a button.
A pool a package still points at cannot be deleted, and the refusal names the packages:
2 package(s) still schedule into this server group (Basic, Pro) — point them
somewhere else first; deleting it would leave provisioning with nowhere to placeThe rules the panel will not let you break
| It refuses when | Because |
|---|---|
| the machine does not carry that role | the placement would be perfectly consistent and the site would serve nothing |
| a nameserver set would have fewer than two machines | one published NS looks healthy right up until that machine reboots |
| a backup would sit on the machine holding the data | a copy on the same disk is not a backup |
| you remove a role a machine still serves | move the customers off it first |
| you delete a pool a package still uses | provisioning would break silently, later |
And two it warns about rather than refusing, because they are trade-offs and not mistakes:
- The web server and the database on different machines, with a slow path between them. Every query on every page pays that number. The panel measures it from the machine itself — not from the panel, which would be measuring something else entirely — and warns above about 2 ms.
- A mail machine with no reverse name for its address. Large providers filter mail from such addresses.
Web and database together is the default, and splitting them is a decision, not an upgrade.
One machine, several resellers
A machine belongs to one node group. A customer belongs to a realm — the root realm, or a reseller's. Those are two different things and they need not agree: that is exactly what shared hosting is. stck1 can sit in the root group and serve the customers of three resellers at the same time.
For a while it could not. The machine counted as a third ownership question on every account-bound call — mail, DNS, FTP, files, backups, WordPress — so the moment a customer sat in a reseller's realm she got 403 node_out_of_scope on her own website. One machine could therefore serve exactly one realm. Since 10 August 2026 placement is what it should be again: an operator question.
| Who | Judged on the machine? |
|---|---|
| administrator, server administrator | yes — a machine outside your assignment is outside your authority |
| reseller | only when creating an account: where does the new one land? |
| end user | never |
Nothing became wider. What you reach is still decided by the account itself: it has to be in your realm and owned by you or by one of your customers. What went away is a question resellers and end users should never have been asked, because they are not shown the answer anyway (see the note at the top of this page).
Panel — Fleet → machines shows a machine's group; Customers shows a customer's realm. They are deliberately not on the same screen, because they are not the same question.
Shell — on the panel machine, straight out of the database, because no screen puts the two side by side:
ssh root@panel1.corecp.dev sudo -u postgres psql -d corecp_panel -c \
"SELECT n.fqdn, o.name AS node_group FROM node n
JOIN organization o ON o.id = n.organization_id ORDER BY n.fqdn" fqdn | node_group
------------------+------------
stck1.corecp.dev | CoreCPssh root@panel1.corecp.dev sudo -u postgres psql -d corecp_panel -c \
"SELECT a.username, o.name AS realm, p.email AS owner
FROM account a JOIN node n ON n.id = a.node_id
LEFT JOIN organization o ON o.id = a.organization_id
LEFT JOIN panel_user p ON p.id = a.owner_user_id
WHERE n.fqdn = 'stck1.corecp.dev'
AND a.username IN ('demo','test200','test300') ORDER BY a.username" username | realm | owner
----------+------------------+------------------
demo | Reseller Test BV | owner@test100.nl
test200 | Reseller Test BV | owner@test200.nl
test300 | CoreCP | owner@test300.nlThree accounts, one machine, two realms. That is what it should look like — and it is exactly what scripts/seed-fixtures.sh puts there; see Using the test set.
Moving one service to another machine
Moving copies customer data and changes what answers for a website, so you have to confirm it explicitly.
Panel — Placement → Move… on the service's line. The same step list as below appears under Moves, with a counter (4/10) and each step's state. If it stops, Resume and Cancel are on the card itself. What is below on the command line is literally the same work — the screen starts it and watches.
Shell
corecp-panel bindings move <binding-id> --to stck2.corecp.dev --confirm --wait 600You will see every step:
move 1091ad55-… — web of shop.example.com for demo
stck1.corecp.dev → stck2.corecp.dev, done
STEP STATE DETAIL
preflight done stck2.corecp.dev carries the web role and answers
account_on_target done created demo on stck2.corecp.dev
export_source done demo-web-20260808T210709Z.tar
transfer done 4823040 bytes to stck2.corecp.dev
import_target done imported on stck2.corecp.dev
verify_target done stck2.corecp.dev answered 200 for shop.example.com
cutover done 2 address record(s) now point at stck2.corecp.dev
activate done stck2.corecp.dev now serves the web role
drain_source done stck1.corecp.dev no longer serves shop.example.com; …
cleanup done staged archives removedverify_target fetches the site from the new machine, by its own name, before anything is switched over. If the new machine cannot serve it, the move stops there and the customer never notices.
If it stops halfway
Nothing is lost and nothing is stuck. The old machine kept serving until cutover, and the binding goes back to active where it was. Look at the step that failed, fix what it names, and continue:
corecp-panel bindings moves --state failed --config …
corecp-panel bindings resume <move-id> --wait 600 --config …Resume, not retry. It carries on at the first step that is not finished; the work already done is not done again.
A mail move waits on purpose
A mail move ends in paused, not done. Mail the old machine already accepted is on the old machine, and every mail server in the world still holds the old MX record until its TTL expires. So the old mail store stays readable for four hours, and the binding says draining. Resume the move after that and the old sessions are ended.
paused here means waiting, as designed. It is not a failure.
What a move never does
- It never rewrites a zone somebody else hosts. If we do not host the DNS, the move tells you which record to change and the old machine keeps serving until you change it.
- It never edits your customers' configuration files. If splitting web and database means
DB_HOSThas to change inwp-config.php, the move says so and leaves the file alone. - It never deletes anything a customer owns. The old machine stops serving the site; the files stay, and the step tells you the command that removes them.
Measuring the path between two machines
corecp-panel bindings rtt stck1.corecp.dev stck2.corecp.dev --config …stck1.corecp.dev → stck2.corecp.dev: 0.31 ms over 5 samplesMeasured by the agent on the first machine, connecting to the second — not from the panel, which could be twenty milliseconds from both of them and tell you nothing about the path between them. It uses port 22 unless you name another with --port; the agents' own port is deliberately open to the panel only, so timing that would time a firewall.
Asking the assistant
The assistant can answer "which machine is this customer's site on" and "what moves are running". It cannot start, resume or cancel a move — that is an administrator's action with a confirmation, and not something to do between two sentences of a conversation.