Advanced deployment automation for SvelteKit applications with release management and shared resource symlinking.
Portano is a deployment script that implements a release-based deployment strategy with automatic rollback capabilities. It maintains multiple releases on the server, symlinks shared directories for data persistence, and provides easy rollback to previous versions.
- Zero-downtime deployments with atomic symlink switching
- Automatic release history management (keeps last 10 releases)
- Shared directory management for persistent data
- Separate build and deploy stages for flexible workflows
- SSH-based secure deployment
- Rsync-powered efficient file transfers
- Node.js and npm installed
- SSH access to remote server
- rsync installed locally
- SSH server running
- Proper SSH key authentication configured
- Write permissions to deployment directory
- rsync installed on server
Edit the configuration section in deploy.sh:
# Configuration
REMOTE_USER="user"
REMOTE_HOST="example.com"
DEPLOY_PATH="/home/domains/podcast.example.com"
RELEASES_PATH="${DEPLOY_PATH}/releases"
CURRENT_LINK="${DEPLOY_PATH}/current"
SHARED_PATH="${DEPLOY_PATH}/shared"
CURRENT_ENVIRONMENT="${NODE_ENV:-production}"
# Shared directories to symlink to each release
SHARED_SYMLINKS=(
"content"
"uploads"
"database.db"
)
MAX_RELEASES=10| Variable | Description | Example |
|---|---|---|
REMOTE_USER |
SSH username on remote server | user |
REMOTE_HOST |
Remote server hostname or IP | example.com |
DEPLOY_PATH |
Base deployment directory | /home/domains/example.com |
CURRENT_ENVIRONMENT |
Environment name for .env file | ${NODE_ENV:-production} |
SHARED_SYMLINKS |
Array of shared directories/files | ("content" "uploads" "database.db") |
MAX_RELEASES |
Number of releases to keep | 10 |
The script uses CURRENT_ENVIRONMENT variable (defaults to $NODE_ENV or "production") to manage environment-specific configuration files:
- Local file:
.env.${CURRENT_ENVIRONMENT}(e.g.,.env.production,.env.staging) - Synced to:
shared/.env.${CURRENT_ENVIRONMENT}on server - Symlinked as:
.envin each release
This allows different environments to have separate configurations while maintaining them across deployments.
After deployment, the remote server will have this structure:
/home/domains/example.com/
├── current -> releases/20260103143022/ # Symlink to active release
├── releases/
│ ├── 20260103143022/ # Current release
│ │ ├── index.js
│ │ ├── package.json
│ │ ├── .env -> ../../shared/.env.production # Symlink
│ │ ├── content -> ../../shared/content # Symlink
│ │ ├── uploads -> ../../shared/uploads # Symlink
│ │ └── database.db -> ../../shared/database.db # Symlink (file)
│ ├── 20260103120815/ # Previous release
│ └── 20260103101234/ # Older release
└── shared/
├── .env.production # Persistent environment config
├── content/ # Persistent podcast content (directory)
├── uploads/ # Persistent file uploads (directory)
└── database.db # Persistent database file
Releases are named using timestamp format: YYYYMMDDHHMMSS
Example: 20260103143022 = January 3, 2026 at 14:30:22
Run the script without arguments to see usage information and the remote folder structure:
./deploy.shOutput:
Portano Deployment Script
Usage: ./deploy.sh [command]
Commands:
init - Initialize remote directory structure (first-time setup)
build - Build the application locally only
deploy - Deploy existing build to server (skips build step)
all - Build and deploy
Examples:
./deploy.sh init # Initialize remote server structure
./deploy.sh all # Build and deploy
./deploy.sh build # Build only
./deploy.sh deploy # Deploy only
Remote Folder Structure:
/home/domains/example.com/
├── current -> releases/YYYYMMDDHHMMSS/ # Symlink to active release
├── releases/
│ ├── 20260103143022/ # Current release
│ │ ├── index.js
│ │ ├── package.json
│ │ ├── .env -> ../../shared/.env.production
│ │ ├── content -> ../../shared/content
│ │ ├── uploads -> ../../shared/uploads
│ │ ├── database.db -> ../../shared/database.db
│ ├── 20260103120815/ # Previous release
│ └── 20260103101234/ # Older release
└── shared/
├── .env.production # Environment config
├── content/
├── uploads/
├── database.db # File
Before deploying for the first time, initialize the directory structure on the remote server:
./deploy.sh initSteps executed:
- Create base deployment directories
- Create releases directory
- Create shared directory
- Create shared subdirectories (only directories, not files)
Output example:
==> Initializing remote directory structure...
==> Target: example.com:/home/domains/example.com
==> ✓ Directory structure initialized
Created directories:
- /home/domains/example.com/releases
- /home/domains/example.com/shared
- /home/domains/example.com/shared/content/
- /home/domains/example.com/shared/uploads/
Note: Files (like database.db) will be created during first deployment
==> Server is ready for deployments
Note: The init command only creates directories. Files in SHARED_SYMLINKS (like database.db) are created/synced during the first actual deployment when they exist locally.
Build the application and deploy to server in one command:
./deploy.sh allSteps executed:
- Install dependencies (
npm install) - Build application (
npm run build) - Create remote directory structure
- Upload application files
- Upload static files
- Sync shared directories
- Create symlinks for shared resources
- Update current release symlink
- Clean up old releases
Build the application without deploying:
./deploy.sh buildUse case: Test build locally before deploying or prepare build for later deployment.
Deploy an existing build without rebuilding:
./deploy.sh deployUse case: Quickly redeploy after fixing deployment configuration or when build already exists.
Requirement: build/ directory must exist from previous build.
Shared paths are directories or files that persist across deployments. Instead of copying these to each release, they're stored once in the shared/ directory and symlinked to each release.
- content - Application data and configuration
- uploads - User-uploaded files
- database.db - SQLite database file (if using file-based database)
To add a new shared directory/file:
- Edit the
SHARED_SYMLINKSarray indeploy.sh:
SHARED_SYMLINKS=(
"content"
"uploads"
"database.db"
"logs" # New addition
"cache" # New addition
)- Deploy - the script will automatically:
- Create the directory on the server in
shared/ - Sync local version if it exists
- Create symlink in each new release
- Create the directory on the server in
- If path exists locally: Synced to server via rsync
- If path doesn't exist locally: Skipped with info message
- On server: Created as directory in
shared/and symlinked to release
The script automatically manages environment-specific configuration files using the CURRENT_ENVIRONMENT variable.
-
Environment Detection:
- Uses
$NODE_ENVenvironment variable if set - Falls back to "production" if not set
- Can be overridden:
CURRENT_ENVIRONMENT="staging"
- Uses
-
Sync Process:
- Looks for
.env.${CURRENT_ENVIRONMENT}file locally (e.g.,.env.production) - If found, syncs to
shared/.env.${CURRENT_ENVIRONMENT}on server - If not found locally, skips sync (preserves existing server file)
- Looks for
-
Symlink Creation:
- If
shared/.env.${CURRENT_ENVIRONMENT}exists on server - Creates symlink:
release/.env -> shared/.env.${CURRENT_ENVIRONMENT} - Each release gets the same environment configuration
- If
Deploy to Production:
# Uses .env.production locally
./deploy.sh all
# Or explicitly set environment
NODE_ENV=production ./deploy.sh allDeploy to Staging:
# Uses .env.staging locally
NODE_ENV=staging ./deploy.sh allDeploy to Custom Environment:
# Uses .env.development locally
NODE_ENV=development ./deploy.sh allCreate separate environment files for each environment:
.env.production # Production secrets
.env.staging # Staging configuration
.env.development # Development settingsDeploy to different environments:
# First-time: Initialize server
./deploy.sh init
# Deploy to production
NODE_ENV=production ./deploy.sh all
# Deploy to staging (requires separate server config)
# Edit deploy.sh to point to staging server, then:
NODE_ENV=staging ./deploy.sh all-
Never commit .env files to git
# Add to .gitignore .env* !.env.example
-
Use .env.example as template
# .env.example (safe to commit) DATABASE_URL=postgresql://user:pass@localhost:5432/db SESSION_SECRET=your-secret-here PUBLIC_URL=https://example.com -
First deployment to new environment
- Manually create
.env.${ENVIRONMENT}on server first - Or sync from local during first deploy
- Subsequent deploys preserve existing server file if not present locally
- Manually create
REMOTE_USER="deploy"
REMOTE_HOST="example.com"
DEPLOY_PATH="/var/www/myapp"
SHARED_SYMLINKS=(
"uploads"
"database.db"
)
MAX_RELEASES=5REMOTE_USER="production"
REMOTE_HOST="prod.example.com"
DEPLOY_PATH="/home/apps/myapp"
CURRENT_ENVIRONMENT="${NODE_ENV:-production}"
SHARED_SYMLINKS=(
"content"
"uploads"
"database.db"
"logs"
"cache"
"tmp"
)
MAX_RELEASES=15Note: Environment files (.env.production, .env.staging) are managed automatically by the script and don't need to be in SHARED_SYMLINKS.
REMOTE_USER="staging"
REMOTE_HOST="staging.example.com"
DEPLOY_PATH="/home/staging/myapp"
SHARED_SYMLINKS=(
"uploads"
"database.db"
)
MAX_RELEASES=3# 1. Configure deploy.sh with your server details
vim deploy.sh
# 2. Initialize remote directory structure
./deploy.sh init
# Output:
# ==> Initializing remote directory structure...
# ==> Target: example.com:/home/domains/example.com
# ==> ✓ Directory structure initialized
# ...
# ==> Server is ready for deployments
# 3. Deploy application
./deploy.sh all# 1. Pull latest changes
git pull origin main
# 2. Run tests locally
npm test
# 3. Deploy
./deploy.sh all
# Output:
# ==> Building application...
# ==> ✓ Build complete
# ==> Starting deployment to example.com
# ==> Release: 20260103143022
# ...
# ==> ✓ Deployment complete!# 1. Ensure .env.staging exists locally
ls .env.staging
# 2. Deploy to staging
NODE_ENV=staging ./deploy.sh all
# Output:
# ==> Managing environment file for: staging
# ==> Syncing .env.staging to shared folder...
# ==> ✓ Environment file synced
# ...# Build once
./deploy.sh build
# Make configuration changes to deploy.sh
vim deploy.sh
# Deploy without rebuilding
./deploy.sh deploy
# Make more changes
vim deploy.sh
# Deploy again
./deploy.sh deploy# SSH to server
ssh user@example.com
# List releases
ls -lt /home/domains/example.com/releases/
# Switch to previous release
cd /home/domains/example.com
ln -sfn releases/20260103120815 current
# Restart application
pm2 restart myappThe script automatically excludes these directories from upload:
.git- Git repository datanode_modules- Dependencies (should be installed on server if needed).svelte-kit- SvelteKit build cache.claude- Claude Code configurationcontent- Managed as shared path
The script handles deployment but doesn't manage the application process. You need a process manager like PM2, systemd, or Docker.
# On remote server
cd /home/domains/example.com/current
pm2 start npm --name "myapp" -- start
pm2 save
pm2 startupCreate /etc/systemd/system/myapp.service:
[Unit]
Description=Application
After=network.target
[Service]
Type=simple
User=user
WorkingDirectory=/home/domains/example.com/current
ExecStart=/usr/bin/node index.js
Restart=on-failure
[Install]
WantedBy=multi-user.targetEnable and start:
sudo systemctl enable myapp
sudo systemctl start myappAdd to your deploy.sh or run manually after deployment:
# PM2
ssh user@example.com "pm2 reload myapp"
# systemd
ssh user@example.com "sudo systemctl restart myapp"
# Docker
ssh user@example.com "cd /home/domains/example.com && docker-compose up -d --no-deps app"==> ✗ Error: Build directory not found. Run './deploy.sh build' first
Solution: Run ./deploy.sh build before ./deploy.sh deploy
rsync: permission denied
Solution: Check SSH key authentication and write permissions on remote server
ln: failed to create symbolic link
Solution: Verify SHARED_PATH and RELEASE_PATH are correct and accessible
After deployment, application still serves old version.
Solution: Ensure your process manager points to /current symlink and restart it after deployment
==> Skipping uploads (not found locally)
Solution:
- If directory should exist locally, create it:
mkdir uploads - If directory should only exist on server, this message is normal
==> No local .env.production found, skipping sync
Solution:
- This is normal if environment file already exists on server
- To update environment file, create
.env.${ENVIRONMENT}locally and redeploy - Or manually edit file on server at
shared/.env.${ENVIRONMENT}
Application uses wrong environment configuration.
Solution:
# Check which environment was deployed
ssh user@example.com "readlink /home/domains/example.com/current/.env"
# Should show: ../../shared/.env.production (or your environment)
# Redeploy with correct environment
NODE_ENV=production ./deploy.sh allmkdir: cannot create directory: Permission denied
Solution:
- Verify SSH user has write permissions to
DEPLOY_PATH - Create parent directory manually:
ssh user@host "mkdir -p /home/domains/example.com" - Check that
DEPLOY_PATHis correct in deploy.sh
Set up passwordless SSH for smoother deployments:
# Generate SSH key if you don't have one
ssh-keygen -t ed25519 -C "deploy@example.com"
# Copy to server
ssh-copy-id user@example.com- Run tests locally
- Review changes in git
- Verify correct environment (check
$NODE_ENV) - Ensure
.env.${ENVIRONMENT}exists if updating configuration - Verify database migrations (if any)
- Backup database before major changes
- For first deployment: Run
./deploy.sh initfirst - Monitor logs after deployment
# Check application status
ssh user@example.com "pm2 status"
# Check logs
ssh user@example.com "pm2 logs myapp --lines 50"
# Verify site is accessible
curl -I https://example.com
# Check current release
ssh user@example.com "readlink /home/domains/example.com/current"
# Verify environment file symlink
ssh user@example.com "readlink /home/domains/example.com/current/.env"
# Verify all symlinks
ssh user@example.com "ls -la /home/domains/example.com/current/"- Never commit sensitive data or
.envfiles to git - Add
.env*to.gitignore(except.env.example) - Use environment-specific files (
.env.production,.env.staging) - Store environment files in
shared/directory on server (persists across deploys) - Set restrictive permissions on environment files:
chmod 600 .env.* - Restrict SSH access to deployment user only
- Use SSH key authentication, never passwords
- Use firewall to limit access to deployment ports
- Regularly update server packages and security patches
- Monitor deployment logs for unusual activity
- Audit who has access to production environment files
- Rotate secrets periodically (database passwords, API keys, session secrets)
Portano Deployment Script
Developed by Gudasoft https://gudasoft.com
© 2026 Gudasoft. All rights reserved.