a2NSoft ERP · Deployment · Document 01

Installing a2NSoft on Ubuntu Server

A complete, line-by-line installation. Every command is written out. You do not need to know Linux — follow the steps in order and do not skip one.

Operating system
Ubuntu 24.04 LTS
Database
PostgreSQL 16
Runtime
Python 3.12 · Node 22
Web
Nginx + Let's Encrypt
Time needed
45–60 minutes
00

What you need before you start

Five things. Get all five ready now — stopping halfway to order a domain name is how installations go wrong.

You needDetails
A serverUbuntu 24.04 LTS, minimum 2 CPU, 4 GB RAM, 40 GB disk. For 25 users or more, use 4 CPU and 8 GB. Any provider works — DigitalOcean, Hetzner, AWS Lightsail, or a machine in your own office.
Administrator accessA username and password (or SSH key) for the server, and that user must be able to run sudo. Your hosting provider gives you this when the server is created.
A domain nameFor example erp.yourcompany.com. In your domain provider's control panel, create an A record pointing that name at your server's IP address. Do this first — it can take up to an hour to take effect, and the HTTPS certificate in step 13 will not work until it has.
The application codeAccess to github.com/sidmectech/a2NSoft-ERP. This is a private repository, so you need either a deploy key or a personal access token. Step 05 covers both.
An email addressUsed once, to register the free HTTPS certificate. Renewal warnings are sent there.

How to read this guide

Every grey box is something you type into the server and press Enter. The $ at the start is the server prompt — do not type it. Use the Copy button rather than retyping, because a single wrong character will produce an error.

Wherever you see erp.example.com, replace it with your own domain. Wherever you see a password placeholder, replace it with your own. Nothing else should be changed.

Do the steps in order

Each step depends on the one before it. If a command returns an error, stop and fix it before continuing — do not carry on hoping it sorts itself out. Section 19 lists the errors you are most likely to meet.

01

Connect to the server and update it

Connect

On Windows, open PowerShell. On a Mac, open Terminal. Type this, replacing the address with your server's IP:

On your own computer
$ ssh root@203.0.113.10

The first time you connect it asks whether to trust the server. Type yes and press Enter, then enter the password your provider gave you. You are now typing commands on the server.

Update Ubuntu

This installs the latest security patches. It is the first thing to do on any new server.

On the server
$ sudo apt update
$ sudo apt upgrade -y

This takes a few minutes. If it finishes with a message asking you to restart, do it — you will be disconnected for about 30 seconds, then reconnect with the same ssh command as above.

Only if a restart was requested
$ sudo reboot
02

Create the account the software runs as

The ERP must not run as the administrator. It gets its own account with no password and no ability to log in — so that if the application is ever compromised, the attacker gains almost nothing.

On the server
$ sudo adduser --system --group --home /opt/a2nsoft --shell /usr/sbin/nologin a2nsoft

This creates the user a2nsoft and the folder /opt/a2nsoft, which is where the application will live for the rest of this guide.

What each part means --system a service account, not a person --group give it a group of its own --home the folder it owns --shell nologin: nobody can sign in as this user
03

Install the system software

Four things: Python (runs the application), PostgreSQL (stores the data), Node.js (builds the screens), and Nginx (serves it to browsers safely).

Python, PostgreSQL, Nginx and tools

Ubuntu 24.04 already includes Python 3.12, which is the version a2NSoft needs, so there is nothing extra to add for it.

On the server
$ sudo apt install -y python3.12 python3.12-venv python3-pip \
    postgresql postgresql-contrib \
    nginx git curl ca-certificates ufw

Node.js 22

The version Ubuntu ships is too old to build the a2NSoft screens, which require Node 22.12 or newer. Add the official Node repository first:

On the server
$ curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
$ sudo apt install -y nodejs

Check that everything arrived

On the server
$ python3.12 --version
$ node --version
$ psql --version
$ nginx -v
You should see something close to Python 3.12.3 v22.14.0 (22.12 or higher) psql (PostgreSQL) 16.x nginx version: nginx/1.24.0

