Docker + GitHub Actions CI/CD + one-click deploy to any VPS.
Build your app. Push to deploy.
English | 한국어
Part of Starter Series — Stop explaining CI/CD to your AI every time. Clone and start.
Docker Deploy · Discord Bot · Telegram Bot · Browser Extension · Electron App · npm Package · React Native · VS Code Extension · MCP Server · Python MCP Server · Cloudflare Pages
Via create-starter (recommended):
npx @starter-series/create my-service --template docker-deploy
cd my-service
# Add your app's Dockerfile + code, then:
docker compose upOr clone directly:
git clone https://github.com/starter-series/docker-deploy-starter my-service
cd my-service
docker compose upFull setup (bring your own app):
# 1. Click "Use this template" on GitHub (or clone)
git clone https://github.com/starter-series/docker-deploy-starter.git my-app
cd my-app
# 2. Replace app/ with your application
rm -rf app/
# Copy your app files here
# 3. Update Dockerfile for your language
# See docs/DOCKERFILE_EXAMPLES.md for Python, Go, Rust, Java, etc.
# 4. Test locally
cp .env.example .env
docker compose up├── app/ # Example app (replace with yours)
│ ├── server.js # Minimal Node.js HTTP server
│ └── package.json
├── Dockerfile # Example build (swap for your language)
├── docker-compose.yml # Local development
├── .github/
│ ├── workflows/
│ │ ├── ci.yml # Lint, compose validate, build test, JS tests
│ │ ├── cd.yml # Build → GHCR push → VPS deploy via SSH
│ │ └── setup.yml # Auto setup checklist on first use
│ └── PULL_REQUEST_TEMPLATE.md
├── docs/
│ ├── DOCKERFILE_EXAMPLES.md # Dockerfiles for Node, Python, Go, Rust, Java
│ ├── GHCR_SETUP.md # GitHub Container Registry setup
│ ├── HTTPS_SETUP.md # HTTPS with Caddy reverse proxy
│ └── VPS_DEPLOY.md # VPS SSH deployment guide
├── scripts/
│ ├── bump-version.js # Version bump utility (validates VERSION)
│ └── deploy-with-rollback.sh # Health-checked deploy + auto rollback
├── tests/ # node:test suites + rollback integration test
├── package.json # `npm test` runner
└── VERSION # Current version
- Language agnostic — Swap the Dockerfile for any language (Node, Python, Go, Rust, Java, static)
- CI Pipeline — Dockerfile lint (hadolint), docker-compose validation, build verification, Trivy CVE scan, plus
node:testsuites (version-bump +/health) on every push - CD Pipeline — Build → push to GHCR → health-checked deploy to VPS via docker compose + auto GitHub Release
- Real health checks —
/healthreflects a readiness signal and can return503; wire your own dependency probes (DB, cache, …) so failed deploys actually roll back - Dockerfile examples — Multi-stage builds for Node, Python, Go, Rust, Java in docs
- Version management —
node scripts/bump-version.js patch/minor/major(validatesVERSION, fails loudly on a malformed file instead of writing garbage) - Local dev —
docker compose upwith volume mounts for live reload - HTTPS guide — Caddy reverse proxy with automatic TLS
- Deploy guides — Step-by-step docs for GHCR and VPS setup
- Template setup — Auto-creates setup checklist issue on first use
The deploy pipeline rolls back when the new container fails its health check
(docker compose up -d --wait). That safety net only works if /health can
actually report failure — a /health that always returns 200 makes every
deploy look healthy and silently disables rollback.
-
Currently implemented —
app/server.jsexposes/healthbacked by a list of async readiness checks. All checks passing →200 {"status":"ok"}. Any check returning falsy or throwing →503 {"status":"unavailable"}. Unknown paths return404(the example server is not a catch-all). The default check only confirms the HTTP listener is bound. -
Design intent — fail-closed: a dependency outage should surface as an unhealthy container so the orchestrator stops routing traffic and the CD rollback triggers, rather than serving a broken app behind a green check.
-
You must wire real checks. Replace the example app and register probes for the dependencies your app actually needs:
const { createApp } = require('./server.js'); const { server } = createApp({ readinessChecks: [ async () => { await db.query('SELECT 1'); return true; }, async () => (await redis.ping()) === 'PONG', ], }); server.listen(process.env.PORT || 3000);
-
Non-goals — this is not a metrics/liveness framework. It is the minimal readiness contract the rollback logic depends on; swap in your stack's health library if you need more.
| Step | What it does |
|---|---|
| Lint Dockerfile | Hadolint checks for best practices |
| Validate compose | Verifies docker-compose.yml syntax |
| Build test | Builds the Docker image to catch build errors |
| Scan image | Trivy scans for CRITICAL CVEs |
| Workflow | What it does |
|---|---|
CodeQL (codeql.yml) |
Static analysis for security vulnerabilities (push/PR + weekly) |
Maintenance (maintenance.yml) |
Weekly CI health check — auto-creates issue on failure |
Stale (stale.yml) |
Labels inactive issues/PRs after 30 days, auto-closes after 7 more |
| Step | What it does |
|---|---|
| Version guard | Fails if git tag already exists for this version |
| Build & push | Builds image and pushes to GitHub Container Registry |
| Deploy | SSHs into your VPS, pulls new image, health-checked restart via docker compose |
| Image cleanup | Prunes old images on VPS + keeps last 10 versions on GHCR |
| GitHub Release | Creates a tagged release with auto-generated notes |
How to deploy:
- Set up GitHub Secrets (see below)
- Bump version:
node scripts/bump-version.js patch - Manual: Go to Actions tab → Deploy → Run workflow
- Auto: Push a version tag —
git tag v$(cat VERSION) && git push --tags
| Secret | Description |
|---|---|
VPS_HOST |
Your server IP or domain |
VPS_USER |
SSH username |
VPS_SSH_KEY |
SSH private key |
See docs/VPS_DEPLOY.md for a detailed setup guide.
Note: GHCR authentication uses
GITHUB_TOKENautomatically — no extra secrets needed.
# Start locally with Docker
docker compose up
# Rebuild after Dockerfile changes
docker compose up --build
# Bump version (fails loudly if VERSION is malformed — never writes 1.2.NaN)
node scripts/bump-version.js patch # 1.0.0 → 1.0.1
node scripts/bump-version.js minor # 1.0.0 → 1.1.0
node scripts/bump-version.js major # 1.0.0 → 2.0.0# Node tests: version-bump validation + /health (200) and unknown path (404)
npm test
# Rollback integration test (needs Docker; also run in CI)
bash tests/rollback-integration.sh- Replace
app/with your application code - Pick a Dockerfile from docs/DOCKERFILE_EXAMPLES.md (Python, Go, Rust, Java, static)
- Update
docker-compose.ymlports if needed - Update
.env.examplewith your app's environment variables - Test:
docker compose up --build
Platforms like Railway/Render/Vercel are great for single apps. But when you need more, VPS wins:
- One server, everything — Run app + DB + cache on one machine instead of paying per service
- No vendor lock-in — Standard Docker + SSH. Move between any VPS provider
- Full system access — GPU, custom packages, compliance, any OS-level config
- Always on — No cold starts, no spin-down, no sleep timers
- Predictable cost — Flat monthly price, no usage-based surprises
Use Railway/Render/Vercel instead if:
- You're deploying a single web app and want zero infrastructure management
- You need managed databases with automatic backups
Every "Docker + GitHub Actions" tutorial teaches the same steps. You end up copy-pasting YAML, debugging GHCR auth, wiring SSH keys, and setting up health checks — every single time.
This template gives you the entire pipeline, tested and ready. git clone → replace app/ → push → deployed.
PRs welcome. Please use the PR template.