Self-Hosting

Storno.ro can be deployed on your own servers using Docker. Self-hosted instances connect to the Storno.ro SaaS for license validation and plan management, while all your data stays on your infrastructure.

Prerequisites

Quick Start

1. Download the deployment files

mkdir storno && cd storno
curl -O https://raw.githubusercontent.com/stornoro/storno/main/deploy/docker-compose.yml
curl -O https://raw.githubusercontent.com/stornoro/storno/main/deploy/.env.example
curl -O https://raw.githubusercontent.com/stornoro/storno/main/deploy/centrifugo.json

2. Configure environment

cp .env.example .env

Edit .env and fill in the required values:

# Generate secrets (run each one separately)
openssl rand -hex 32  # → APP_SECRET
openssl rand -hex 32  # → JWT_PASSPHRASE
openssl rand -hex 32  # → CENTRIFUGO_API_KEY
openssl rand -hex 32  # → CENTRIFUGO_TOKEN_HMAC_SECRET

# Set your database password
MYSQL_ROOT_PASSWORD=your-strong-password
MYSQL_PASSWORD=your-strong-password

# Paste your license key
LICENSE_KEY=your-license-key-here

3. Start the services

docker compose --profile local-db up -d

This starts five containers:

ServiceDescriptionDefault Port
backendPHP API server (Symfony + Nginx)8900
frontendNuxt SSR web application8901
dbMySQL 8.0 database3306
redisRedis 7 (cache, queues, locks)6379
centrifugoWebSocket server for real-time updates8445

4. Initialize the database

On first startup, create the database schema and mark all migrations as applied:

docker compose exec backend php bin/console doctrine:schema:create
docker compose exec backend php bin/console doctrine:migrations:sync-metadata-storage
docker compose exec backend php bin/console doctrine:migrations:version --add --all --no-interaction

5. Create the first user

docker compose exec backend php bin/console app:user:create \
  [email protected] \
  --password=your-password \
  --admin

JWT keys are generated automatically on first startup — no manual step needed.

6. Access the application

Open http://localhost:8901 in your browser (or your configured domain) and log in.


Environment Variables

Required

VariableDescriptionExample
APP_SECRETSymfony application secretopenssl rand -hex 32
JWT_PASSPHRASEJWT key passphraseopenssl rand -hex 32
MYSQL_ROOT_PASSWORDMySQL root password
MYSQL_PASSWORDMySQL user password
CENTRIFUGO_API_KEYCentrifugo internal API keyopenssl rand -hex 32
CENTRIFUGO_TOKEN_HMAC_SECRETCentrifugo HMAC secretopenssl rand -hex 32
LICENSE_KEYYour Storno.ro license keyGet from SaaS dashboard

Optional

VariableDefaultDescription
BACKEND_PORT8900Backend API port
FRONTEND_PORT8901Frontend web port
CENTRIFUGO_PORT8445WebSocket port
MYSQL_PORT3306MySQL port
MYSQL_DATABASEstornoDatabase name
MYSQL_USERstornoDatabase user
FRONTEND_URLhttp://localhost:8901Public URL for the frontend
PUBLIC_API_BASE/apiHow the browser reaches the API
CORS_ALLOW_ORIGINlocalhost patternCORS allowed origins regex
MAILER_DSNnull://nullSMTP/SES transport DSN
MAIL_FROM[email protected]Sender email address
GOOGLE_CLIENT_IDGoogle OAuth client ID (optional). Mapped to both backend and frontend containers.
GOOGLE_CLIENT_SECRETGoogle OAuth client secret (optional). Mapped to the backend container.
AWS_S3_BUCKETS3 bucket name (local disk used if AWS credentials are not set)
AWS_DEFAULT_REGIONus-east-1AWS region
AWS_ACCESS_KEY_IDAWS access key (set to enable S3 storage)
AWS_SECRET_ACCESS_KEYAWS secret key
LICENSE_SERVER_URLhttps://app.storno.roLicense validation server (do not change)

License Key

Your license key connects your self-hosted instance to your Storno.ro subscription. The key is validated periodically against the SaaS server.

Obtaining a License Key

  1. Log in to app.storno.ro
  2. Go to Settings → Billing
  3. Go to Settings → Licensing and generate a new key
  4. Copy the key and paste it in your .env file as LICENSE_KEY