If any command says command not found, that package did not install. Run the install command for it again and read the error.

04

Create the database

a2NSoft stores everything in PostgreSQL and will refuse to start without it. There is no fallback — by design, because a silently wrong database is worse than none.

Invent a database password

Generate one now and keep it somewhere safe; you will paste it twice — here, and in step 08.

On the server
$ openssl rand -base64 24
xK8pR2mQ7vN4tL9wZ5hB3cF6dG1jY0sA ← yours will differ; copy it

Create the user and the database

Replace PASTE_DB_PASSWORD_HERE with what you just generated, keeping the single quotes.

On the server
$ sudo -u postgres psql -c "CREATE ROLE a2n_app WITH LOGIN PASSWORD 'PASTE_DB_PASSWORD_HERE';"
$ sudo -u postgres psql -c "CREATE DATABASE a2n_production OWNER a2n_app ENCODING 'UTF8';"
Expected CREATE ROLE CREATE DATABASE

Confirm the login works

On the server
$ PGPASSWORD='PASTE_DB_PASSWORD_HERE' psql -h 127.0.0.1 -U a2n_app -d a2n_production -c "select 1;"

A small table containing 1 means the database is ready. password authentication failed means the password does not match — create the role again with the correct one.

The database stays private

PostgreSQL only listens on the server itself (127.0.0.1). Nothing on the internet can reach it, and that is exactly how it should stay. Do not change listen_addresses.

05

Download the application

Give yourself access to the private repository

Choose one of the two methods. A deploy key is the better choice for a server because it is read-only and tied to this machine alone.

Option A — deploy key (recommended)

On the server
$ sudo -u a2nsoft -H ssh-keygen -t ed25519 -f /opt/a2nsoft/.ssh/id_ed25519 -N ""
$ sudo cat /opt/a2nsoft/.ssh/id_ed25519.pub

Copy the line that prints. In GitHub, open the repository → Settings → Deploy keys → Add deploy key, paste it, leave "Allow write access" unticked, and save.

Option B — personal access token

In GitHub → Settings → Developer settings → Personal access tokens, create a token with repo read access. You will paste it as the password when git asks.

Fetch the code

Deploy key
$ sudo -u a2nsoft -H git clone git@github.com:sidmectech/a2NSoft-ERP.git /opt/a2nsoft/app
Access token
$ sudo -u a2nsoft -H git clone https://github.com/sidmectech/a2NSoft-ERP.git /opt/a2nsoft/app

The application now lives in /opt/a2nsoft/app. Check it arrived:

On the server
$ ls /opt/a2nsoft/app
README.md fastapi_app frontend modules requirements.txt scripts ...
06

Install the Python packages

These go into a private folder belonging to the application, not into the system — so they can never conflict with anything else on the server.

On the server
$ sudo -u a2nsoft -H python3.12 -m venv /opt/a2nsoft/venv
$ sudo -u a2nsoft -H /opt/a2nsoft/venv/bin/pip install --upgrade pip
$ sudo -u a2nsoft -H /opt/a2nsoft/venv/bin/pip install -r /opt/a2nsoft/app/requirements.txt

The last command takes two to five minutes and prints a long list. The final line should read Successfully installed ....

If it fails while building a package

Install the compiler toolchain and try the last command again:

sudo apt install -y build-essential python3.12-dev libpq-dev

07

Build the web interface

The screens users see are written in React and have to be compiled once into plain files. The application then serves those files itself — there is no second web server to manage.

On the server
$ cd /opt/a2nsoft/app/frontend
$ sudo -u a2nsoft -H npm ci
$ sudo -u a2nsoft -H npm run build

npm ci downloads the exact package versions the project was tested with; it takes a few minutes. npm run build checks the code and produces the finished files.

Confirm the build produced something

On the server
$ ls /opt/a2nsoft/app/frontend/dist
assets index.html ← both must be present

If dist is missing or empty the build failed. Scroll up through the output to the first line containing error — that is the real cause; everything after it is noise.

You repeat this step on every update

Whenever you pull a new version of a2NSoft (step 17), run npm ci and npm run build again, or users will keep seeing the old screens.

