Operations
Running an installation after docs/INSTALL.md: backups, restore, upgrades, rollback, moving workspaces, monitoring and troubleshooting. Commands assume the Docker Compose setup and are run in the installation folder.
1. Backups
./deploy/backup.sh
writes three files to ./backups (change with BACKUP_DIR):
| File | Content |
|---|---|
db-<stamp>.dump | The whole database, pg_dump custom format |
files-<stamp>.tar.gz | Uploaded files from the files volume |
version-<stamp>.txt | The software version the backup belongs to |
Backups older than 14 days are deleted (KEEP_DAYS, 0 keeps all).
Schedule it nightly, for example with cron:
15 3 * * * cd /opt/rampline && ./deploy/backup.sh >> backups/backup.log 2>&1
Then copy ./backups off the server (rclone or restic to object storage, or another machine). A backup on the same disk does not survive the loss of that disk.
Not in the backup and to be kept separately:
.env, above allSECRET_KEY. Without it stored email and payment credentials cannot be read after a restore (everything else works; enter those credentials again).- Files in an S3 bucket, when
S3_BUCKETis used. Turn on the bucket's versioning or replication. - Caddy's certificates. They are requested again automatically.
Restore one backup into a test machine at least once, so you know it works before you need it.
2. Restore
./deploy/restore.sh 20261018T031500Z
stops the app and worker, replaces the database and the uploaded files with the backup, and starts both again. It asks you to type RESTORE first. Use the code version from version-<stamp>.txt; a newer version applies its migrations on start.
Disaster recovery on a new server
- Install Docker, unpack the same release, copy your saved
.env(sameSECRET_KEYand database passwords). - Copy the backup files into
./backups. docker compose up -d db, then./deploy/restore.sh <stamp>.docker compose --profile proxy up -d, point DNS at the new server.docker compose exec app npm run install:check -- --smoke.
3. Upgrades
Releases are applied in place:
# unpack the new release over a copy of the installation folder, keep .env
./deploy/upgrade.sh
The script makes a backup, builds the new image tagged with the new version, restarts app and worker (the app applies new migrations on start, under a lock) and runs the installation check. Afterwards set RAMPLINE_VERSION=<new version> in .env.
Before upgrading, read the release notes for changes to environment variables. node scripts/db.mjs status shows which migrations a version will apply (exit code 3 means some are pending).
4. Rollback
Database migrations only go forward. Rolling back therefore means restoring the backup that upgrade.sh made right before the upgrade:
- Put the previous release back in the installation folder (or set
RAMPLINE_VERSIONto the previous image tag if it still exists:docker image ls rampline). ./deploy/restore.sh <stamp of the pre-upgrade backup>.docker compose exec app npm run install:check.
Data created between the upgrade and the rollback is lost. If that matters, export the affected workspaces first (section 5) and import them again after the rollback only if their migrations allow it; the import refuses exports from a newer schema.
The installation check warns when the database has migrations the running code does not know, which is the sign of a version mismatch after a partial rollback.
5. Moving a workspace between installations
When a client leaves a reseller, or a business moves to its own server:
- In the app: the workspace owner downloads everything under Settings, Data export. The operator or agency directly above a workspace downloads it on the client's page (Export data). Each download is recorded in the workspace's audit log.
- On the command line:
docker compose exec app npm run workspace -- export <workspace-slug> /tmp/export.json.gz(thendocker compose cp app:/tmp/export.json.gz .).
To import on the target installation:
docker compose cp export.json.gz app:/tmp/export.json.gz
docker compose exec app npm run workspace -- import /tmp/export.json.gz --parent <operator-or-agency-slug>
Without --parent the workspace goes under the house operator. Ids stay the same, so published pages, links and tracking keep working once DNS points at the new server. The import refuses a workspace that already exists and exports from a newer software version.
What does not travel, by design: passwords and sessions (users are matched by email, new ones are created without a password and need an invitation or password reset), payment and email provider credentials (they are encrypted with the old installation's key), access granted to the old agency or operator, platform plans and invoices of the old seller. The import prints what needs doing: DNS for custom domains, Stripe keys and webhook, email settings.
Limits: the export is built in memory, so very large workspaces (several GB of uploads) should be exported with --no-media and their files copied from the storage volume or bucket separately. User references inside automation steps (for example a fixed owner for new leads) keep the old user id when that person already existed on the target under another id; check those automations after a move.
6. Monitoring
| Where | What |
|---|---|
GET /api/health | 200 with database state, migration count and worker heartbeat; 503 when the database is unreachable. Point your uptime monitor at it. |
| Platform, Installation | Environment check with what to fix. The same list: npm run install:check. |
| Insights, Health (per workspace) | Broken pages, failing checkouts, domains, stopped forms, dead jobs, email bounces |
docker compose logs -f app worker | Application and job logs |
The worker writes a heartbeat regularly while it runs. A heartbeat older than two minutes means jobs (emails, automations, invoices) are not running.
7. Troubleshooting
| Symptom | Cause and fix |
|---|---|
/setup says the link is not valid | The token changed. docker compose exec app npm run setup:token prints a new link. |
| Sign-in works but you are signed out on every page | The site is served over http while NODE_ENV=production sets secure cookies. Use the https address (Caddy profile or your proxy). |
App container restarts, log shows APP_DB_PASSWORD and SERVICE_DB_PASSWORD must be set | Fill them in .env. |
Log shows rampline_service has no BYPASSRLS | Managed database: run alter role rampline_service bypassrls; as its admin. |
Pages and sign-in fail with password authentication failed for user "rampline_app" | .env passwords changed after the database was created. The app sets them on start: docker compose restart app worker. |
| A migration fails on start | The app stops and the log names the migration; nothing of that migration was applied. Restore the pre-upgrade backup or fix the cause (often disk space) and restart. |
| Emails are not delivered | Installation check, System email. Workspace mail: Settings, Email sending shows the provider's last error. Check SPF and DKIM at your DNS host. |
| Custom domain shows a certificate error | DNS does not point at this server yet, or the domain is not added in the workspace. Caddy retries on the next visit; docker compose logs caddy shows the reason. |
| Jobs wait, nothing is sent | Worker not running: docker compose up -d worker, then docker compose logs worker. |
| "No license is activated", new workspaces refused | Independent edition without a valid license. Activate it under Platform, Licenses. Existing data keeps working. |
| Stripe payments are not recorded | The Stripe webhook must point at https://<your address>/api/webhooks/stripe/<account id> (shown under Settings, Payments) with the signing secret entered there. |
8. Environment reference
.env.production.example documents every variable with its purpose.
9. Railway and similar platforms
Tested on 2026-10-19 (docs/acceptance/TEST_SERVER.md).
- Services: one app service from the Dockerfile with
RUN_WORKER=1(app and worker in one container, because a volume belongs to one service), a PostgreSQL service, a volume at/app/storage,RAILWAY_RUN_UID=0(the volume is mounted root-owned). - Variables:
DB_NAME=rampline,DATABASE_ADMIN_URLpointing at thepostgresdatabase with the admin user (postgresql://${{Postgres.PGUSER}}:${{Postgres.PGPASSWORD}}@${{Postgres.PGHOST}}:${{Postgres.PGPORT}}/postgres),DATABASE_URLandSERVICE_DATABASE_URLwith the role passwords and databaserampline,APP_DB_PASSWORD,SERVICE_DB_PASSWORD,SECRET_KEY,SETUP_TOKEN,APP_URL,TRUSTED_PROXY_HOPS=1. - Deploy:
railway upfrom a clean export (git archive HEAD | tar -x -C <folder>), so local data never ships. - Domains:
railway domain <host>for the app and for each customer domain; set the CNAME and Railway's_railway-verifyTXT record at the DNS provider with the proxy off. Create the records before anyone opens the address: routers cache "unknown domain" answers. - Backup: database:
railway ssh --service Postgres "pg_dump -U postgres -Fc -d rampline -f /tmp/rampline.dump", then copy it off the platform; files:railway ssh --service app "tar czf /tmp/storage.tgz -C /app storage", then copy it off. Keep theSECRET_KEYwith the backups. - Restore:
createdba new database andpg_restorethe dump into it, pointDB_NAME,DATABASE_URLandSERVICE_DATABASE_URLat it (the start command sets the role passwords), extract the file archive into/app/storage.