Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 18 additions & 78 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,90 +1,30 @@
```
# Dependencies
node_modules/
target/
_build/
deps/
envs/prod.env
envs/staging.env
envs/env.env
# Build outputs
dist/
build/
out/
.next/
.docusaurus/

# Environment files
.env*
!.env.example

# IDE
.vscode/
.idea/
*.iml
.swp
.swo
.sublime-*

# OS
.DS_Store
Thumbs.db
.AppleDouble
.LSOverride
# Environment
.env
.env.local
.env.*

# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Cache
.cache/
.pnpm-store/
.turbo/
.vercel/
*.tsbuildinfo

# Test artifacts
test-results/
playwright-report/
.coverage
htmlcov/
# Coverage
coverage/
htmlcov/
.coverage

# Python
__pycache__/
*.py[cod]
*.pyc
.pytest_cache/
.mypy_cache/
.ruff_cache/
*.egg-info/
.venv/
venv/
env/

# Rust
**/*.rs.bk
*.pdb

# Elixir/Phoenix
*.ez
erl_crash.dump
.fetch

# Build artifacts
*.o
*.so
*.dylib
*.dll
*.exe

# Database
*.sqlite
*.db
*.sqlite3
# Temporary files
*.tmp
*.swp
*.swo

# Docker
docker-compose.override.yml
# OS generated files
.DS_Store
Thumbs.db

# Rust specific
Cargo.lock
```
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,33 @@ The config drives:
- **SEO**: title, description, keywords, favicon, OG image
- **Branding**: logo path and alt text
- **Repo**: GitHub org/repo for links and edit URLs
- **Deployment**: `appName` for `/opt/<name>/{frontend,backend}/{staging,production}`, systemd service names (staging/production)
- **Deployment**: `appName` for `/opt/<name>/{frontend,backend}/{staging,production}`, systemd service names (staging/production), dedicated port mappings

See `template.config.example.yaml` for every option. After applying, commit the changes and set up GitHub Environments and secrets as in `infra/systemd/README.md`.

## 🔍 Finding Available Ports

Before deploying a new app, use the port finder script to scan running services and get port recommendations:

```bash
# Scan all services and get recommendations
pnpm run find-available-port

# Or run directly
./scripts/find-available-port.sh

# Filter by app name
./scripts/find-available-port.sh myapp

# Filter by environment
./scripts/find-available-port.sh myapp staging

# Filter by component
./scripts/find-available-port.sh myapp production backend
```

The script scans `/etc/systemd/system` and `/lib/systemd/system` for services matching the `{appname}-{env}-{frontend|backend}.service` pattern, extracts configured ports, checks which are actively listening, and recommends available ports in the appropriate ranges.

## 🚀 Features

