Running Nocturna on your home server¶
The setup script runs the trading system around the clock on an Ubuntu machine at home, headless or not. It works on a fresh machine, and on one that already runs other services. You reach the dashboard from your phone through Tailscale, and alerts and commands come through Telegram. Paper trading only.
git clone https://github.com/Yog-Sotho/Nocturna-Trading-System.git
cd Nocturna-Trading-System
sudo ./setup.sh --check # what is there, what is missing, what it would change: changes nothing
sudo ./setup.sh # the same, then it asks, and sets everything up
What the setup does¶
It first looks at everything Nocturna needs on this machine, and changes nothing while doing
so:
- The system: Ubuntu version, memory, disk and clock.
- Tools: git, curl and ca-certificates.
- Docker: where it was installed from, whether it starts at boot and runs, and whether
Compose v2 and buildx are there.
- Your user and Docker: whether your account is in the docker group.
- Tailscale: connected, MagicDNS, HTTPS certificates, key expiry, and everything it
already serves, with which entries are public through Funnel.
- Ports: which ones are in use, and by which program.
- The network: whether the services the setup and the stack use answer.
- Your settings: .env, and the app's own verdict on them.
- The stack: each service's state, and the newest backup.
- The dashboard account.
Then it lists the changes it would make, in order, and asks. If something it must not decide for you is in the way, it says what and stops before changing anything (see When it stops before changing anything).
When you say yes, it does each change and verifies it:
- The clock: turns on network time, if the clock is not synchronized.
- Missing packages: installs whichever of git, curl and ca-certificates is missing.
- Docker: installs Docker Engine, Compose and buildx from Docker's repository, if Docker is missing. If Docker is there, it only adds a missing Compose or buildx, and starts the service if it is not running.
- Tailscale: installs it and connects the machine, if it is missing.
- The dashboard's address: serves the dashboard on its own private Tailscale HTTPS port. That is 9443, or the next free port. Funnel can only use ports 443, 8443 and 10000, so this port can never become public, even by mistake.
- The setup compares Tailscale's configuration before and after. If anything other than the new entry changed, it removes the new entry again and stops.
.env: generates the passwords and secrets, and asks you only for what it cannot work out (see What it asks you). It tests each value as you give it.- The stack: builds and starts it, and waits until every service is healthy.
- Your dashboard account: creates it.
- Alerts: sends a test alert to Telegram.
It ends by checking everything again and printing your dashboard's address.
Run it again at any time, for instance after git pull. What is already done stays as it is,
and .env keeps every value unless you change it. When a step fails, the setup says why and
stops; fix that and run it again, and it carries on from there.
What it never does¶
- The
dockergroup: it never adds anyone to it. Membership works as root without a password, so Docker runs throughsudoinstead. - Packages: it never removes one.
- What Tailscale already serves: it never changes those entries or their Funnel, and never
runs
tailscale upon a machine that is already set up. - Your other services: it never touches nginx or any other service. The API listens on
127.0.0.1only, on port 5000 or the next free one. PostgreSQL and Redis stay inside Docker's network. - Trading: it never starts it. You do, when you are ready.
- Secrets: it never prints one, so you can share its reports when you need help.
Before you run it¶
These are the things only you can do. Have them ready, because the setup asks for them.
Alpaca paper keys.
1. Sign in at app.alpaca.markets.
2. Make sure the account switcher at the top left shows your Paper account, not Live.
3. On the paper account's home page, find the API Keys box and generate the keys.
4. Copy both the Key ID and the Secret Key: the secret is shown only once. Paper Key IDs
normally start with PK.
Generating new keys disables the previous pair, wherever it is still in use, so if you still have a working pair, use it.
A Telegram bot.
1. In Telegram, open @BotFather, Telegram's own bot for creating bots, and send /newbot.
2. Give the bot a display name.
3. Give it a username: 5 to 32 letters, digits or underscores, ending in bot.
4. BotFather answers with the token: digits, a colon, then about 35 letters, digits, -
and _ (shortened here: 4839574812:AAFD39kk…). Anyone with the token controls the
bot, so keep it private.
You do not need your chat id: the setup finds it.
healthchecks.io (recommended): it tells you when the machine goes silent.
1. Create a check engine, with Period 1 minute and Grace Time 5 minutes.
2. Create a check backup, with Period 1 day and Grace Time 2 hours.
3. Each check's page shows its ping URL, https://hc-ping.com/<uuid>.
4. To be told on Telegram, open Integrations, choose Telegram, send /start to
@HealthchecksBot, open the link it answers with, and click Connect Telegram.
Tailscale. You need an account at tailscale.com, and the Tailscale app signed in on your phone. If this machine is not on your tailnet yet, the setup installs Tailscale and prints a sign-in link. In the admin console: - DNS page: turn on MagicDNS and, under HTTPS Certificates, select Enable HTTPS. The setup checks both and waits for you if they are off. Machine names and your tailnet's name then appear in a public certificate log. - Machines page: open the menu at the right of the server's row and choose Disable Key Expiry. Otherwise the server drops off your tailnet when its key expires, after 180 days by default. The setup tells you the date if the key expires.
What it asks you¶
- Go ahead? It asks this after showing the list of changes.
- Alpaca: the Key ID and the Secret Key. Both are hidden as you type or paste. It signs in to your paper account to check them, and shows the account number and equity.
- Telegram:
- The bot token, hidden; it checks the token with Telegram.
- It then tells you to open your bot in Telegram and tap Start, finds your chat, and sends a test message.
- Whether the bot may also take commands (
/status,/start_engine,/halt…). - healthchecks.io: the ping URLs of the
engineandbackupchecks. Press Enter to skip one. - Your dashboard account:
- A username: letters, digits and underscores; your login name is offered.
- An email: it only identifies the account, and nothing is sent to it.
- A password, typed twice and hidden: at least 12 characters, with upper case, lower case,
a digit and a symbol (any character that is not a letter, a digit or a space, such as
-,_,!or#). If one is missing, it says which and asks again. - Did the test alert reach your phone?
When you run the setup again, it offers to keep each value already in .env (press Enter).
It tests the Alpaca keys and the bot token again.
When it stops before changing anything¶
| It says | What to do |
|---|---|
| Docker comes from the snap store | It does not remove the snap for you: that would delete its containers and images. If nothing else uses it, run sudo snap remove docker, then run the setup again, and it installs Docker's packages. |
| Packages that conflict with Docker are installed | Other software (Kubernetes, Podman) may need them. Remove them only if nothing uses them, then run the setup again. |
| Tailscale is installed but not logged in, stopped, or its service is not running | Bring it up the way you normally do, with your usual options; the setup does not change your Tailscale settings. Then run the setup again. |
| Tailscale waits for this machine to be approved | Approve it on the Machines page of the admin console. |
| The dashboard is served with Funnel on, so it is public | Turn that off (sudo tailscale funnel --https=<port> off), then run the setup again: it serves the dashboard privately. |
| This machine is not Ubuntu | The setup installs software with Ubuntu's tools. |
Docker with sudo¶
Your account stays out of the docker group, so Docker commands start with sudo:
sudo docker compose ps # each service's state
sudo docker compose logs api --tail 50 # what a service says (api, engine, postgres, redis, backup)
What goes in .env¶
The setup writes .env with permissions that only root can read. If you edit it by hand
(sudo nano .env): one NAME=value per line, with no spaces around = and no quotes needed.
Then run sudo ./setup.sh to check the result and apply it.
| Setting | Needed | What it is | Where it comes from |
|---|---|---|---|
FLASK_SECRET_KEY |
yes | Signs the server's cookies | Generated by the setup (64 random hex characters) |
JWT_SECRET_KEY |
yes | Signs sign-in tokens; must differ from FLASK_SECRET_KEY |
Generated by the setup |
API_KEY_PEPPER |
yes | Mixed into stored API key hashes; changing it voids every API key | Generated by the setup |
DB_PASSWORD |
yes | The database password, fixed when the database is first created; letters and digits only | Generated by the setup; never change it afterwards |
REDIS_PASSWORD |
yes | Password of the rate-limit store; letters and digits only | Generated by the setup |
ALPACA_API_KEY |
yes | The Key ID of your Alpaca paper account | Before you run it |
ALPACA_SECRET_KEY |
yes | Its Secret Key | Before you run it |
ALLOWED_ORIGINS |
yes | The dashboard's address, such as https://<machine>.<tailnet>.ts.net:9443, with no trailing /. Only requests from other websites are checked against it, but the stack refuses to start without it. |
Set by the setup from Tailscale |
TRUSTED_PROXY_COUNT |
yes | 1: Tailscale Serve forwards visitors to the API. With 0, every visitor looks the same, and a few failed logins lock everyone out. |
Set by the setup |
API_PORT |
no | The API's port on this machine, on 127.0.0.1 only (default 5000) |
Set by the setup: 5000, or the next free port |
ENGINE_AUTOSTART |
no | false: after any restart, trading waits for you |
Set by the setup to false |
ALERT_TELEGRAM_BOT_TOKEN |
recommended | Your bot's token | Before you run it |
ALERT_TELEGRAM_CHAT_ID |
recommended | The id of your private chat with the bot (a number) | Found by the setup after you tap Start |
TELEGRAM_COMMANDS |
recommended | true: the bot also takes /status, /start_engine, /halt and the other commands, each change confirmed with a code |
Your answer in the setup |
HEARTBEAT_URL |
recommended | Ping URL of the engine check |
healthchecks.io |
BACKUP_HEARTBEAT_URL |
recommended | Ping URL of the backup check |
healthchecks.io |
docker-compose.yml sets FLASK_ENV, FLASK_DEBUG, TRADING_MODE, ENGINE_MODE,
DATABASE_URL, REDIS_URL, LOG_TO_FILE and ENGINE_ALIVE_FILE itself, so values for them
in .env are ignored, and the setup drops them. Leave API_BIND out, so that the API listens
on this machine only. The risk limits (RISK_*, SIZING_*) are optional. Their defaults are
in the README's safety limits, in the technical reference and in
.env.example, and the setup keeps any you set.
After the setup¶
- Open the dashboard. On your phone, with Tailscale connected, open the address the setup printed and sign in. Tick Stay signed in on this device, and add the page to the home screen. Try it over mobile data, with Wi-Fi off.
- Start trading. Send
/start_engineto the bot, then the/confirm <code>it answers with, or press Start engine in the dashboard./statusshowsrunningonce the engine has loaded the account and the price history. Until trading runs, the healthchecks.ioenginecheck stays silent and reports it after its grace time: pause the check if you start later. - Test a restart. Run
sudo reboot. The "Engine process started … trading waits" alert arrives,/statussaysstopped, and/start_enginestarts trading again. - Keep a copy of the backups. After the first day,
sudo ./setup.sh --checkshowsnewest backup … hours old. Copy./backupsto another machine from time to time: a backup on the same disk is lost with the disk.
Day to day¶
| You want to | Telegram | Dashboard |
|---|---|---|
| See how it is doing | /status, /positions |
Account, Equity, Positions |
| Start after a restart | /start_engine + /confirm |
Start engine |
| Lift a halt | /resume + /confirm |
Resume after halt |
| Stop everything now | /halt or /flatten + /confirm |
Emergency stop |
Pause the engine check on healthchecks.io before you stop trading on purpose.
BIL among the positions is not a trade: it is the idle cash, held in T-bills so it earns interest.
To update to a new version, do it outside US market hours. The engine restarts too, and then waits for you to start trading:
cd ~/Nocturna-Trading-System
git pull
sudo ./setup.sh
RUNBOOK.md covers what each alert means, incidents and restores. Its commands
are written without sudo, so add it in front of each docker command.
The paper soak¶
Before any strategy trades real money, paper trading has to do what the backtest says for at least 60 trading days (see the tolerance in ROADMAP.md). The soak starts when you start trading. Once a week, outside US market hours, run:
cd ~/Nocturna-Trading-System
sudo docker compose exec -T api flask --app src.main soak report > soak-report.md
cat soak-report.md
It reads what the engine recorded, backtests the same days on Alpaca's bars, and reads the paper account's dividends and interest from Alpaca. It places no orders and changes nothing. The first line gives the verdict:
| Verdict | Meaning | What to do |
|---|---|---|
NOT YET |
Fewer than 60 trading days, or too few to compare | Nothing; run it again next week |
REVIEW |
An error, halt, kill-switch trip or unexplained difference with Alpaca is listed | Read the listed entries with RUNBOOK.md; the soak passes only once each one is understood |
FAIL |
Tracking error is 2% or more, or the total return is 1 point or more away from the backtest | Stop and look for the cause before trusting any backtest result |
PASS |
All five checks hold | The soak is done for this lineup |
The report also lists the days without a recorded equity (the engine was stopped or
unreachable), the worst day against the backtest, and the opening-auction fills against the
open as traded. --since 2026-11-02 starts the comparison on a given day, for example after a
restart from scratch, and --json prints the same as JSON.