How Validation Works

  • The license key is a signed JWT validated entirely offline — no network calls to the SaaS server
  • Plan, features, and expiration are embedded in the JWT and verified via RSA signature
  • When the key expires, the instance falls back to the Community (free) plan
  • No user data or business information is ever transmitted

License Sync Command

The license is validated automatically every 6 hours, but you can also run it manually:

docker compose exec backend php bin/console app:license:sync
⚠️

After changing LICENSE_KEY in your .env, you must restart the backend and run the sync command for the new key to take effect:

docker compose restart backend
docker compose exec -T backend php bin/console app:license:sync

Reverse Proxy Setup

For production, place a reverse proxy (Nginx, Caddy, Traefik) in front of the services to handle SSL termination.

Nginx Example

Step 1: Create an HTTP-only config at /etc/nginx/sites-available/storno.conf:

server {
    listen 80;
    server_name app.storno.ro;

    # Frontend
    location / {
        proxy_pass http://127.0.0.1:8901;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # API
    location /api {
        proxy_pass http://127.0.0.1:8900;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        client_max_body_size 50M;
    }

    # WebSocket
    location /connection/websocket {
        proxy_pass http://127.0.0.1:8445;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}

Enable the site:

ln -s /etc/nginx/sites-available/storno.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Step 2: Install SSL with Certbot (Let's Encrypt). Certbot will automatically modify the nginx config to add HTTPS (port 443) and redirect HTTP to HTTPS:

# Install certbot (Ubuntu/Debian)
apt install certbot python3-certbot-nginx

# Obtain certificate and auto-configure nginx for SSL
certbot --nginx -d app.storno.ro

# Verify auto-renewal
certbot renew --dry-run

Step 3: Update your .env to match:

FRONTEND_URL=https://app.storno.ro
PUBLIC_API_BASE=https://app.storno.ro/api
CORS_ALLOW_ORIGIN=^https://app\.storno\.ro$
CENTRIFUGO_ALLOWED_ORIGINS=https://app.storno.ro
💡

PUBLIC_API_BASE is your FRONTEND_URL + /api — it is NOT a separate subdomain. The nginx config above proxies /api requests to the backend container.


Upgrading

To upgrade to the latest version:

make update

Or manually:

docker compose pull
docker compose up -d
docker compose exec backend php bin/console doctrine:migrations:migrate --no-interaction
docker compose exec backend php bin/console cache:clear

Backups

Database

docker compose exec db mysqldump -u root -p storno > backup_$(date +%Y%m%d).sql

Application Data

Back up the Docker volumes for persistent data:

# List volumes
docker volume ls | grep storno

# Backup database volume
docker run --rm -v storno_db_data:/data -v $(pwd):/backup alpine tar czf /backup/db_data.tar.gz /data

# Backup uploaded documents
docker run --rm -v storno_backend_var:/data -v $(pwd):/backup alpine tar czf /backup/backend_var.tar.gz /data

Company-Level Backup

Storno.ro also supports per-company backup/restore through the API:

# Export company data
curl -X POST https://your-instance/api/v1/backup/export \
  -H "Authorization: Bearer {token}" \
  -H "X-Company: {company_uuid}"

# Import company data
curl -X POST https://your-instance/api/v1/backup/import \
  -H "Authorization: Bearer {token}" \
  -H "X-Company: {company_uuid}" \
  -F "[email protected]"

Troubleshooting

License validation fails

# Check license status
docker compose exec backend php bin/console app:license:sync

# Verify LICENSE_KEY is set
docker compose exec backend printenv LICENSE_KEY

# Test connectivity to SaaS
docker compose exec backend curl -s https://app.storno.ro/api/health

Database connection refused

# Check if MySQL is healthy
docker compose ps db

# View MySQL logs
docker compose logs db

WebSocket not connecting

Both the frontend and mobile app derive the WebSocket URL automatically from the current host (e.g. https://factura.yourdomain.comwss://factura.yourdomain.com/connection/websocket). No extra configuration is needed.

  1. Ensure your reverse proxy forwards WebSocket upgrades on /connection/websocket to the Centrifugo container (see the Nginx/Caddy examples above).

  2. Check Centrifugo is running:

    docker compose logs centrifugo
    

View application logs

# All services
docker compose logs -f

# Specific service
docker compose logs -f backend
docker compose logs -f frontend