- **Backend**: Rust-based API server with base endpoints
Expand Down
11 changes: 10 additions & 1 deletion apps/backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use axum::{
Router,
};
use serde::{Deserialize, Serialize};
use std::env;
use std::net::SocketAddr;
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
Expand Down Expand Up @@ -41,6 +42,14 @@ async fn main() {
.with_env_filter("backend=debug,tower_http=debug")
.init();

// Get port from command line args or environment variable (default: 3000)
let port: u16 = std::env::args()
.nth(1)
.and_then(|arg| arg.strip_prefix("--port=").map(|s| s.to_string()))
.or_else(|| env::var("PORT").ok())
.and_then(|s| s.parse().ok())
.unwrap_or(3000);

// Build application with routes
let app = Router::new()
.route("/", get(root))
Expand All @@ -51,7 +60,7 @@ async fn main() {
.layer(TraceLayer::new_for_http());

// Run server
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
let addr = SocketAddr::from(([0, 0, 0, 0], port));
tracing::info!("🚀 Server starting on http://{}", addr);

let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
Expand Down
53 changes: 51 additions & 2 deletions infra/systemd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,59 @@

Each app is deployed in **two environments** on the VM: **staging** and **production**. Paths and service names are env-specific. CI/CD uses the same **env mode** mapping for all workflows; see [.github/ENV_MODES.md](../../.github/ENV_MODES.md).

## Naming Convention

Service files follow the naming pattern: **`{appname}-{env}-{frontend|backend}.service`**

Examples:
- `myapp-staging-frontend.service`
- `myapp-staging-backend.service`
- `myapp-production-frontend.service`
- `myapp-production-backend.service`

This convention ensures consistent identification of services across multiple apps on the same VM.

## Port Allocation

Each service has a **dedicated port** configured via environment variables:

| Component | Environment | Default Port Range | Example Port |
|-----------|-------------|-------------------|--------------|
| Backend | Staging | 6100-6199 | 6100 |
| Frontend | Staging | 6200-6299 | 6200 |
| Backend | Production | 6300-6399 | 6300 |
| Frontend | Production | 6400-6499 | 6400 |

Ports are configured in `template.config.yaml` under `deployment:` section and applied via `pnpm run apply-template`.

### Finding Available Ports

Use the provided script to scan running services and find available ports:

```bash
# Scan all services and get recommendations
./scripts/find-available-port.sh

# Filter by app name
./scripts/find-available-port.sh myapp

# Filter by environment
./scripts/find-available-port.sh myapp staging

# Filter by component
./scripts/find-available-port.sh myapp production backend
```

The script will:
1. Scan `/etc/systemd/system` and `/lib/systemd/system` for matching service files
2. Extract configured ports from service files
3. Check which ports are actively listening
4. Recommend available ports in the appropriate ranges

| Branch | Environment | Deploy paths (on VM) | Systemd units |
|----------|-------------|----------------------|----------------|
| `main` | **staging** | `/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/staging`, `/opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/staging` | `__TEMPLATE_SYSTEMD_BACKEND_STAGING__`, `__TEMPLATE_SYSTEMD_FRONTEND_STAGING__` |
| `prod` | **production** | `/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/production`, `/opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/production` | `__TEMPLATE_SYSTEMD_BACKEND_PRODUCTION__`, `__TEMPLATE_SYSTEMD_FRONTEND_PRODUCTION__` |
| `main` | **staging** | `/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/staging`, `/opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/staging` | `{appname}-staging-backend.service`, `{appname}-staging-frontend.service` |
| `prod` | **production** | `/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/production`, `/opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/production` | `{appname}-production-backend.service`, `{appname}-production-frontend.service` |

Paths follow `/opt/<app>/{frontend,backend}/{staging,production}`.

Expand Down
10 changes: 7 additions & 3 deletions infra/systemd/backend-production.service.example
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
# Example systemd unit for backend (production).
# IMPORTANT: Create a service file named: __TEMPLATE_BACKEND_PRODUCTION_SERVICE_FILE__
# (e.g., solo-template-backend-production.service) in infra/systemd/ before deploying.
# Naming convention: {appname}-{env}-{backend/frontend}.service (e.g., myapp-production-backend.service)
# Copy to /etc/systemd/system/__TEMPLATE_SYSTEMD_BACKEND_PRODUCTION__.service on the VM.
# Path: /opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/production/backend
# Each app: /opt/<app>/{frontend,backend}/{staging,production}.
# Port: __TEMPLATE_BACKEND_PRODUCTION_PORT__ (dedicated port for this app/env combination)

[Unit]
Description=__TEMPLATE_APP_NAME__ Backend API (production)
Description=__TEMPLATE_APP_NAME__ Backend API (production) - Port __TEMPLATE_BACKEND_PRODUCTION_PORT__
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/production/backend
ExecStart=/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/production/backend --port __TEMPLATE_BACKEND_PRODUCTION_PORT__
Restart=on-failure
RestartSec=5
WorkingDirectory=/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/production
EnvironmentFile=-/etc/default/__TEMPLATE_SYSTEMD_BACKEND_PRODUCTION__
Environment=PORT=__TEMPLATE_BACKEND_PRODUCTION_PORT__
Environment=APP_ENV=production
Environment=APP_NAME=__TEMPLATE_DEPLOY_APP_NAME__

[Install]
WantedBy=multi-user.target
10 changes: 7 additions & 3 deletions infra/systemd/backend-staging.service.example
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
# Example systemd unit for backend (staging).
# IMPORTANT: Create a service file named: __TEMPLATE_BACKEND_STAGING_SERVICE_FILE__
# (e.g., solo-template-backend-staging.service) in infra/systemd/ before deploying.
# Naming convention: {appname}-{env}-{backend/frontend}.service (e.g., myapp-staging-backend.service)
# The CI/CD workflow expects this file to exist.
# Copy to /etc/systemd/system/__TEMPLATE_SYSTEMD_BACKEND_STAGING__.service on the VM.
# Path: /opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/staging/backend
# Each app: /opt/<app>/{frontend,backend}/{staging,production}.
# Port: __TEMPLATE_BACKEND_STAGING_PORT__ (dedicated port for this app/env combination)

[Unit]
Description=__TEMPLATE_APP_NAME__ Backend API (staging)
Description=__TEMPLATE_APP_NAME__ Backend API (staging) - Port __TEMPLATE_BACKEND_STAGING_PORT__
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/staging/backend
ExecStart=/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/staging/backend --port __TEMPLATE_BACKEND_STAGING_PORT__
Restart=on-failure
RestartSec=5
WorkingDirectory=/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/staging
EnvironmentFile=-/etc/default/__TEMPLATE_SYSTEMD_BACKEND_STAGING__
Environment=PORT=__TEMPLATE_BACKEND_STAGING_PORT__
Environment=APP_ENV=staging
Environment=APP_NAME=__TEMPLATE_DEPLOY_APP_NAME__

[Install]
WantedBy=multi-user.target
10 changes: 7 additions & 3 deletions infra/systemd/frontend-production.service.example
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
# Example systemd unit for frontend (production).
# IMPORTANT: Create a service file named: __TEMPLATE_FRONTEND_PRODUCTION_SERVICE_FILE__
# (e.g., solo-template-frontend-production.service) in infra/systemd/ before deploying.
# Naming convention: {appname}-{env}-{backend/frontend}.service (e.g., myapp-production-frontend.service)
# Copy to /etc/systemd/system/__TEMPLATE_SYSTEMD_FRONTEND_PRODUCTION__.service on the VM.
# Serves from /opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/production (SvelteKit adapter-node SSR build).
# Each app: /opt/<app>/{frontend,backend}/{staging,production}.
# Port: __TEMPLATE_FRONTEND_PRODUCTION_PORT__ (dedicated port for this app/env combination)

[Unit]
Description=__TEMPLATE_APP_NAME__ Frontend (production)
Description=__TEMPLATE_APP_NAME__ Frontend (production) - Port __TEMPLATE_FRONTEND_PRODUCTION_PORT__
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/production
ExecStart=/usr/bin/node serve.mjs
ExecStart=/usr/bin/node serve.mjs --port __TEMPLATE_FRONTEND_PRODUCTION_PORT__
Restart=on-failure
RestartSec=5
EnvironmentFile=-/etc/default/__TEMPLATE_SYSTEMD_FRONTEND_PRODUCTION__
Environment=PORT=__TEMPLATE_FRONTEND_PRODUCTION_PORT__
Environment=APP_ENV=production
Environment=APP_NAME=__TEMPLATE_DEPLOY_APP_NAME__
Environment=BACKEND_URL=http://localhost:__TEMPLATE_BACKEND_PRODUCTION_PORT__

[Install]
WantedBy=multi-user.target
10 changes: 7 additions & 3 deletions infra/systemd/frontend-staging.service.example
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
# Example systemd unit for frontend (staging).
# IMPORTANT: Create a service file named: __TEMPLATE_FRONTEND_STAGING_SERVICE_FILE__
# (e.g., solo-template-frontend-staging.service) in infra/systemd/ before deploying.
# Naming convention: {appname}-{env}-{backend/frontend}.service (e.g., myapp-staging-frontend.service)
# The CI/CD workflow expects this file to exist.
# Copy to /etc/systemd/system/__TEMPLATE_SYSTEMD_FRONTEND_STAGING__.service on the VM.
# Serves from /opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/staging (SvelteKit adapter-node SSR build).
# Each app: /opt/<app>/{frontend,backend}/{staging,production}.
# Port: __TEMPLATE_FRONTEND_STAGING_PORT__ (dedicated port for this app/env combination)

[Unit]
Description=__TEMPLATE_APP_NAME__ Frontend (staging)
Description=__TEMPLATE_APP_NAME__ Frontend (staging) - Port __TEMPLATE_FRONTEND_STAGING_PORT__
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/staging
ExecStart=/usr/bin/node serve.mjs
ExecStart=/usr/bin/node serve.mjs --port __TEMPLATE_FRONTEND_STAGING_PORT__
Restart=on-failure
RestartSec=5
EnvironmentFile=-/etc/default/__TEMPLATE_SYSTEMD_FRONTEND_STAGING__
Environment=PORT=__TEMPLATE_FRONTEND_STAGING_PORT__
Environment=APP_ENV=staging
Environment=APP_NAME=__TEMPLATE_DEPLOY_APP_NAME__
Environment=BACKEND_URL=http://localhost:__TEMPLATE_BACKEND_STAGING_PORT__

[Install]
WantedBy=multi-user.target
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"frontend": "pnpm --filter frontend",
"backend": "pnpm --filter backend",
"docs": "pnpm --filter docs",
"apply-template": "node scripts/apply-template-config.mjs"
"apply-template": "node scripts/apply-template-config.mjs",
"find-available-port": "bash scripts/find-available-port.sh"
},
"devDependencies": {
"yaml": "^2.8.2"
Expand Down
Loading
Loading