Skip to content

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.

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-worker
browser -> admin.nexisapi.xyz -> platform-admin + /admin/* -> API

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.

  1. 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 the Auth__TenantCookiePath config value. This is the make-or-break fix for “login survives reload.” No proxy plugins, and it works anywhere.
  2. Apply database schema on startup (API). The RunCentralMigrationsOnStartup flag 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.
  3. 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.)
  • 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 points panel.nexisapi.xyz at forge today is the place.
  • Confirm the subdomains. The default assumption is app.nexisapi.xyz for the business dashboard and admin.nexisapi.xyz for 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.

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.

  1. 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 ssh
    PermitRootLogin no
    PasswordAuthentication no
    KbdInteractiveAuthentication no
  2. Turn on automatic security updates.

    Terminal window
    sudo apt update && sudo apt install -y unattended-upgrades fail2ban
    sudo dpkg-reconfigure -plow unattended-upgrades # choose "Yes"
  3. fail2ban (installed above) bans IPs that brute-force SSH. The defaults are fine; it is running now. Check with sudo fail2ban-client status sshd.

  4. 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.

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/netfilter inside 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.

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:

Terminal window
# on forge - allow HTTP/HTTPS
sudo iptables -I INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -I INPUT -p tcp --dport 443 -j ACCEPT
# on forge-worker - allow Postgres ONLY from forge's private IP
sudo iptables -I INPUT -p tcp -s 10.0.x.x --dport 5432 -j ACCEPT
# make the rules survive reboot (both boxes)
sudo netfilter-persistent save

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

Let Dokploy manage the database so you get backups and a terminal from the UI.

  1. Add forge-worker as a remote server. In Dokploy -> Settings -> SSH Keys, create a key; add its public half to forge-worker’s ~/.ssh/authorized_keys. Then Remote Servers -> Add Server and enter forge-worker’s IP and user. Dokploy installs its agent over SSH.
  2. Create the database. Databases -> Create -> PostgreSQL, and target the forge-worker server. Pick version 18, set a strong password, name it (for example nexisomni). Dokploy provisions it and shows you the internal connection details.
  3. Note the connection info for the API step: host = forge-worker’s private IP, port 5432, the user and password. The API creates the nexisomni_central database on first boot. Keep Postgres reachable on the private network only (you scoped port 5432 to forge in step 2).
  1. Create an Application in Dokploy (target the forge server), source = the NexisOmni Git repo, branch release/sprint-02, Build type: Dockerfile (the repo already has one). It builds natively on ARM.

  2. 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.

  3. Set environment variables (Environment tab):

    Variable Value
    ASPNETCORE_ENVIRONMENT Production
    ConnectionStrings__CentralDb Host=10.0.x.x;Port=5432;Database=nexisomni_central;Username=...;Password=...
    Jwt__RsaPrivateKeyPem the RSA key, one line, \n for newlines, in quotes
    Jwt__KeyId pilot-1
    Auth__BootstrapAdmin__Email / __Password your first platform-admin login
    TenantProvisioning__ServerConnectionString same host as central, DB postgres
    DataProtection__KeyRingPath /keys
    Auth__TenantCookiePath /api/identity (the cookie fix)
    RunCentralMigrationsOnStartup true (auto-migrate)
    Email__Resend__ApiKey / Email__FromAddress optional; blank = reset emails only logged

    The last two variable names are the config keys that the prerequisite change adds.

  4. 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).

  5. 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.

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:

  1. hq dashboard. VITE_API_BASE_URL=https://app.nexisapi.xyz/api - the same origin it is served from, with the /api prefix. It serves the business back-office.
  2. platform-admin. VITE_API_BASE_URL=https://admin.nexisapi.xyz - the origin only, no /api (the admin client prepends /admin itself). This is your operator console.
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
  1. Health check. curl -fsS https://app.nexisapi.xyz/api/health should return a small JSON healthy. /api/health/ready returns ready once the schema is migrated.
  2. 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-check Auth__TenantCookiePath and the strip-path setting.
  3. 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).
  4. Owner logs in at https://app.nexisapi.xyz, and the getting-started checklist walks them through setting up the shop.
  • 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-worker is not fatal.
  • The key ring too. Back up the /keys volume 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.