Skip to Content
Ranlanka CRM documentation — pre-release, subject to change.

VPS Setup & CI/CD

Image publishing

On every push to the main branch (or manual trigger), a two-job pipeline builds and publishes the Docker image:

  • build — compiles the JAR via Maven only, no Docker involved, uploads it as a workflow artifact.
  • push — downloads the JAR, builds a multi-arch image (linux/amd64 and linux/arm64, via Buildx + QEMU), and pushes both a latest tag and a commit-SHA tag to the registry.

Split into two jobs so the build step never needs registry credentials. Multi-arch matters if your VPS provider’s free/cheap tier runs on ARM (e.g. Oracle Cloud’s Always-Free Ampere shape) rather than the x86_64 a CI runner builds on by default — pulling a single-arch amd64 image onto an arm64 host fails immediately with exec format error.

Required secrets: a registry username and an access token (not your account password).

Deploying to the VPS

A second, manual-trigger-only workflow (workflow_dispatch) handles the actual deploy — decoupled from image publishing, so pushing a new image never auto-deploys. Triggering it runs, in order:

Ensure Docker is installed and current

SSHes in and installs Docker via the distro’s official apt repository (not a generic convenience install script — some of those install extra packages that aren’t published for every distro/architecture and fail the whole install). Upgrades it if already present.

Copy deploy files to the server

Copies docker-compose.yml, the logging shipper config, and the backup script to a fixed directory on the VPS (e.g. ~/ranlanka).

Register the backup cron

Makes the backup script executable and installs its crontab entry idempotently (removes any existing entry for the same script first, so re-running the deploy doesn’t create duplicate cron lines).

Write .env and start the stack

Writes a fresh .env file on the VPS from GitHub secrets (chmod 600), then runs docker compose pull && docker compose up -d — optionally with the cloud-logging profile enabled, depending on a repo variable (see Logging & Monitoring).

Required secrets:

SecretPurpose
<HOST>_HOSTThe instance’s public IP/hostname
<HOST>_USERSSH user (e.g. ubuntu for stock Ubuntu images)
<HOST>_SSH_KEYFull contents of the private key, pasted as-is
<HOST>_SSH_PORTOptional, defaults to 22
DB_USERNAME / DB_PASSWORDProduction database credentials
APP_JWT_SECRETMust be a real generated key, never the repo-default placeholder
SPRING_MAIL_USERNAME / SPRING_MAIL_PASSWORDSMTP credentials
APP_EXCHANGE_RATE_API_KEYOnly needed for non-default-currency quotes/invoices
APP_FRONTEND_URL, APP_CORS_ALLOWED_ORIGINSReal production origins

The real .env never needs to exist in git or be hand-created on the server — the workflow writes it fresh on every run.

A fresh cloud instance’s default firewall typically only opens the SSH port. Opening the ports your app actually needs (HTTP/HTTPS, and temporarily the app’s own port for direct testing) is a one-time manual step in both the cloud provider’s console and the instance’s own OS firewall — see the two-layer firewall note in Networking & Firewalls.

Local Docker workflow

# Configure environment cp .env.example .env # fill in DB credentials, JWT secret, mail credentials, exchange-rate key # Start the stack (API + Postgres) docker compose up -d # Stop, keeping data volumes docker compose down # Stop and WIPE data volumes - destructive, confirm scope first docker compose down -v

To run a published image instead of building locally, set the image reference in .env before starting — compose pulls it directly rather than building from source.

Operational commands

# Redeploy the latest published image without a full workflow run cd ~/ranlanka sudo docker compose pull sudo docker compose up -d # Full clean restart (also re-reads docker-compose.yml/.env) sudo docker compose down sudo docker compose up -d # Remove old/unused images after a redeploy sudo docker image prune -f

Direct database access (inside the running Postgres container):

# Open a psql shell sudo docker exec -it <db-container> psql -U postgres -d postgres # One-off query without an interactive shell sudo docker exec -it <db-container> psql -U postgres -d postgres -c \ "SET search_path TO <schema>; SELECT email, is_active FROM staff;" # Check which Flyway migration is currently applied sudo docker exec -it <db-container> psql -U postgres -d postgres -c \ "SET search_path TO <schema>; SELECT version, description, success FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 5;"

Full purge (docker compose down -v + docker system prune -a -f --volumes) stops and removes every container, network, and named volume — this wipes the production database and uploaded files. Only ever run this deliberately, with the scope fully understood, never as a reflexive “reset everything” step.

Git hooks (Husky)

The backend has its own hooks-only package.json (not a runtime dependency of the Spring Boot app). After cloning, run npm install once to register:

  • pre-commit — runs the full backend test suite. No database required (no live Spring context in any test), so this is fast and safe on every commit.
  • pre-push — a full build, as a final sanity check before code leaves your machine.
Last updated on