08

Write the settings file

One file holds the database password and the key that signs user sessions. It lives outside the code folder, is readable only by the service, and is never committed to git.

Generate the secret key

On the server
$ openssl rand -base64 48

Copy the result. This key signs every user's session cookie; if it changes, everyone is signed out.

Create the file

On the server
$ sudo mkdir -p /etc/a2nsoft
$ sudo nano /etc/a2nsoft/a2nsoft.env

A text editor opens. Paste the block below, then replace the two placeholder lines with your real values.

Paste into the editor
# a2NSoft production settings
A2N_DEBUG=0
A2N_SECRET_KEY=PASTE_THE_48_CHARACTER_SECRET_HERE

# Database (step 04)
A2N_DB_ENGINE=postgresql
A2N_DB_NAME=a2n_production
A2N_DB_USER=a2n_app
A2N_DB_PASSWORD=PASTE_DB_PASSWORD_HERE
A2N_DB_HOST=127.0.0.1
A2N_DB_PORT=5432
A2N_DB_SSLMODE=prefer

# Connection pool and safety timeouts
A2N_DB_POOL_MIN=2
A2N_DB_POOL_MAX=10
A2N_DB_STATEMENT_TIMEOUT_MS=30000
A2N_DB_LOCK_TIMEOUT_MS=15000
A2N_DB_IDLE_TX_TIMEOUT_MS=30000
A2N_DB_APP_NAME=a2nsoft

# Printing service (step 15). Leave as-is if you skip printing.
A2N_GOTENBERG_URL=http://127.0.0.1:3000
A2N_PRINT_TIMEOUT_SECONDS=45

Save and close: press Ctrl+O, then Enter, then Ctrl+X.

Lock the file down

It contains two passwords, so only the service account may read it.

On the server
$ sudo chown root:a2nsoft /etc/a2nsoft/a2nsoft.env
$ sudo chmod 640 /etc/a2nsoft/a2nsoft.env
A2N_DEBUG must stay 0

With A2N_DEBUG=0 the session cookie is marked secure, meaning browsers only send it over HTTPS. That is what makes the login safe — and it is also why signing in will not work until the certificate in step 13 is installed. Setting it to 1 on a live server turns that protection off and enables development-only tools. Never do it.

Two settings in .env.example do nothing

A2N_ALLOWED_HOSTS and A2N_CSRF_TRUSTED_ORIGINS are left over from an earlier Django version of this product and are not read by the current application. Host filtering is done by Nginx in step 12 instead. They are omitted above on purpose.

09

Create the tables

The database exists but is empty. This builds every table the ERP needs.

On the server
$ cd /opt/a2nsoft/app/fastapi_app
$ sudo -u a2nsoft -H --preserve-env bash -c 'set -a; . /etc/a2nsoft/a2nsoft.env; set +a; \
    /opt/a2nsoft/venv/bin/python -m alembic upgrade head'
INFO [alembic.runtime.migration] Running upgrade -> 0001, initial schema INFO [alembic.runtime.migration] Running upgrade 0001 -> 0002, ... (one line per migration; no line containing ERROR)

The middle part of that command loads the settings file so the application knows which database to use. You will see the same pattern again — a2NSoft deliberately does not read the settings file by itself, so that starting it from the wrong place fails loudly instead of quietly opening a different database.

Confirm the tables exist

On the server
$ sudo -u postgres psql -d a2n_production -c "\dt" | head -20

You should see a list of tables with names beginning foundation_, erp_, a2n_inventory_ and a2n_crm_.

10

Create the first administrator

Read this before running the command

a2NSoft ships two account-creation scripts — bootstrap_local and dev_user — and both refuse to run unless A2N_DEBUG=1. That is correct behaviour: they create demonstration data and weak passwords that must never reach a live server. It does, however, mean the product currently has no supported way to create the first real administrator.

The command below fills that gap. It uses the application's own password hashing and company creation code — the same functions the built-in scripts use — and creates exactly one account with a password you choose. Nothing else is inserted.

