Deploy
Production deployment uses Docker Compose with Traefik as a reverse proxy. The stack is defined in docker-compose.prod.yml at the repository root.
Services
The production stack runs five services:
| Service | Image | Role |
|---|---|---|
| bugzkit-api | while1618/bugzkit-api:1.0.0 | Spring Boot API |
| bugzkit-ui | while1618/bugzkit-ui:1.0.0 | SvelteKit frontend (Node) |
| postgres | postgres:17.2-alpine | Database |
| redis | redis:7.4.1 | Token storage and cache |
| traefik | traefik:v3.3 | Reverse proxy with automatic HTTPS |
Routing
Traefik handles TLS termination and routing:
bugzkit.comandwww.bugzkit.comroute to the UI on port 5173.api.bugzkit.comroutes to the API on port 8080.- HTTP is automatically redirected to HTTPS.
- Certificates are obtained via Let’s Encrypt (HTTP challenge).
Replace the domain names in docker-compose.prod.yml with your own.
Secrets
Sensitive values are stored as plain files in a secrets/ directory next to the compose file. Docker Compose mounts each file into the container at /run/secrets/<name>. Spring Boot reads them via spring.config.import: configtree:/run/secrets/.
Create the directory and one file per secret before deploying:
mkdir -m 700 secrets
echo -n "your_password" > secrets/postgres_password
echo -n "your_password" > secrets/redis_password
echo -n "your_secret" > secrets/jwt_secret # min 32 chars
echo -n "your_password" > secrets/smtp_password
echo -n "your_password" > secrets/user_password
echo -n "your_client_id" > secrets/google_client_id
echo -n "your_client_secret" > secrets/google_client_secret
chmod 400 secrets/*The secrets/ directory is listed in .gitignore and must never be committed.
VPS Initialization
scripts/init-vps.sh automates the full server setup on a fresh Debian 12 (DigitalOcean) droplet. Run it as root:
bash init-vps.shIt covers: system hardening, SSH lockdown, UFW firewall, Fail2ban, automatic security updates, Docker CE, and writing all secret files with correct permissions.
Deploying via GitHub Actions
The deploy.yml workflow deploys the stack via SSH. It is triggered manually and only runs on the master branch. It requires the following repository secrets:
HOST: The server address.DEPLOY_USER: SSH user on the server.DEPLOY_SSH_PRIVATE_KEY: SSH private key for authentication.
On each run the workflow copies docker-compose.prod.yml from the repository to the server, then pulls updated images and restarts only the affected containers.
Manual Deploy
cd ~/bugzkit
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d --remove-orphansClient IP Resolution
The production profile is configured for a Cloudflare + Traefik setup out of the box:
server.forward-headers-strategy: native— Tomcat resolves the real client IP fromX-Forwarded-Forset by Traefik.server.client-ip-header: CF-Connecting-IP— the API reads Cloudflare’s header for the real visitor IP, which cannot be spoofed by clients.
If you use a different CDN or no CDN at all, override the CLIENT_IP_HEADER environment variable:
| Setup | CLIENT_IP_HEADER |
|---|---|
| Cloudflare (default) | CF-Connecting-IP |
| Fastly | Fastly-Client-IP |
| Akamai | True-Client-IP |
nginx (real_ip module) | X-Real-IP |
| Traefik only (no CDN) | (unset) |
When unset, the server falls back to getRemoteAddr() resolved by Tomcat from X-Forwarded-For, which is correct for a Traefik-only deployment.