Deploying to Oracle Cloud
This is the long-form companion to the deployment summary. The summary states the final topology and the container prerequisites; this page walks the whole path, in order, from two freshly provisioned ARM instances to a live HTTPS URL your dashboards and API run on. It is written for someone who is new to servers. Read the architecture section first, because it changes where one piece lives.
The worked example targets two Ampere A1 (ARM) instances in the Singapore region, fronted by Dokploy (a self-hosted PaaS on Traefik), with the domain nexisapi.xyz.
A. How the pieces fit (read first)
Section titled “A. How the pieces fit (read first)”The natural instinct is forge = frontends and forge-worker = database + API. One security rule in NexisOmni forces a small change: the app is same-origin only. The API sends no CORS headers and its login cookie is locked to one website (SameSite=Strict), so a dashboard and the API it calls must share one domain. Under Dokploy’s simple multi-server mode each server gets its own separate domain, so splitting the dashboard and API across two servers would break login.
The clean fix that still uses both machines: keep everything the browser touches on forge (dashboards, the API, and the Dokploy/Traefik proxy), and make forge-worker a dedicated, isolated database server. Isolating the data is the most valuable split anyway.
| Host | Role | Runs |
|---|---|---|
forge (public) |
Dokploy + Traefik, ports 80/443 | hq dashboard (static SPA), platform-admin (static SPA), NexisOmni API (Docker) |
forge-worker (private) |
Data only, no public ports | PostgreSQL 18 |
The API reaches the database over the private VCN on port 5432. The request flow is:
browser -> app.nexisapi.xyz (Traefik on forge) / -> hq dashboard files /api/* -> API -> private network -> Postgres on forge-workerbrowser -> admin.nexisapi.xyz -> platform-admin + /admin/* -> APIB. Small code changes (already landed)
Section titled “B. Small code changes (already landed)”The app was originally built for a hand-rolled Caddy setup. Three quick changes make it deploy cleanly under Dokploy’s Traefik. All three are merged, so the steps below are ready to run as written.
- Configurable login-cookie path (API). Traefik cannot rewrite a cookie’s path the way Caddy could, so the API sets the tenant login cookie at its real public path (
/api/identity) via theAuth__TenantCookiePathconfig value. This is the make-or-break fix for “login survives reload.” No proxy plugins, and it works anywhere. - Apply database schema on startup (API). The
RunCentralMigrationsOnStartupflag makes the API migrate the central database itself when it boots, so there is no separate migration command to run by hand, which fits Dokploy’s model. It is safe because you run a single copy of the API. - Build files for the two dashboards (web). Small Dockerfiles so Dokploy builds each dashboard from the monorepo and serves it with non-root nginx on port 8080. (The API already has its Dockerfile.)
C. What is needed from you
Section titled “C. What is needed from you”- Where is
nexisapi.xyz’s DNS managed? The domain registrar, Cloudflare, or Oracle DNS? That is where the two records in step 3 are added. Whoever pointspanel.nexisapi.xyzatforgetoday is the place. - Confirm the subdomains. The default assumption is
app.nexisapi.xyzfor the business dashboard andadmin.nexisapi.xyzfor the platform-admin console. Change these if you prefer. - The two IPs of each instance. Each instance has a public IP and a private (
10.0.x.x) IP; you will paste them into a few config fields. - Resend for password-reset emails. Optional for testing. If you want reset emails to actually send, you need a verified Resend domain; otherwise the API just logs them and skips sending.
1. Secure both servers
Section titled “1. Secure both servers”If the boxes are not locked down yet, do this on both instances first, over SSH. It takes ten minutes and closes the doors that get scanned within hours of a server going public.
-
Use SSH keys only, no passwords. Oracle images already ship key-only, but confirm and disable root and password login:
# /etc/ssh/sshd_config - set these, then: sudo systemctl restart sshPermitRootLogin noPasswordAuthentication noKbdInteractiveAuthentication no -
Turn on automatic security updates.
Terminal window sudo apt update && sudo apt install -y unattended-upgrades fail2bansudo dpkg-reconfigure -plow unattended-upgrades # choose "Yes" -
fail2ban(installed above) bans IPs that brute-force SSH. The defaults are fine; it is running now. Check withsudo fail2ban-client status sshd. -
Create a non-root sudo user if you are still logging in as
ubuntu/opc, and do daily work as that user. Keep root for break-glass only.
2. Open the right ports (the Oracle trap)
Section titled “2. Open the right ports (the Oracle trap)”Oracle Cloud instances sit behind two firewalls, and both must allow a port or traffic silently fails:
- The cloud Security List / NSG - in the OCI console, on your VCN.
- The instance’s own firewall -
iptables/netfilterinside the Linux box. Oracle’s default image blocks almost everything here, so a port can look “open” in the console and still refuse connections. This is the number one thing that trips up new Oracle users.
Step 2a - Cloud Security List (console)
Section titled “Step 2a - Cloud Security List (console)”Networking -> your VCN (gamezmi-dev-vcn) -> the subnet -> Security List -> add ingress rules:
| Port | Source | On which host | Why |
|---|---|---|---|
80, 443 |
0.0.0.0/0 |
forge | Public web + HTTPS (Traefik) |
5432 |
forge’s private IP /32 |
forge-worker | API to Postgres, private only |
22 |
your home/office IP /32 |
both | SSH (tighten from 0.0.0.0/0) |
Dokploy’s panel (port 3000, or its domain) is already reachable on forge; leave that as it is.
Step 2b - Instance firewall (inside each box)
Section titled “Step 2b - Instance firewall (inside each box)”On forge, allow the web ports; on forge-worker, allow Postgres only from forge’s private IP. On the Ubuntu images this is iptables:
# on forge - allow HTTP/HTTPSsudo iptables -I INPUT -p tcp --dport 80 -j ACCEPTsudo iptables -I INPUT -p tcp --dport 443 -j ACCEPT
# on forge-worker - allow Postgres ONLY from forge's private IPsudo iptables -I INPUT -p tcp -s 10.0.x.x --dport 5432 -j ACCEPT
# make the rules survive reboot (both boxes)sudo netfilter-persistent save3. Point DNS at forge
Section titled “3. Point DNS at forge”Wherever nexisapi.xyz’s DNS lives (the same place panel.nexisapi.xyz is set), add two A records pointing at forge’s public IP:
| Type | Name | Value |
|---|---|---|
A |
app |
forge public IP |
A |
admin |
forge public IP |
4. PostgreSQL on forge-worker
Section titled “4. PostgreSQL on forge-worker”Let Dokploy manage the database so you get backups and a terminal from the UI.
- Add
forge-workeras a remote server. In Dokploy -> Settings -> SSH Keys, create a key; add its public half toforge-worker’s~/.ssh/authorized_keys. Then Remote Servers -> Add Server and enterforge-worker’s IP and user. Dokploy installs its agent over SSH. - Create the database. Databases -> Create -> PostgreSQL, and target the
forge-workerserver. Pick version 18, set a strong password, name it (for examplenexisomni). Dokploy provisions it and shows you the internal connection details. - Note the connection info for the API step: host =
forge-worker’s private IP, port5432, the user and password. The API creates thenexisomni_centraldatabase on first boot. Keep Postgres reachable on the private network only (you scoped port5432toforgein step 2).
5. Deploy the NexisOmni API on forge
Section titled “5. Deploy the NexisOmni API on forge”-
Create an Application in Dokploy (target the
forgeserver), source = theNexisOmniGit repo, branchrelease/sprint-02, Build type: Dockerfile (the repo already has one). It builds natively on ARM. -
Add a persistent volume (Advanced -> Storage) mounting a volume at
/keys. This holds the encryption key ring; if it is not persisted, every tenant’s stored credentials become unreadable after a restart. -
Set environment variables (Environment tab):
Variable Value ASPNETCORE_ENVIRONMENTProductionConnectionStrings__CentralDbHost=10.0.x.x;Port=5432;Database=nexisomni_central;Username=...;Password=...Jwt__RsaPrivateKeyPemthe RSA key, one line, \nfor newlines, in quotesJwt__KeyIdpilot-1Auth__BootstrapAdmin__Email/__Passwordyour first platform-admin login TenantProvisioning__ServerConnectionStringsame host as central, DB postgresDataProtection__KeyRingPath/keysAuth__TenantCookiePath/api/identity(the cookie fix)RunCentralMigrationsOnStartuptrue(auto-migrate)Email__Resend__ApiKey/Email__FromAddressoptional; blank = reset emails only logged The last two variable names are the config keys that the prerequisite change adds.
-
Generate the RSA key once and paste it into
Jwt__RsaPrivateKeyPem:Terminal window openssl genrsa 2048 2>/dev/null | awk 'BEGIN{ORS="\\n"} {print}'That prints the key as a single line with literal
\n, ready to paste (wrap the whole thing in double quotes in the Dokploy field). -
Deploy. On first boot the API connects to Postgres and creates the central schema itself (because of the auto-migrate flag). Do not add a public domain to the API on its own; it is reached only through the dashboard domains in step 7.
6. Deploy the two dashboards on forge
Section titled “6. Deploy the two dashboards on forge”Two more Dokploy Applications from the nexis-omni-web repo (targeting forge), each built by the small Dockerfile from prerequisite 3.
Both are built with the build arg VITE_API_BASE_URL (Dokploy: Advanced -> Build args). It must be a full URL, because the app validates it as one at load, and each app’s Dockerfile checks the value, so a wrong one fails the build:
- hq dashboard.
VITE_API_BASE_URL=https://app.nexisapi.xyz/api- the same origin it is served from, with the/apiprefix. It serves the business back-office. - platform-admin.
VITE_API_BASE_URL=https://admin.nexisapi.xyz- the origin only, no/api(the admin client prepends/adminitself). This is your operator console.
7. Domains, HTTPS, and the login-cookie fix
Section titled “7. Domains, HTTPS, and the login-cookie fix”Now wire the domains so one address serves a dashboard and proxies the API. In each service’s Domains tab, add a domain, set the container port, and toggle HTTPS on -> Let’s Encrypt. Set the Path and Strip Path as below. All three services listen on container port 8080: the dashboards run nginx as a non-root user (which cannot bind :80) and the API’s Dockerfile sets ASPNETCORE_HTTP_PORTS=8080.
| Domain | Service | Path | Strip path |
|---|---|---|---|
app.nexisapi.xyz |
hq dashboard | / |
n/a |
app.nexisapi.xyz |
API | /api |
yes |
admin.nexisapi.xyz |
platform-admin | / |
n/a |
admin.nexisapi.xyz |
API | /admin |
no |
8. Verify and onboard the first tenant
Section titled “8. Verify and onboard the first tenant”- Health check.
curl -fsS https://app.nexisapi.xyz/api/healthshould return a small JSONhealthy./api/health/readyreturns ready once the schema is migrated. - The make-or-break test. Open
https://admin.nexisapi.xyz, sign in as the bootstrap admin, then reload the page. If you stay signed in, the same-origin plus cookie chain is correct end to end. If a reload logs you out, re-checkAuth__TenantCookiePathand the strip-path setting. - Onboard a tenant. In platform-admin: provision a tenant (Shared tier), then create its Owner via the add-user flow (it shows a one-time temp password).
- Owner logs in at
https://app.nexisapi.xyz, and the getting-started checklist walks them through setting up the shop.
9. Backups
Section titled “9. Backups”- Database. Dokploy has scheduled backups for its managed Postgres (to an S3-compatible bucket). Turn on a daily schedule and point it off-box so losing
forge-workeris not fatal. - The key ring too. Back up the
/keysvolume alongside the database. A database dump is useless without the encryption key ring that decrypts the tenant credentials inside it.
This is a test environment. Before real customers, revisit: a least-privilege database role for the app (instead of the superuser), a real secrets store for the JWT key and passwords, and whichever vendor you settle on for production. Everything above transfers - Dokploy and the app run the same way on any Ubuntu VM.