This should become a proper script in the repository (fastapi_app/scripts/create_admin.py). Until it does, this is the documented procedure, and it is the only step of this guide that is not a plain command.

Choose the password first

Minimum twelve characters; sixteen or more is better. You will type it into the command below, and change it after the first sign-in if you prefer.

On the server — edit the three marked lines first
$ cd /opt/a2nsoft/app/fastapi_app
$ sudo -u a2nsoft -H bash -c 'set -a; . /etc/a2nsoft/a2nsoft.env; set +a; \
/opt/a2nsoft/venv/bin/python - <<"PY"
import sys; sys.path.insert(0, ".")
from app.db import SessionLocal
from app.models import User
from app.security import hash_password
from app.services.companies import create_company
from app.services.util import now

USERNAME = "admin"                        # change if you want
PASSWORD = "PutYourLongPasswordHere"      # CHANGE THIS
COMPANY  = "Your Company Name LLC"        # CHANGE THIS

db = SessionLocal()
if db.query(User).first() is not None:
    print("A user already exists. Nothing was changed."); raise SystemExit(0)
user = User(username=USERNAME, password=hash_password(PASSWORD),
            first_name="System", last_name="Administrator", is_active=True,
            is_staff=False, is_superuser=False, date_joined=now(),
            can_create_company=True)
db.add(user); db.flush()
create_company(db, user, {"code": "AE01", "legal_name": COMPANY,
                          "business_mode": "both", "city": "Dubai"})
db.commit()
print("Created administrator:", USERNAME)
PY'
Created administrator: admin

If it prints A user already exists, an account was created earlier — sign in with that one instead. The command never overwrites an existing account.

Change the company details later

Legal name, address, TRN and the rest are all editable inside the application once you sign in. The values above only need to be good enough to get you through the front door.

11

Keep it running — install it as a service

A service starts a2NSoft when the server boots, restarts it if it ever stops, and writes its log where you can read it. Without this, the ERP would stop the moment you closed your terminal window.

Create the service file

On the server
$ sudo nano /etc/systemd/system/a2nsoft.service

Paste this exactly as it is — nothing in it needs changing:

Paste into the editor
[Unit]
Description=a2NSoft ERP
After=network.target postgresql.service
Requires=postgresql.service

[Service]
Type=exec
User=a2nsoft
Group=a2nsoft
WorkingDirectory=/opt/a2nsoft/app/fastapi_app
EnvironmentFile=/etc/a2nsoft/a2nsoft.env
ExecStart=/opt/a2nsoft/venv/bin/python -m uvicorn app.main:app \
    --host 127.0.0.1 --port 8000 --workers 3 \
    --proxy-headers --forwarded-allow-ips=127.0.0.1
Restart=always
RestartSec=5
TimeoutStopSec=30

# Hardening: the service can only write where it must.
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/a2nsoft/app/data
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictSUIDSGID=true

[Install]
WantedBy=multi-user.target

Save and close (Ctrl+O, Enter, Ctrl+X).

Two settings worth understanding --workers 3 three copies serve requests at once. Use (CPU cores x 2) + 1, so 3 for a 1-core server, 5 for 2 cores, 9 for 4 cores. --proxy-headers lets the app see that the visitor arrived over HTTPS via Nginx. Without it, secure cookies are rejected and nobody can sign in.

Start it

On the server
$ sudo mkdir -p /opt/a2nsoft/app/data
$ sudo chown a2nsoft:a2nsoft /opt/a2nsoft/app/data
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now a2nsoft
$ sudo systemctl status a2nsoft
● a2nsoft.service - a2NSoft ERP Loaded: loaded (/etc/systemd/system/a2nsoft.service; enabled) Active: active (running) since ... "enabled" = starts on boot. "active (running)" = running now.

Check the application answers

On the server
$ curl http://127.0.0.1:8000/api/v1/health
{"status":"ok","service":"a2nsoft","version":"0.3.0-dev"}

This is the health check. It also queries the database, so a successful reply proves the application and the database connection are working. If it fails, read the log:

On the server
$ sudo journalctl -u a2nsoft -n 50 --no-pager
12

