Command Palette

Search for a command to run...

Hectal
← All projects

Project 1 of 12 · project brief

Deploy Spring Boot on a Bare Linux Server

Ubuntu + Nginx + PostgreSQL + a Spring Boot service, installed and hardened by hand, the way everything worked before containers.

Beginner 1–2 days 5 milestones

The scenario

ShopLite's first customer-facing API has to go live on a single Ubuntu VM, and the team wants someone who understands every layer before automating it. Your brief: get the Spring Boot API running as a proper service behind Nginx, with PostgreSQL on the same box, locked down, logging, and surviving reboots.

Doing this manually once is the fastest way to understand what Docker, Kubernetes, and Ansible automate later.

Before you start

Stack

Ubuntu 24.04OpenJDK 21Spring BootPostgreSQL 17NginxsystemdufwSSH

Target architecture

Project 1 architecturediagram
Rendering diagram…

Deliverables and requirements

You will hand in

  • A VM (local VirtualBox/Multipass or a cloud instance) serving the API at http://<ip>/api/health through Nginx
  • A shoplite-api.service systemd unit running as an unprivileged user, restarting on failure
  • PostgreSQL with a dedicated database and least-privilege app role (Stateful course, Unit 1.1)
  • A setup.md runbook with every command, and a backup.sh cron job

Functional

  • API reachable only through Nginx on port 80; port 8080 and 5432 not reachable from outside
  • App starts automatically on boot and restarts if it crashes
  • Database backup runs nightly with 7 days of retention

Non-functional

  • SSH key-only login, root login disabled
  • Firewall allows only 22 (from your IP) and 80
  • App logs visible with journalctl -u shoplite-api
  • No secrets in the unit file committed to Git (use an EnvironmentFile with 0600 permissions)

Milestones

  1. 1

    Provision and secure the server

    Done when: A patched Ubuntu VM you can SSH into with a key, with a firewall on.

    • Create the VM; create a sudo user; install your SSH public key
    • Disable password auth and root login in sshd_config; restart ssh
    • apt update && apt upgrade; enable unattended-upgrades
    • Enable ufw allowing only OpenSSH and 80
    Stuck? Hints
    • Keep a second SSH session open while changing sshd config so you can't lock yourself out.
    • sudo ufw allow from <your-ip> to any port 22 is tighter than allowing OpenSSH from anywhere.

    Prove it works

    terminal
    $ sudo ufw status verbose; sudo sshd -T | grep -E 'passwordauthentication|permitrootlogin'
    ── expected output ──
    Status: active
    ...
    passwordauthentication no
    permitrootlogin no
  2. 2

    Install PostgreSQL and create the app database

    Done when: A shop database owned by shop_owner, with an app role limited to DML.

    • Install postgresql
    • Create roles and database using the SQL from Stateful Systems Unit 1.1
    • Confirm Postgres listens only on localhost (listen_addresses)

    Prove it works

    terminal
    $ sudo ss -tlnp | grep 5432
    ── expected output ──
    LISTEN 0 200 127.0.0.1:5432 ... postgres
  3. 3

    Run the app as a systemd service

    Done when: The JAR runs as user shoplite, reads config from an EnvironmentFile, and restarts on failure.

    • Install OpenJDK 21; create a system user shoplite with no login shell
    • Build the Spring Boot JAR (./mvnw package) and copy it to /opt/shoplite/app.jar
    • Write /etc/shoplite/api.env (DB URL, user, password) with mode 0600, owned by root:shoplite
    • Write /etc/systemd/system/shoplite-api.service with User=shoplite, EnvironmentFile=, Restart=on-failure, and memory limits
    Stuck? Hints
    • Add ProtectSystem=strict, ReadWritePaths=/var/lib/shoplite, and NoNewPrivileges=true for cheap hardening.
    • systemctl daemon-reload after editing unit files.

    Prove it works

    terminal
    $ sudo systemctl enable --now shoplite-api && systemctl is-active shoplite-api && curl -s localhost:8080/actuator/health
    ── expected output ──
    active
    {"status":"UP"}
  4. 4

    Put Nginx in front

    Done when: Nginx proxies port 80 to the app, with sensible timeouts and headers.

    • Install nginx; write a site config proxying / to 127.0.0.1:8080
    • Set proxy_set_header for Host and X-Forwarded-For; add a /healthz location
    • nginx -t then reload

    Prove it works

    terminal
    $ curl -si http://<vm-ip>/actuator/health | head -1; curl -s --max-time 3 http://<vm-ip>:8080 || echo 'blocked (good)'
    ── expected output ──
    HTTP/1.1 200 OK
    blocked (good)
  5. 5

    Backups, logs, and a reboot test

    Done when: Nightly pg_dump, log rotation, and proof everything survives a reboot.

    • Write backup.sh (pg_dump -Fc, keep 7 days) and a cron entry at 02:00
    • Check journald retention (SystemMaxUse)
    • Reboot and verify the site comes back without intervention

    Prove it works

    terminal
    $ sudo reboot # wait, reconnect
    curl -s http://<vm-ip>/actuator/health && ls -1 /var/backups/shoplite | tail -2
    ── expected output ──
    {"status":"UP"}
    shop-2026-09-26.dump
    shop-2026-09-27.dump

Would you run this in production?

  • ☐SSH hardened, firewall minimal, automatic security updates on
  • ☐App runs unprivileged with systemd hardening options
  • ☐Secrets outside Git with strict file permissions
  • ☐Backups automated AND a restore has been tested
  • ☐Health endpoint monitored from outside (e.g. an uptime check)

Stretch goals

  • Add HTTPS with a certificate from Let's Encrypt (certbot) and redirect HTTP to HTTPS
  • Ship logs to a central place (Fluent Bit → OpenSearch or Loki)
  • Turn your setup.md into an Ansible playbook (GitOps course, Stage 0)

Show it off

Résumé bullet

Deployed and hardened a Spring Boot + PostgreSQL service on Ubuntu with Nginx, systemd, ufw, and automated backups; documented a reproducible runbook.

Demo script

  • Show systemctl status and a crash → auto-restart (kill -9 the Java PID)
  • Show the firewall blocking 8080 and 5432 from outside
  • Restore last night's backup into a scratch database

Interview questions about this project

01

Why run the app behind Nginx instead of exposing Spring Boot directly?

02

How do you make a Java app start on boot and restart on failure on Linux?