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/amd64andlinux/arm64, via Buildx + QEMU), and pushes unique tags: Git Short SHA (sha-a1b2c3d), release tags (vX.Y.Z), andlatestto 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 & Rollbacks
A second, manual-trigger-only workflow (workflow_dispatch) handles the
actual deploy — decoupled from image publishing, so pushing a new image
never auto-deploys. It supports an optional image_tag workflow input (defaulting to latest or a specified commit SHA/release version) so admins can deploy or roll back to any historical tag directly from GitHub.
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 and rollback script to the server
Copies docker-compose.yml, the logging shipper config, the backup script (backup-db.sh), and the rollback script (rollback.sh) to a fixed directory on the VPS (e.g. ~/ranlanka).
Register scripts and cron
Makes backup-db.sh and rollback.sh executable and installs the backup crontab entry
idempotently.
Write .env, log history, and start the stack
Writes a fresh .env file on the VPS from GitHub secrets, appends a deployment record to ~/ranlanka/deploy_history.log, then runs docker compose pull && docker compose up -d --remove-orphans. Old container images are pruned with a time filter (until=168h) to retain recent version images locally.
Instant Server-Side Rollback
In the event of an issue on production, admins can execute an instant rollback directly on the server without waiting for CI/CD pipeline builds:
# Interactively list deployment history and roll back to a previous tag
cd ~/ranlanka
./rollback.sh
# Or specify a target tag directly:
./rollback.sh sha-a1b2c3dThe script updates API_IMAGE in ~/ranlanka/.env, logs the rollback to deploy_history.log, and restarts container services in under 3 seconds.
Required secrets:
| Secret | Purpose |
|---|---|
<HOST>_HOST | The instance’s public IP/hostname |
<HOST>_USER | SSH user (e.g. ubuntu for stock Ubuntu images) |
<HOST>_SSH_KEY | Full contents of the private key, pasted as-is |
<HOST>_SSH_PORT | Optional, defaults to 22 |
DB_USERNAME / DB_PASSWORD | Production database credentials |
APP_JWT_SECRET | Must be a real generated key, never the repo-default placeholder |
SPRING_MAIL_USERNAME / SPRING_MAIL_PASSWORD | SMTP credentials |
APP_EXCHANGE_RATE_API_KEY | Only needed for non-default-currency quotes/invoices |
APP_FRONTEND_URL, APP_CORS_ALLOWED_ORIGINS | Real 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 -vTo 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 -fDirect 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.