Nginx reverse proxy

a2NSoft listens only on the server itself. Nginx sits in front of it, accepts visitors from the internet, and passes their requests through. It is also what will hold the HTTPS certificate.

Create the site file

On the server
$ sudo nano /etc/nginx/sites-available/a2nsoft

Paste the block below and change erp.example.com to your domain — in both places.

Paste into the editor
server {
    listen 80;
    listen [::]:80;
    server_name erp.example.com;

    # Invoices and attachments. Raise if you upload larger files.
    client_max_body_size 25m;

    access_log /var/log/nginx/a2nsoft.access.log;
    error_log  /var/log/nginx/a2nsoft.error.log;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;

        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        # Tells the app the visitor used HTTPS. Sign-in fails without it.
        proxy_set_header X-Forwarded-Proto $scheme;

        # PDF generation can take a while on large documents.
        proxy_connect_timeout 60s;
        proxy_send_timeout    120s;
        proxy_read_timeout    120s;
    }

    # Compiled screens never change without a new filename, so cache them hard.
    location /assets/ {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Enable it

On the server
$ sudo ln -s /etc/nginx/sites-available/a2nsoft /etc/nginx/sites-enabled/
$ sudo rm -f /etc/nginx/sites-enabled/default
$ sudo nginx -t
$ sudo systemctl reload nginx
nginx: configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful

Only reload once you see successful. If the test fails it names the file and line number — that is where the typo is.

Do not try to sign in yet

Your domain now shows the a2NSoft sign-in screen, but the login will not complete over plain http://. That is the secure-cookie protection from step 08 doing its job. Finish step 13 first.

13

Install the HTTPS certificate

A free certificate from Let's Encrypt, installed and renewed automatically. This is not optional — a2NSoft will not let anyone sign in without it.

Check the domain first

Run dig +short erp.example.com (or check on your phone's browser). It must return your server's IP address. If it returns nothing, the DNS record from step 00 has not taken effect yet — wait and try again. Requesting a certificate too early wastes one of your five attempts per week.

Install and run Certbot

On the server
$ sudo apt install -y certbot python3-certbot-nginx
$ sudo certbot --nginx -d erp.example.com

It asks three things:

  1. Your email address — for expiry warnings.
  2. Agreement to the terms — type Y.
  3. Whether to share your address with the EFF — N is fine.

Certbot then edits your Nginx file for you: it adds the certificate and redirects all http:// traffic to https://. You do not need to change anything by hand.

Successfully received certificate. Deploying certificate Successfully deployed certificate for erp.example.com Congratulations! You have successfully enabled HTTPS

Confirm renewal is automatic

Certificates last 90 days. Ubuntu renews them for you; this proves it.

On the server
$ sudo certbot renew --dry-run
$ sudo systemctl status certbot.timer
Congratulations, all simulated renewals succeeded ● certbot.timer - Run certbot twice daily active (waiting)

Sign in

Open https://erp.example.com in a browser and sign in with the username and password from step 10. The installation is complete.

14

Close everything except the front door

The firewall allows only what is needed: your SSH connection and web traffic. Everything else — including the database — becomes unreachable from outside.

On the server
$ sudo ufw allow OpenSSH
$ sudo ufw allow 'Nginx Full'
$ sudo ufw --force enable
$ sudo ufw status
Status: active OpenSSH ALLOW Anywhere Nginx Full ALLOW Anywhere
Allow OpenSSH before enabling the firewall

The commands above are in the right order for a reason. Enabling the firewall without the first line would cut off your own connection and lock you out of the server — recoverable only through your hosting provider's emergency console.

15

The printing service (optional)

a2NSoft produces invoices and reports as PDFs through a separate renderer called Gotenberg, which runs in Docker. Skip this section if you do not need PDF output yet — everything else works without it.

Install Docker

On the server
$ sudo apt install -y docker.io docker-compose-v2
$ sudo systemctl enable --now docker

Build and start the renderer

On the server
$ cd /opt/a2nsoft/app
$ sudo docker compose -f compose.printing.yml build
$ sudo docker compose -f compose.printing.yml up -d
$ sudo docker compose -f compose.printing.yml ps
NAME STATUS app-gotenberg-1 Up 20 seconds (healthy)

Check it responds

On the server
$ curl http://127.0.0.1:3000/health
Never expose the printing service

It is bound to 127.0.0.1 so only a2NSoft on this same machine can reach it, and it is locked down hard — no JavaScript, no network access, read-only filesystem. Do not add it to Nginx, do not open port 3000 in the firewall, and do not change the port binding. A reachable renderer is a way into your server.

Full detail is in the repository at docs/PRINTING.md.

16

Automatic backups

Do this on day one. A backup you set up next month does not protect you today.

Create the backup script

On the server
$ sudo nano /usr/local/bin/a2nsoft-backup.sh
Paste into the editor
#!/bin/bash
set -euo pipefail
DEST=/var/backups/a2nsoft
STAMP=$(date +%Y%m%d-%H%M%S)
mkdir -p "$DEST"

# Compressed custom-format dump: the fastest to restore selectively.
sudo -u postgres pg_dump -Fc a2n_production > "$DEST/a2n_production-$STAMP.dump"

# Keep the settings file too - without it the dump cannot be opened.
cp /etc/a2nsoft/a2nsoft.env "$DEST/a2nsoft.env-$STAMP"

# Keep 14 days.
find "$DEST" -type f -mtime +14 -delete
echo "Backup written: $DEST/a2n_production-$STAMP.dump"
On the server
$ sudo chmod 750 /usr/local/bin/a2nsoft-backup.sh
$ sudo /usr/local/bin/a2nsoft-backup.sh

Run it every night at 2am

On the server
$ echo '0 2 * * * root /usr/local/bin/a2nsoft-backup.sh >> /var/log/a2nsoft-backup.log 2>&1' \
    | sudo tee /etc/cron.d/a2nsoft-backup
$ sudo chmod 644 /etc/cron.d/a2nsoft-backup
A backup on the same server is not a backup

If the machine is lost, the backups go with it. Copy /var/backups/a2nsoft to somewhere else — object storage, another server, or an office machine — on a schedule. And restore one into a test database at least once, before you ever need to do it under pressure.

Restoring

On the server
$ sudo systemctl stop a2nsoft
$ sudo -u postgres dropdb a2n_production
$ sudo -u postgres createdb -O a2n_app a2n_production
$ sudo -u postgres pg_restore -d a2n_production /var/backups/a2nsoft/a2n_production-YYYYMMDD-HHMMSS.dump
$ sudo systemctl start a2nsoft
17

Updating to a new version

Always back up first. The order below matters: new code, new packages, new screens, new tables, then restart.

On the server
$ sudo /usr/local/bin/a2nsoft-backup.sh

$ cd /opt/a2nsoft/app
$ sudo -u a2nsoft -H git pull

$ sudo -u a2nsoft -H /opt/a2nsoft/venv/bin/pip install -r requirements.txt

$ cd /opt/a2nsoft/app/frontend
$ sudo -u a2nsoft -H npm ci
$ sudo -u a2nsoft -H npm run build

$ cd /opt/a2nsoft/app/fastapi_app
$ sudo -u a2nsoft -H bash -c 'set -a; . /etc/a2nsoft/a2nsoft.env; set +a; \
    /opt/a2nsoft/venv/bin/python -m alembic upgrade head'

$ sudo systemctl restart a2nsoft
$ curl http://127.0.0.1:8000/api/v1/health

Users are signed out for two or three seconds during the restart. Any work saved before it is safe; anything half-typed on a screen is not.

If an update goes wrong

Go back to the previous version with git log --oneline -5 to find the commit before the update, then sudo -u a2nsoft -H git checkout <commit>, rebuild, and restart. If the database was already migrated, restore the backup you took at the start — that is what it is for.

18

Everyday commands

The eight you will actually use. Keep this page bookmarked.

CommandWhat it does
sudo systemctl status a2nsoftIs it running?
sudo systemctl restart a2nsoftRestart it (after a settings change).
sudo systemctl stop a2nsoftStop it — for maintenance or a restore.
sudo systemctl start a2nsoftStart it again.
sudo journalctl -u a2nsoft -fWatch the log live. Ctrl+C to stop watching.
sudo journalctl -u a2nsoft -n 100 --no-pagerThe last 100 log lines — the first thing to check when something breaks.
curl http://127.0.0.1:8000/api/v1/healthIs the application and its database healthy?
sudo /usr/local/bin/a2nsoft-backup.shTake a backup right now.
19

If something goes wrong

The log is always the answer: sudo journalctl -u a2nsoft -n 100 --no-pager. These are the messages you are most likely to find in it.

What you seeWhat to do
A2N_DB_ENGINE must be 'postgresql' and is not set The service started without its settings. Check EnvironmentFile=/etc/a2nsoft/a2nsoft.env is in the service file and that the file is readable by the a2nsoft group (step 08).
password authentication failed for user "a2n_app" A2N_DB_PASSWORD does not match the database. Reset it: sudo -u postgres psql -c "ALTER ROLE a2n_app WITH PASSWORD 'new';" then put the same value in the settings file and restart.
502 Bad Gateway in the browser Nginx is up but a2NSoft is not. Run sudo systemctl status a2nsoft and read the log.
The sign-in page reloads and never signs in The secure cookie is being dropped. Confirm you are on https://, that proxy_set_header X-Forwarded-Proto $scheme; is present in the Nginx file (step 12), and that --proxy-headers is in the service file (step 11).
A blank white page after signing in The screens were never built, or were built before the last update. Re-run npm ci and npm run build in frontend (step 07), then reload the browser with Ctrl+Shift+R.
Too many sign-in attempts Protection against password guessing: 15 attempts per username and 60 per address, in a 15-minute window. Wait it out.
Printing fails or times out The renderer is not running. sudo docker compose -f compose.printing.yml ps from /opt/a2nsoft/app, then up -d if it is down (step 15).
Certbot: challenge failed / DNS problem The domain does not yet point at this server. Check the A record, wait, and run sudo certbot --nginx -d erp.example.com again.
Disk full df -h to confirm, then clear old backups and logs: sudo journalctl --vacuum-time=14d.
20

Status of this procedure

What this document is, and what it is not. Read it before deploying for a paying customer.

This is the first production deployment procedure for a2NSoft

The repository states plainly that no production deployment has been completed. Every command here is derived from the project's own scripts, settings and code — scripts/setup.ps1, scripts/dev.ps1, scripts/setup_postgres.ps1, .env.example, app/config.py, app/main.py and compose.printing.yml — translated from Windows development to an Ubuntu server. It has not yet been executed end to end on a clean Ubuntu machine.

Run it once on a throwaway server before you run it for a customer, and correct anything this document gets wrong.

Known gaps, carried here rather than hidden

GapConsequence
No production account-creation scriptBoth shipped scripts refuse to run outside development. Step 10 works around it with an inline command; a real create_admin script belongs in the repository.
Backup recovery not certifiedThe project's own backup tool covers local SQLite only. The PostgreSQL procedure in step 16 is standard practice, not a tested product feature. Test your restore.
Two dead settingsA2N_ALLOWED_HOSTS and A2N_CSRF_TRUSTED_ORIGINS appear in .env.example but are not read. Nginx does host filtering instead.
Not all modules completeThe product is at version 0.3.0-dev. See docs/MODULE-DELIVERY.md for what is implemented.

Recommended before the first customer install

  1. Add fastapi_app/scripts/create_admin.py so step 10 becomes one command.
  2. Run this guide start to finish on a clean Ubuntu 24.04 server and record what differed.
  3. Restore a backup into an empty database and confirm the ERP starts against it.
  4. Decide the update window and who is allowed to run step 17.
a2NSoft ERP · Installation Guide · Document 01 of the deployment set
Written against a2NSoft 0.3.0-dev for Ubuntu 24.04 LTS.
Repository: github.com/sidmectech/a2NSoft-ERP