A modular, plug-and-play Infrastructure as Code (IaC) configuration for deploying self-healing containerized applications on any Linux VPS.
Add or remove apps by simply adding or deleting folders. No need to edit docker-compose or deploy configs!
my-vps-stack/
├── apps/ # 👈 Each app = one folder
│ ├── .template/ # 👈 Copy this to create new apps
│ │ ├── docker-compose.yml
│ │ ├── ingress.yml
│ │ ├── init.sh
│ │ └── README.md
│ ├── portainer/
│ │ ├── docker-compose.yml
│ │ └── ingress.yml # Tunnel routing
│ ├── filebrowser/
│ │ ├── docker-compose.yml
│ │ ├── ingress.yml
│ │ └── init.sh # First-time setup
│ ├── .archive/ # 👈 Disabled apps (excluded from builds)
│ │ └── n8n/
│ └── ...
├── docker-compose.yml # Auto-generated on deploy
└── .github/workflows/
└── deploy.yml # Auto-generates everything
- Add app: Create a folder in
apps/withdocker-compose.yml - Expose via tunnel: Add
ingress.ymlwith subdomain + service - First-time setup: Optionally add
init.shfor initialization - Push to GitHub: Deploy workflow auto-generates tunnel config!
| App | Subdomain | Description | Default Credentials |
|---|---|---|---|
| Dashboard | home.* | Auto-generated app launcher | (No setup needed) |
| Portainer | docker.* | Docker management UI | (Setup on first launch) |
| Uptime Kuma | status.* | Service monitoring | (Setup on first launch) |
| Telegram Bot | - | Remote VPS management/status | (Token in secrets) |
| WhatsApp Bot | - | Group commands via WhatsApp | (Session scanned via QR) |
| Cloudflare Tunnel | - | Exposes all services securely | (Auto-configured) |
Tip
Archived Apps: The following apps are in apps/.archive/ and excluded from builds:
changedetection- Website change monitoringdockge- Docker Compose Managerfilebrowser- File manager / Streamerglances- Real-time system monitorhomarr- Dashboard alternativeit-tools- Developer utilitiesjellyfin- Media Servern8n- Workflow automationqbittorrent- Torrent clientstirling-pdf- PDF manipulation toolswatchtower- Auto-updates containers
Move folders out of .archive/ to re-enable them.
Caution
Change default passwords immediately after first login!
Internal ports used by each service (accessible only via Cloudflare Tunnel):
| App | Internal Port | External Access |
|---|---|---|
| Dashboard | 8090 |
home.* subdomain |
| Portainer | 9000 |
docker.* subdomain |
| Uptime Kuma | 3001 |
status.* subdomain |
| Telegram Bot | - | N/A (Bot API) |
| WhatsApp Bot | - | N/A (WhatsApp Web) |
| Cloudflare Tunnel | - | N/A (Outbound only) |
Note
No ports are exposed to the internet directly. All traffic flows through the Cloudflare Tunnel.
# 1. Copy template folder
cp -r apps/.template apps/myapp
# 2. Edit docker-compose.yml
cd apps/myapp
nano docker-compose.yml
# 3. Optional: Add web access
nano ingress.yml
# 4. Optional: Add custom dashboard icon
echo "myapp=🚀" >> ../dashboard/icons.conf
# 5. Commit and push
git add ..
git commit -m "Add myapp"
git push- Create folder:
apps/myapp/ - Add
docker-compose.yml:services: myapp: image: myapp/image:latest restart: always ports: ["3000:3000"]
- Add
ingress.yml(if exposing via tunnel):hostname: myapp service: http://myapp:3000
- Push to GitHub. That's it! 🎉
The deploy workflow will:
- ✅ Include your app in
docker-compose.yml - ✅ Configure tunnel routing (if
ingress.ymlexists) - ✅ Add dashboard tile (if
ingress.ymlexists) - ✅ Run
init.sh(if exists)
If your app needs persistent storage or config directories:
-
Create in
init.sh:#!/bin/bash mkdir -p "$(dirname "$0")/data" chown -R 1000:1000 "$(dirname "$0")/data"
-
Mount in
docker-compose.yml:volumes: - ./data:/app/data
Apps get a default 🔗 icon. For custom icons:
-
Edit
apps/dashboard/icons.conf:myapp=🚀 database=🗄️ ai=🤖 -
Or just use the default (works fine!)
Option A: Use existing secrets (if applicable)
DOMAIN_NAME- Your domainTG_BOT_TOKEN- Telegram bot token
Option B: Add new secrets (for app-specific API keys, etc.):
- Add to GitHub → Settings → Secrets and variables → Actions
- Edit
.github/workflows/deploy.ymlStep 5:echo "MYAPP_API_KEY=${{ secrets.MYAPP_API_KEY }}" >> .env
- Use in your
docker-compose.yml:environment: - API_KEY=${MYAPP_API_KEY}
- Delete the app folder from
apps/ - Push to GitHub
| Component | Minimum | Recommended | Notes |
|---|---|---|---|
| OS | Ubuntu 22.04 LTS | Ubuntu 24.04 LTS | Debian-based distros also work |
| RAM | 2 GB | 4 GB+ | More RAM needed for media apps |
| CPU | 1 vCPU | 2 vCPUs | ARM64 or x86_64 architecture |
| Storage | 20 GB | 50 GB+ | SSD strongly recommended |
| Network | 100 Mbps | 1 Gbps | Unlimited bandwidth preferred |
- OS: Ubuntu 22.04 LTS or newer (Debian-based distros also supported)
- Docker: Latest stable version with Docker Compose v2
# Quick install script curl -fsSL https://get.docker.com | sh
- Git: For repository management
apt install -y git
- Network: Open port 22 (SSH). All other traffic is routed securely via Cloudflare Tunnel.
Run these commands on your VPS as root (Manual Method):
cd /root
git clone https://github.com/PyNAABO/my-vps-stack.git
cd my-vps-stack
docker compose up -dTip
Better Way: Check the "Setup Automation" section below for the Zero-Config setup!
The stack will start within seconds.
You do NOT need to clone the repo manually!
- Install Docker on your VPS.
- Add Secrets to GitHub (Settings → Secrets and variables → Actions):
SSH_PRIVATE_KEY: SSH private key for your server (must match authorized_keys on VPS)SSH_HOSTNAME: SSH hostname (e.g.,ssh.yourdomain.comor IP)SSH_USER: SSH username (default:root)CF_CLIENT_ID: Cloudflare Access Service Token IDCF_CLIENT_SECRET: Cloudflare Access Service Token SecretDOMAIN: Your domainTUNNEL_ID: Cloudflare Tunnel IDTUNNEL_CREDENTIALS: Cloudflare Tunnel JSONTG_BOT_TOKEN,ALLOWED_GROUP_ID: Bot secrets
- Push to Main: The deploy workflow will automatically:
- Connect to your server via Cloudflare Access SSH
- Clone the repository (if missing)
- Setup directories & permissions
- Deploy the stack
- Generate a Deploy Key
ssh-keygen -t ed25519 -f ~/.ssh/github_action -N ""
cat ~/.ssh/github_action.pub >> ~/.ssh/authorized_keys
cat ~/.ssh/github_action # Copy this as SSH_PRIVATE_KEY- Configure Cloudflare Tunnel
cloudflared tunnel login
cloudflared tunnel create vps-cli-tunnelCreate a wildcard CNAME: * → <UUID>.cfargotunnel.com
- Add GitHub Secrets (Same as Option A)
- Make changes locally
- Push to main branch
- GitHub Actions:
- Pulls latest code
- Auto-generates tunnel config from
apps/*/ingress.yml - Runs
apps/*/init.shscripts - Rebuilds changed containers
Warning
The deploy workflow runs git reset --hard on the VPS, which wipes any local changes (hot-fixes, manual config tweaks). Always commit changes back to the repo—never edit files directly on the VPS.
The stack enforces a standard directory structure using a shared vps-data folder located next to the repository (sibling directory).
/root/
├── my-vps-stack/ # This repository
└── vps-data/ # Shared data (Owned by 1000:1000)
└── downloads/ # qBittorrent downloads
- qBittorrent writes to
/downloads(mapped to../vps-data/downloads). - FileBrowser manages
/srv(mapped to../vps-data).
# Check containers
docker ps
# View logs
docker logs <container-name>
# Hard reset
docker compose down
rm -rf config/<app-config>
git pull
docker compose up -d| Script | Description |
|---|---|
scripts/run_once.sh |
Sets up update alias |
scripts/update.sh |
System update script |
scripts/cloud_backup.sh |
rclone backup to Google Drive |
scripts/list_subdomains.sh |
Lists all configured subdomains |
📦 Seedbox setup? See docs/Seedbox.md
Note
.env is auto-generated by CI/CD. GitHub Secrets are injected during deploy.
| Variable | Source |
|---|---|
DOMAIN_NAME |
secrets.DOMAIN |
TG_BOT_TOKEN |
secrets.TG_BOT_TOKEN |
ALLOWED_GROUP_ID |
secrets.ALLOWED_GROUP_ID |
Q: Do I need to open any ports on my VPS?
A: No! Only port 22 (SSH) needs to be open. All web traffic flows through the Cloudflare Tunnel, which creates an outbound-only connection.
Q: Can I use this on a VPS behind NAT or with a dynamic IP?
A: Yes! The Cloudflare Tunnel works from anywhere with an internet connection, regardless of your network configuration.
Q: Is my data secure?
A: Yes. All traffic is encrypted via HTTPS (provided by Cloudflare). Additionally, Cloudflare Access provides an authentication layer before services are accessible.
Q: How much does this cost to run?
A: The software is free and open-source. You only pay for your VPS hosting (typically $5-20/month) and your domain name (~$10/year).
Q: What happens if the deployment fails?
A: Check the GitHub Actions logs for detailed error messages. Common issues include:
- Missing GitHub Secrets
- Incorrect SSH key configuration
- Port conflicts between apps
Q: Can I deploy manually without GitHub Actions?
A: Yes! Simply clone the repo on your VPS and run
docker compose up -d. However, you'll need to manually configure the Cloudflare Tunnel.
Q: How do I update the stack?
A: Changes pushed to the
mainbranch are automatically deployed. For manual updates, rungit pull && docker compose up -d.
Q: Can I add my own custom apps?
A: Absolutely! Copy the
apps/.templatefolder, customize thedocker-compose.yml, and push your changes. See the "Adding a New App" section above for details.
Q: How do I reset an app's password?
A: Most apps store credentials in their data directory. Check the app's specific README in
apps/<app-name>/README.mdfor reset instructions.
Q: Where are my downloaded files stored?
A: Downloads go to
../vps-data/downloads/(relative to the repository). This is a shared directory accessible by FileBrowser and qBittorrent.
Q: Can I access services locally without going through Cloudflare?
A: Yes, you can access services directly via their internal ports (e.g.,
http://localhost:8090for Dashboard). See the "Network Ports" table above for the full list.
Q: How do I back up my data?
A: Use the included
scripts/cloud_backup.shscript, which backs up the entirevps-datadirectory to Google Drive via rclone.
Q: My app shows "502 Bad Gateway"
A: This usually means the service isn't running. Check
docker psto see if the container is up, and checkdocker logs <container-name>for errors.
Q: I get "permission denied" errors
A: Data directories must be owned by user 1000:1000. Run:
sudo chown -R 1000:1000 ../vps-data/
Q: Changes I made on the VPS disappeared
A: The deploy workflow runs
git reset --hard, which wipes local changes. Always commit changes back to the repository before pushing.
Q: How do I view container logs?
A: Use
docker logs <container-name>. For follow mode (liketail -f), usedocker logs -f <container-name>.