Unchained Engine
Overview
This guide covers the deployment and configuration of Unchained Engine on Ubuntu 24.04 on Azure, using SecureAnyCloud Azure Marketplace images. Unchained Engine is an open-source headless ecommerce engine.
Rather than shipping a storefront you have to bend into shape, it gives you the commerce domain itself as a single GraphQL API: products and variants, assortments and filters, carts and orders, quotations, subscriptions and enrollments, pricing and discounts, delivery, payment and warehousing, plus users, roles and a background work queue. Your storefront, mobile app, point-of-sale terminal or kiosk is whatever you choose to build against that API.
Because the engine is headless, it also ships a full browser admin console for the people who run the shop day to day. They can create and publish products, organise assortments, work through orders and quotations, manage customers and roles, and import or export catalogue data in bulk.
The SecureAnyCloud image ships the free and open-source, EUPL-1.2 licensed Unchained Engine 4.8.25. The engine, its MongoDB datastore, the prebuilt admin console and nginx are already wired together and captured into the VM, so your instance is usable within a minute of first boot.
Nothing here ships with a known credential, which matters more than usual with this product. Every upstream example application commits a published administrator password and published signing secrets to a public repository. This image ships neither. A single administrator with a random 24-character password, a fresh session signing secret and fresh MongoDB passwords are generated on the first boot of each VM, before the engine is allowed to start.
Unchained is a trademark of its respective owner. This image is produced by SecureAnyCloud and is not affiliated with, endorsed by, or sponsored by Unchained Commerce. It ships the free and open-source EUPL-1.2 licensed self-hosted software, with the deployment-specific choices described under "Differences from a stock install" below.
What you get:
- Unchained Engine 4.8.25 (EUPL-1.2), installed from the published npm packages at exact pinned versions
- The Unchained admin console, served at
/and calling the API on the same origin, so it works at any address with no reconfiguration - The full GraphQL commerce API at
/graphql - Node.js 22 LTS running the engine as
unchained.service, bound to the loopback interface only - MongoDB 8.0 Community with SCRAM authentication, bound to the loopback interface only
- nginx on port
80as the single front door - The
baseplugin preset: postal shipping, invoice payment, store warehousing, the full pricing chain, quotations, enrollments, GridFS file storage and the whole worker set unchained-firstboot.service, which generates every secret and the single administrator account on first boot- No administrator account, no signing secret and no database password baked into the image
- Ubuntu 24.04 LTS base with latest security patches applied at build time
- Azure Linux Agent for seamless cloud integration and SSH key injection
- 24/7 SecureAnyCloud support with guaranteed 24 hour response SLA
A note on the screenshots below. The admin console screenshots in this guide were taken with a small demonstration catalogue and two example orders, so you can see what a working shop looks like. Your VM starts with an empty catalogue and no orders. The demonstration data is destroyed before the image is captured. Your VM creates the base shop configuration (language, currency, country, a shipping provider and an invoice payment provider) for itself on first boot.
Prerequisites
- Active Azure subscription, SSH public key, VNet and subnet in target region
- Subscription to the Unchained Engine listing on Azure Marketplace
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is a sensible starting point for evaluation, development and a small production shop. For a large catalogue or heavy order volume, choose Standard_D2s_v5 or larger.
For NSG inbound rules, allow 22/tcp from your management network and 80/tcp from the networks that use the console and the API. Port 443/tcp is used once you attach your own certificate, as shown in Step 9.
Deploy from the Azure Portal
Sign in to the Azure Portal. Choose Create a resource, search the Marketplace for Unchained Engine by SecureAnyCloud, and select Create.
On Basics, pick your subscription, resource group, region and size. Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22) and HTTP (80).
Then select Review + create and Create.
Deploy from the Azure CLI
RG="unchained-prod"; LOCATION="eastus"; VM_NAME="unchained-01" IMAGE_URN="sac1647283583153:unchained-ubuntu-24-04:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION" az network vnet create -g "$RG" --name unchained-vnet --address-prefix 10.100.0.0/16 --subnet-name unchained-subnet --subnet-prefix 10.100.1.0/24 az network nsg create -g "$RG" --name unchained-nsg az network nsg rule create -g "$RG" --nsg-name unchained-nsg --name allow-ssh --priority 100 \
--source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp az network nsg rule create -g "$RG" --nsg-name unchained-nsg --name allow-http --priority 110 \
--destination-port-ranges 80 --access Allow --protocol Tcp
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:unchained-ubuntu-24-04:default:latest
az vm create -g "$RG" --name "$VM_NAME" --image "$IMAGE_URN" \
--size Standard_B2s --storage-sku StandardSSD_LRS \
--admin-username azureuser --ssh-key-values "$SSH_KEY" \
--vnet-name unchained-vnet --subnet unchained-subnet --nsg unchained-nsg --public-ip-sku StandardConfirm the services are running
Connect over SSH as azureuser, then confirm all four units are active. unchained-firstboot.service is a one-shot unit that reports active once it has completed successfully.
systemctl is-active mongod nginx unchained unchained-firstbootExpected output:
active active active activeCheck the engine release that is actually installed, its licence, and that both the console and the API answer:
node -p "'Unchained Engine ' + require('/opt/unchained/node_modules/@unchainedshop/platform/package.json').version + ' (licence ' + require('/opt/unchained/node_modules/@unchainedshop/platform/package.json').license + ')'"
node --version mongod --version | head -1 curl -s -o /dev/null -w 'Unchained admin console: HTTP %{http_code}\n' http://127.0.0.1/ curl -s -o /dev/null -w 'Unchained GraphQL API: HTTP %{http_code}\n' -X POST http://127.0.0.1/graphql \
-H 'content-type: application/json' --data '{"query":"{shopInfo{version}}"}'Expected output:
Unchained Engine 4.8.25 (licence EUPL-1.2) v22.23.2 db version v8.0.31 Unchained admin console: HTTP 200 Unchained GraphQL API: HTTP 200Retrieve your per-VM credentials
Every secret on this VM was generated on its first boot and exists nowhere else. They are written to a root-only file.
sudo stat -c '%n mode=%a owner=%U:%G' /root/unchained-credentials.txt
sudo grep -o '^[a-z.]*=' /root/unchained-credentials.txt | tr -d '='Expected output:
/root/unchained-credentials.txt mode=600 owner=root:root unchained.url unchained.graphql unchained.admin.username unchained.admin.email unchained.admin.password mongo.admin.username mongo.admin.password mongo.app.username mongo.app.password mongo.databaseRead the values when you need them, with sudo cat /root/unchained-credentials.txt. Copy the administrator username and password into your password manager now, then continue.
Sign in to the admin console
Browse to http://<your-vm-public-ip>/. Signing in takes two steps: enter the unchained.admin.username value, choose Continue, then enter the unchained.admin.password value.
You can prove the same credential works from the command line before you open a browser. This reads the generated password straight from the credentials file, so there is nothing to paste:
JAR="$(mktemp)" ADMIN_USER="$(sudo grep '^unchained.admin.username=' /root/unchained-credentials.txt | cut -d= -f2-)" ADMIN_PASS="$(sudo grep '^unchained.admin.password=' /root/unchained-credentials.txt | cut -d= -f2-)" curl -s -c "$JAR" -X POST http://127.0.0.1/graphql -H 'content-type: application/json' \
--data "{\"query\":\"mutation{loginWithPassword(username:\\\"${ADMIN_USER}\\\",password:\\\"${ADMIN_PASS}\\\"){_id}}\"}" > /dev/null curl -s -b "$JAR" -X POST http://127.0.0.1/graphql -H 'content-type: application/json' \
--data '{"query":"{me{username roles}}"}' echo rm -f "$JAR"Expected output:
{"data":{"me":{"username":"admin","roles":["admin"]}}}Once signed in you land on the dashboard, which confirms the shop is configured and summarises the catalogue, customers, sales and orders.
Create your first product
Your VM starts with an empty catalogue. Add a product from Products in the console sidebar, choose Add Product, pick a product type and give it a title, then set a price under the product's Commerce tab and publish it.
The same thing over the API. This signs in, creates a product with an English title and a price, publishes it, reads it back, and then removes it again so your catalogue is left exactly as it was:
JAR="$(mktemp)" ADMIN_USER="$(sudo grep '^unchained.admin.username=' /root/unchained-credentials.txt | cut -d= -f2-)" ADMIN_PASS="$(sudo grep '^unchained.admin.password=' /root/unchained-credentials.txt | cut -d= -f2-)" gql() { curl -s -c "$JAR" -b "$JAR" -X POST http://127.0.0.1/graphql -H 'content-type: application/json' --data "$1"; } gql "{\"query\":\"mutation{loginWithPassword(username:\\\"${ADMIN_USER}\\\",password:\\\"${ADMIN_PASS}\\\"){_id}}\"}" > /dev/null PRODUCT_ID="$(gql '{"query":"mutation($p:CreateProductInput!,$t:[ProductTextInput!]){createProduct(product:$p,texts:$t){_id}}","variables":{"p":{"type":"SIMPLE_PRODUCT","tags":["guide-example"]},"t":[{"locale":"en","title":"Guide example product","slug":"guide-example-product"}]}}' | sed -n 's/.*"_id":"\([^"]*\)".*/\1/p')" gql "{\"query\":\"mutation{updateProductCommerce(productId:\\\"${PRODUCT_ID}\\\",commerce:{pricing:[{amount:2500,currencyCode:\\\"USD\\\",countryCode:\\\"US\\\",isTaxable:true,isNetPrice:false}]}){_id}}\"}" > /dev/null gql "{\"query\":\"mutation{publishProduct(productId:\\\"${PRODUCT_ID}\\\"){_id}}\"}" > /dev/null gql "{\"query\":\"{product(productId:\\\"${PRODUCT_ID}\\\"){status texts{title} ... on SimpleProduct{simulatedPrice{amount currencyCode}}}}\"}" echo gql "{\"query\":\"mutation{removeProduct(productId:\\\"${PRODUCT_ID}\\\"){_id}}\"}" > /dev/null rm -f "$JAR"Expected output:
{"data":{"product":{"status":"ACTIVE","texts":{"title":"Guide example product"},"simulatedPrice":{"amount":2500,"currencyCode":"USD"}}}}Product is a GraphQL interface, so the price lives on the concrete type: the inline fragment ... on SimpleProduct is what reaches it. Prices are held in minor units, so 2500 is $25.00.
Orders
Orders placed by your storefront, mobile app or any other client appear under Orders, with the order number, date, computed total, customer and status. From an order you can inspect its items, delivery and payment, and move it through the fulfilment lifecycle.
Everything the console does here is available over the API, which is how your own storefront drives it. A cart is created by adding a product, then given a contact and billing address and a delivery and payment provider, then checked out:
JAR="$(mktemp)" ADMIN_USER="$(sudo grep '^unchained.admin.username=' /root/unchained-credentials.txt | cut -d= -f2-)" ADMIN_PASS="$(sudo grep '^unchained.admin.password=' /root/unchained-credentials.txt | cut -d= -f2-)" gql() { curl -s -c "$JAR" -b "$JAR" -X POST http://127.0.0.1/graphql -H 'content-type: application/json' --data "$1"; } gql "{\"query\":\"mutation{loginWithPassword(username:\\\"${ADMIN_USER}\\\",password:\\\"${ADMIN_PASS}\\\"){_id}}\"}" > /dev/null gql '{"query":"{deliveryProviders{_id type}paymentProviders{_id type}}"}' echo rm -f "$JAR"Expected output (your identifiers will differ):
{"data":{"deliveryProviders":[{"_id":"027340fe3023d629d1bda2d1","type":"SHIPPING"}],"paymentProviders":[{"_id":"bc8eb70f383b2db30274fc05","type":"INVOICE"}]}}Those two providers are created for you on first boot, so the engine can price and accept an order out of the box. The full cart and checkout flow is addCartProduct, then updateCart with a contact, billing address and those two provider identifiers, then checkoutCart.
Understand the security model
Four properties are worth verifying yourself.
JAR="$(mktemp)" ADMIN_USER="$(sudo grep '^unchained.admin.username=' /root/unchained-credentials.txt | cut -d= -f2-)" ADMIN_PASS="$(sudo grep '^unchained.admin.password=' /root/unchained-credentials.txt | cut -d= -f2-)" curl -s -c "$JAR" -X POST http://127.0.0.1/graphql -H 'content-type: application/json' \
--data "{\"query\":\"mutation{loginWithPassword(username:\\\"${ADMIN_USER}\\\",password:\\\"${ADMIN_PASS}\\\"){_id}}\"}" > /dev/null curl -s -b "$JAR" -X POST http://127.0.0.1/graphql -H 'content-type: application/json' \
--data '{"query":"{usersCount}"}' | sed 's/.*"usersCount":\([0-9]*\).*/accounts on this VM: \1/' rm -f "$JAR" for P in password admin; do printf 'upstream default admin/%s -> ' "$P" curl -s -X POST http://127.0.0.1/graphql -H 'content-type: application/json' \
--data "{\"query\":\"mutation{loginWithPassword(username:\\\"admin\\\",password:\\\"${P}\\\"){_id}}\"}" \
| grep -qo '"_id"' && echo 'ACCEPTED' || echo 'refused' done curl -s -X POST http://127.0.0.1/graphql -H 'content-type: application/json' \
--data '{"query":"mutation{createProduct(product:{type:SIMPLE_PRODUCT}){_id}}"}' \
| python3 -c "import json,sys; print('anonymous createProduct -> ' + (json.load(sys.stdin).get('errors') or [{}])[0].get('message','ALLOWED'))" ss -tlnH | awk '{print $4}' | grep -E ':(80|4010|27017)$' | sort -u | sed 's/^/listening on /'Expected output:
accounts on this VM: 1 upstream default admin/password -> refused upstream default admin/admin -> refused anonymous createProduct -> The user "anonymous" has no permission to perform the action "manageProducts" in "createProduct" listening on 0.0.0.0:80 listening on 127.0.0.1:27017 listening on 127.0.0.1:4010 listening on [::]:80- One account, no default. Every upstream example application creates
adminwith thepasswordpassword and commits the signing secrets alongside it. Those values do not exist in this image, and the build fails closed if any of them ever reaches the shipped configuration. Your VM's single administrator was created on first boot with a random password. - The engine and the database are private. The engine listens on
127.0.0.1:4010and MongoDB on127.0.0.1:27017. Only nginx on port80is reachable from the network. - Administrative operations are role-gated. The commerce API is deliberately reachable, because that is what a headless engine is for and it is how your storefront talks to it. Everything administrative is refused to anyone without the role for it, as the anonymous
createProductabove shows. - Customer sign-up is open by design. The console's sign-in page offers a Sign up link, and the API exposes account creation, because a shop needs customers to be able to register. New accounts get no administrative rights whatsoever. If you do not want public registration, restrict access to port
80with a network security group as described in Step 9, or put your own storefront in front of the API and expose only that.
Put your own domain and TLS in front
The image serves plain HTTP on port 80 so it works immediately at any address. For anything beyond a trial, terminate TLS with your own certificate and restrict access with a network security group.
Point a DNS A record at the VM's public IP, allow 443/tcp in the NSG, then install a certificate. Replace the values in angle brackets with your own:
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d <your-domain> -m <your-email> --agree-tos --redirectBecause the admin console calls the API on the same origin using relative paths, no rebuild or reconfiguration is needed when you move to a hostname or to HTTPS. It simply works at the new address.
Once you are serving over HTTPS, tighten the session cookie. The image ships a cookie that a browser will accept over plain HTTP, which necessarily means it is not marked Secure. Edit /opt/unchained/.env, remove the UNCHAINED_COOKIE_INSECURE=1 line, and restart the engine:
sudo sed -i '/^UNCHAINED_COOKIE_INSECURE=1$/d' /opt/unchained/.env
sudo systemctl restart unchainedAlso update ROOT_URL and EMAIL_WEBSITE_URL in the same file to your https:// address so any absolute links the engine generates point at your domain.
Send email
The engine queues order confirmations, password resets and account messages onto its work queue and hands them to an SMTP server. No SMTP server is configured out of the box, so those work items fail until you set one. Add your SMTP URL to /opt/unchained/.env and restart:
sudo sed -i 's|^MAIL_URL=.*|MAIL_URL=smtps://user:pass@smtp.example.com:465|' /opt/unchained/.env
sudo systemctl restart unchainedUntil you do, the console's dashboard may show a Problems detected in the Work Queue banner counting failed EMAIL items. That is the expected state of an engine with no mail server attached, not a fault with the image. Review or retry the items under Work queue in the console sidebar.
Back up and restore
All shop data lives in the unchained MongoDB database. Take a dump with the application credentials from the credentials file:
MONGO_APP_PASSWORD="$(sudo grep '^mongo.app.password=' /root/unchained-credentials.txt | cut -d= -f2-)" sudo mkdir -p /var/backups/unchained
sudo mongodump --host 127.0.0.1 --port 27017 \
-u unchained -p "${MONGO_APP_PASSWORD}" --authenticationDatabase unchained \
--db unchained --out /var/backups/unchained/$(date -u +%Y%m%d)Uploaded media is stored inside MongoDB by the GridFS file adapter, so the dump above covers it. To restore, use mongorestore with the same credentials.
Connect a MongoDB client
MongoDB is deliberately not reachable from the network. To use a graphical client from your workstation, forward the port over SSH:
ssh -N -L 27017:127.0.0.1:27017 azureuser@<vm-ip>Then connect to mongodb://127.0.0.1:27017 using the mongo.admin.username and mongo.admin.password values from the credentials file, with admin as the authentication database.
Server components
Component Version Purpose
Unchained Engine 4.8.25 Headless commerce engine and GraphQL API
Unchained admin console 4.8.25 Browser application for running the shop
Node.js 22 LTS Runtime for the engine
MongoDB Community 8.0 Datastore, loopback only, SCRAM authentication
nginx 1.24 Front door on port 80, reverse proxy to the engine
Ubuntu Server 24.04 LTS Operating system
Filesystem layout
Path Purpose
/opt/unchained Engine host application, its npm packages and .env
/opt/unchained/.env Runtime configuration and per VM secrets, mode 0640
/opt/unchained/LICENSE The upstream EUPL-1.2 licence text
/var/lib/mongodb MongoDB data directory
/var/log/unchained Engine stdout and stderr
/root/unchained-credentials.txt Per VM credentials, mode 0600 root only
/var/lib/sac First boot sentinel and bootstrap ready marker
Scripts and log files
Path Purpose
/usr/local/sbin/unchained-firstboot.sh Generates every per VM secret and the single administrator
/usr/local/sbin/unchained-roundtrip.sh Proves the per VM credential authenticates end to end
/var/log/sac/sac-firstboot.log First boot log
/var/log/unchained/stdout.log Engine standard output
/var/log/unchained/stderr.log Engine standard error
/var/log/mongodb/mongod.log MongoDB log
On startup
unchained-firstboot.service runs once, on the very first boot of each VM. It resolves the VM's public address, generates the MongoDB administrator and application passwords, the session signing secret, the magic key secret and the GridFS upload secret, writes them to /opt/unchained/.env, creates exactly one administrator with a random 24-character password, writes everything to /root/unchained-credentials.txt with mode 0600, then releases the bootstrap gate and starts the engine.
unchained.service carries ConditionPathExists=/var/lib/sac/data/unchained-bootstrap-ready. That marker is absent in the shipped image and is created by first boot only after the secrets are in place, so the engine can never start against the blank configuration the image ships with. Once first boot has completed, the unit starts normally on every subsequent reboot.
Differences from a stock install
The engine itself is the unmodified upstream release, installed from the published npm packages at exact pinned versions. Unchained is a framework rather than a server binary, so every deployment supplies its own small host application. This image's is at /opt/unchained/boot.js, derived from upstream's own minimal example. The deployment-specific choices are recorded here so the image is auditable:
- The engine binds to the loopback interface. Upstream's example listens on all interfaces; here nginx on port
80is the single front door. - Session cookies are
SameSite=Laxand not markedSecure. Upstream defaults toSameSite=None, which browsers only honour on aSecurecookie, and aSecurecookie is discarded over plain HTTP. On an appliance first reached athttp://<ip>/that combination makes signing in impossible in a browser. Step 9 shows how to restore the stricter setting once you have TLS. - The base shop configuration is created on first boot (English, US dollars, United States, a postal shipping provider and an invoice payment provider) so the engine can price and accept an order immediately.
- Exactly one administrator is created on first boot, from a secret handed to the engine through a file on tmpfs that the engine deletes as soon as it has read it. The password is never written to the environment file.
- The
baseplugin preset is enabled, notall. Theallpreset additionally loads the Stripe, PayPal, Braintree, Datatrans, Payrexx and cryptopay adapters, each of which needs vendor API credentials that a marketplace image cannot hold. Add whichever you use to/opt/unchained/package.jsonand import it in/opt/unchained/boot.js.
Troubleshooting
The console loads but every action fails. Check the engine is running and reachable behind nginx:
systemctl is-active unchained curl -s -o /dev/null -w 'GraphQL through nginx: HTTP %{http_code}\n' -X POST http://127.0.0.1/graphql \
-H 'content-type: application/json' --data '{"query":"{shopInfo{version}}"}'
sudo tail -n 30 /var/log/unchained/stderr.logThe engine did not start after a rebuild or restore. unchained.service will not start until first boot initialisation has completed, which is deliberate: it prevents the engine ever running with a blank signing secret. Confirm the markers exist:
ls -l /var/lib/sac/data/unchained-bootstrap-ready /var/lib/sac/data/unchained-firstboot.done
sudo systemctl status unchained-firstboot.service --no-pager | head -20Signing in fails in the browser but works with curl. That is the session cookie being rejected by the browser. Check the cookie the engine issues:
curl -s -D - -o /dev/null -X POST http://127.0.0.1/graphql -H 'content-type: application/json' \
--data '{"query":"mutation{loginWithPassword(username:\"nobody\",password:\"nobody\"){_id}}'"'"'}' \
| grep -i '^set-cookie:' || echo 'no cookie issued for a failed login, which is correct'A working cookie is HttpOnly; SameSite=Lax with no Secure flag while you are on plain HTTP. If you have added TLS, follow Step 9 so the cookie is marked Secure too.
You lost the administrator password. Resetting it directly against the engine's datastore is not supported. Instead, you can mint a replacement administrator by clearing the first boot state and letting first boot run again. This destroys the current administrator account, so do it only if you have lost access:
echo "This would re-run first boot and mint a new administrator."
echo "Contact sales@secureanycloud.com before running it on a shop with live data."Security recommendations
- Restrict inbound
22/tcpto your management network and80/tcp(or443/tcp) to the networks that need the console and API, using an Azure network security group. - Terminate TLS with your own certificate and follow Step 9 to mark the session cookie
Secure. - Change the generated administrator password from the console after your first sign in, and create individual named accounts for the people who need them rather than sharing one.
- Keep the VM patched. Unattended security upgrades are enabled on the image.
- Take regular backups of the
unchaineddatabase as shown in Step 11.
Support
24/7 technical support is included with this image. Email sales@secureanycloud.com with your Azure subscription ID and the VM name, and we respond within 24 hours.
Deploy on Azure
Launch Unchained Engine on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com