Saltar a contenido

Ejemplos

Un deploy.yml listo para cada framework, probado de punta a punta por el test e2e en Ubuntu 24.04 (y, salvo FrankenPHP, también en AlmaLinux 9). Cópialo a tu proyecto y cambia lo marcado con # change; Primeros pasos explica cada paso.

curl -fsSL -o deploy.yml https://raw.githubusercontent.com/kriollo/versaDeploy/main/examples/deploy.laravel.yml

Todas las opciones están en la referencia de configuración. Para CI hay un workflow de GitHub Actions al final.

Laravel

PHP-FPM detrás de nginx, PostgreSQL, Redis, migraciones, cron y storage. Video.

deploy.laravel.yml
# Laravel (PHP-FPM behind nginx). Copy to your project as deploy.yml and edit the
# values marked "change". Tested end to end with a fresh Laravel on Ubuntu 24.04
# (PostgreSQL) and AlmaLinux 9 (MariaDB).
#
# First deploy:
#   1. versa → Services: install the stack (I), then w (save provision) and a (services_reload)
#   2. versa deploy production --initial-deploy   → answer N to the post_deploy hooks
#   3. versa → Shared → .env → Enter → e: set APP_KEY (php artisan key:generate --show
#      on your machine), DB_*, MAIL_*… Ctrl+S saves it on the server
#   4. versa deploy production --force   (runs the migrations and caches)
project: "myapp"                         # change

environments:
  production:
    ssh:
      host: "myapp.com"                  # change
      user: "deploy"
      key_path: "~/.ssh/id_ed25519"

    remote_path: "/var/www/myapp"

    provision:                           # read only by the Services view
      ssh: { user: root }
      domain: "myapp.com"                # change
      aliases: ["www.myapp.com"]
      web_server: nginx
      php_version: "8.4"
      doc_root: "current/app/public"     # Laravel serves public/
      database: postgres                 # or mariadb (DB_CONNECTION=mariadb, DB_HOST=localhost)
      db_name: myapp
      db_user: myapp_user
      db_password: "${DB_PASS}"          # only used to create the user: DB_PASS=… versa
      redis: true                        # CACHE_STORE=redis / SESSION_DRIVER=redis (or dragonfly: true)
      ssl: true
      backup: true
      deploy_user: deploy
      cron:
        - "* * * * * php artisan schedule:run >> /dev/null 2>&1"

    ignored_paths:
      - ".git"
      - ".github"
      - "tests"
      - "node_modules"                   # assets are built into public/build
      - "vendor/bin"

    # .env lives in shared/: the first deploy creates it from .env.example.
    # storage/ too (logs, sessions, uploads): seeded once from the release.
    shared_paths:
      - ".env"
      - "storage"

    builds:
      php:
        enabled: true
        composer_command: "composer install --no-dev --optimize-autoloader"
      frontend:                          # Vite: public/build
        enabled: true
        npm_command: "npm ci"
        compile_command: "npm run build"

    services_reload:                     # PHP-FPM must reload: OPcache keeps the old release
      - "sudo systemctl reload php8.4-fpm"
      - "sudo systemctl reload nginx"

    post_deploy:
      - "php artisan migrate --force"
      - "php artisan optimize"            # config, routes, views, events cache (reads .env)
      - "php artisan storage:link --force"
      # - "php artisan queue:restart"     # with the queue service below

    # Queue worker (QUEUE_CONNECTION=database/redis): restarted on every deploy.
    # services:
    #   - name: "myapp-queue"
    #     exec: "php artisan queue:work --sleep=3 --tries=3 --max-time=3600"

    health_check:
      url: "https://myapp.com/up"        # Laravel's health route

Laravel con FrankenPHP

FrankenPHP como servicio en vez de PHP-FPM; nginx sigue sirviendo los estáticos.

deploy.laravel-frankenphp.yml
# Laravel on FrankenPHP behind nginx (no PHP-FPM). Copy to your project as deploy.yml
# and edit the values marked "change". Tested end to end with a fresh Laravel on
# Ubuntu 24.04 (PostgreSQL). nginx serves public/ and its rules; only index.php reaches
# FrankenPHP, which runs as the myapp service and reloads on every deploy.
#
# First deploy:
#   1. versa → Services: install the stack (I), then w (save provision) and a (services_reload)
#   2. versa deploy production --initial-deploy   → answer N to the post_deploy hooks
#   3. versa → Shared → .env → Enter → e: set APP_KEY (php artisan key:generate --show
#      on your machine), DB_*, MAIL_*… Ctrl+S saves it on the server
#   4. versa deploy production --force   (runs the migrations and caches)
project: "myapp"                         # change

environments:
  production:
    ssh:
      host: "myapp.com"                  # change
      user: "deploy"
      key_path: "~/.ssh/id_ed25519"

    remote_path: "/var/www/myapp"

    provision:                           # read only by the Services view
      ssh: { user: root }
      domain: "myapp.com"                # change
      aliases: ["www.myapp.com"]
      web_server: nginx
      frankenphp: true                   # instead of PHP-FPM (one PHP per server): PHP 8.5 + extensions
      proxy_port: 8000                   # FrankenPHP listens on 127.0.0.1:8000
      doc_root: "current/app/public"     # Laravel serves public/
      database: postgres                 # or mariadb (DB_CONNECTION=mariadb, DB_HOST=localhost)
      db_name: myapp
      db_user: myapp_user
      db_password: "${DB_PASS}"          # only used to create the user: DB_PASS=… versa
      redis: true                        # CACHE_STORE=redis / SESSION_DRIVER=redis (or dragonfly: true)
      ssl: true
      backup: true
      deploy_user: deploy
      cron:
        - "* * * * * php artisan schedule:run >> /dev/null 2>&1"   # php = frankenphp php-cli

    ignored_paths:
      - ".git"
      - ".github"
      - "tests"
      - "node_modules"                   # assets are built into public/build
      - "vendor/bin"

    # .env lives in shared/: the first deploy creates it from .env.example.
    # storage/ too (logs, sessions, uploads): seeded once from the release.
    shared_paths:
      - ".env"
      - "storage"

    builds:
      php:
        enabled: true
        composer_command: "composer install --no-dev --optimize-autoloader"
      frontend:                          # Vite: public/build
        enabled: true
        npm_command: "npm ci"
        compile_command: "npm run build"

    services_reload:
      - "sudo systemctl reload nginx"

    # The Services view proposes it (w) and installs its config, /etc/frankenphp/myapp.Caddyfile
    services:
      - name: "myapp"
        exec: "/usr/local/bin/frankenphp run --config /etc/frankenphp/myapp.Caddyfile"
        # deploys don't restart it (no dropped requests): the new release runs by itself and
        # this frees the old one's OPcache
        reload: "curl -fsS --unix-socket /var/www/myapp/.frankenphp-opcache.sock http://localhost/"

    post_deploy:
      - "php artisan migrate --force"
      - "php artisan optimize"            # config, routes, views, events cache (reads .env)
      - "php artisan storage:link --force"
      # - "php artisan queue:restart"     # with the queue service below

    # Queue worker (QUEUE_CONNECTION=database/redis): restarted on every deploy.
    #   - name: "myapp-queue"
    #     exec: "php artisan queue:work --sleep=3 --tries=3 --max-time=3600"

    health_check:
      url: "https://myapp.com/up"        # Laravel's health route

Next.js

npm start como servicio; node_modules se instala en el servidor.

deploy.nextjs.yml
# Next.js (SSR) as a service behind nginx. Tested end to end with Next.js 16 on
# Ubuntu 24.04 and AlmaLinux 9. package.json needs "start": "next start".
#
# Runtime variables (server only): shared/.env, edited in versa → Shared → Enter → e,
# then `versa service production restart`. NEXT_PUBLIC_* are build-time: they come
# from your machine when versa builds.
project: "myshop"                        # change

environments:
  production:
    ssh:
      host: "myshop.com"                 # change
      user: "deploy"
      key_path: "~/.ssh/id_ed25519"

    remote_path: "/var/www/myshop"

    provision:
      ssh: { user: root }
      domain: "myshop.com"               # change
      aliases: ["www.myshop.com"]
      web_server: nginx
      node_major: "22"
      proxy_port: 3000                   # nginx → the service
      database: none                     # or postgres / mariadb
      ssl: true
      deploy_user: deploy

    ignored_paths:
      - ".git"
      - ".github"
      - "node_modules/.cache"
      - ".next/cache"

    shared_paths:
      - ".env"                            # created from .env.example on the first deploy

    builds:
      frontend:
        enabled: true
        npm_command: "npm ci"
        compile_command: "NEXT_TELEMETRY_DISABLED=1 npm run build"
        # node_modules installed on the server: native modules (sharp…) for Linux.
        # Reused while package-lock.json doesn't change. Ship the lockfile.
        server_install: "npm ci --omit=dev"

    services:
      - name: "myshop"
        exec: "npm start"
        env_file: ".env"                 # <remote_path>/shared/.env
        environment:
          PORT: "3000"
          NODE_ENV: "production"
          NEXT_TELEMETRY_DISABLED: "1"

    health_check:
      url: "https://myshop.com/"

Nuxt

.output/server con Node como servicio. Video.

deploy.nuxt.yml
# Nuxt (Nitro node-server) as a service behind nginx. Tested end to end with Nuxt 4
# on Ubuntu 24.04 and AlmaLinux 9. .output/ bundles its own dependencies, so no
# node_modules goes to the server.
#
# Runtime config: NUXT_* variables in shared/.env (versa → Shared → Enter → e), then
# `versa service production restart`. NUXT_PUBLIC_* also work at runtime.
project: "mysite"                        # change

environments:
  production:
    ssh:
      host: "mysite.com"                 # change
      user: "deploy"
      key_path: "~/.ssh/id_ed25519"

    remote_path: "/var/www/mysite"

    provision:
      ssh: { user: root }
      domain: "mysite.com"               # change
      aliases: ["www.mysite.com"]
      web_server: nginx
      node_major: "22"
      proxy_port: 3000
      database: none
      ssl: true
      deploy_user: deploy

    ignored_paths:
      - ".git"
      - ".github"
      - ".nuxt"
      - "node_modules"

    shared_paths:
      - ".env"

    builds:
      frontend:
        enabled: true
        npm_command: "npm ci"
        compile_command: "npm run build"

    services:
      - name: "mysite"
        exec: "node .output/server/index.mjs"
        env_file: ".env"
        environment:
          PORT: "3000"
          NODE_ENV: "production"

    health_check:
      url: "https://mysite.com/"

FastAPI

uvicorn como servicio, virtualenv en el servidor, PostgreSQL. Video.

deploy.fastapi.yml
# FastAPI behind uvicorn and nginx. Tested end to end on Ubuntu 24.04 and AlmaLinux 9.
# requirements.txt must include fastapi and uvicorn; main.py defines `app = FastAPI()`.
#
# Secrets: shared/.env (created from .env.example on the first deploy); edit it in
# versa → Shared → Enter → e, then `versa service production restart`.
project: "myapi"                         # change

environments:
  production:
    ssh:
      host: "api.example.com"            # change
      user: "deploy"
      key_path: "~/.ssh/id_ed25519"

    remote_path: "/var/www/myapi"

    provision:
      ssh: { user: root }
      domain: "api.example.com"          # change
      web_server: nginx
      proxy_port: 8000
      database: postgres
      db_name: myapi
      db_user: myapi_user
      db_password: "${DB_PASS}"
      ssl: true
      backup: true
      deploy_user: deploy

    ignored_paths:
      - ".git"
      - ".github"
      - "tests"
      - "__pycache__"
      - ".venv"

    shared_paths:
      - ".env"

    builds:
      python:
        enabled: true
        requirements_file: "requirements.txt"
        web_server: true
        web_framework: fastapi           # run_server.sh: uvicorn main:app
        entry_point: "main.py"           # api/main.py → uvicorn api.main:app
        web_host: "127.0.0.1"
        web_port: 8000
        web_workers: 2

    services:
      - name: "myapi"
        env_file: ".env"

    # post_deploy:
    #   - ".venv/bin/alembic upgrade head"

    health_check:
      url: "https://api.example.com/docs"

Flask

gunicorn como servicio, virtualenv en el servidor.

deploy.flask.yml
# Flask behind gunicorn and nginx. Tested end to end on Ubuntu 24.04 and AlmaLinux 9.
# requirements.txt must include flask and gunicorn; app.py defines `app = Flask(...)`.
# The virtualenv is created on the server (reused while requirements.txt doesn't change).
#
# Secrets: shared/.env, created from .env.example on the first deploy; edit it in
# versa → Shared → Enter → e, then `versa service production restart`.
project: "myapi"                         # change

environments:
  production:
    ssh:
      host: "api.example.com"            # change
      user: "deploy"
      key_path: "~/.ssh/id_ed25519"

    remote_path: "/var/www/myapi"

    provision:
      ssh: { user: root }
      domain: "api.example.com"          # change
      web_server: nginx
      proxy_port: 8000
      database: postgres                 # or mariadb / none
      db_name: myapi
      db_user: myapi_user
      db_password: "${DB_PASS}"
      ssl: true
      backup: true
      deploy_user: deploy

    ignored_paths:
      - ".git"
      - ".github"
      - "tests"
      - "__pycache__"
      - ".venv"

    shared_paths:
      - ".env"

    builds:
      python:
        enabled: true
        requirements_file: "requirements.txt"
        web_server: true
        web_framework: gunicorn          # run_server.sh: gunicorn app:app
        entry_point: "app.py"
        web_host: "127.0.0.1"            # only nginx reaches it
        web_port: 8000
        web_workers: 2

    services:
      - name: "myapi"                    # exec: the release's run_server.sh
        env_file: ".env"

    # post_deploy:
    #   - ".venv/bin/flask --app app db upgrade"   # Flask-Migrate

    health_check:
      url: "https://api.example.com/"

Go (Gin)

Binario estático compilado en tu computador, como servicio.

deploy.gin.yml
# Go with Gin behind nginx. Tested end to end on Ubuntu 24.04 and AlmaLinux 9.
# The binary is cross-compiled on your machine (static, CGO_ENABLED=0): the server
# needs no Go. main.go listens on 127.0.0.1:$PORT.
#
# Secrets: shared/.env (created from .env.example on the first deploy); edit it in
# versa → Shared → Enter → e, then `versa service production restart`.
project: "myapi"                         # change

environments:
  production:
    ssh:
      host: "api.example.com"            # change
      user: "deploy"
      key_path: "~/.ssh/id_ed25519"

    remote_path: "/var/www/myapi"

    provision:
      ssh: { user: root }
      domain: "api.example.com"          # change
      web_server: nginx
      proxy_port: 8080
      database: postgres
      db_name: myapi
      db_user: myapi_user
      db_password: "${DB_PASS}"
      ssl: true
      backup: true
      deploy_user: deploy

    ignored_paths:
      - ".git"
      - ".github"

    shared_paths:
      - ".env"

    builds:
      go:
        enabled: true
        target_os: linux
        target_arch: amd64               # arm64 for Graviton/Ampere servers
        binary_name: "api"
        deploy_path: "bin"               # → current/bin/api
        build_flags: "-trimpath -ldflags=-s"

    services:
      - name: "myapi"                    # exec: current/bin/api
        env_file: ".env"
        environment:
          PORT: "8080"
          GIN_MODE: "release"

    health_check:
      url: "https://api.example.com/"

GitHub Actions

Staging en cada push a main, producción en cada tag v*.

github-actions.yml
# Deploy from GitHub Actions: copy to .github/workflows/deploy.yml.
#
# - every push to main deploys staging;
# - a version tag (git tag v1.2.0 && git push --tags) deploys production. Add
#   `deploy_tags: "v*"` to production in deploy.yml so versa refuses anything else,
#   from CI or from a laptop.
#
# Repository secrets (Settings → Secrets and variables → Actions):
#   DEPLOY_SSH_KEY      private key of the deploy user (ssh-keygen -t ed25519 -f ci_deploy)
#   DEPLOY_KNOWN_HOSTS  output of `ssh-keyscan -t ed25519 your-server` (checked by versa)
#
# deploy.yml reads the key from ~/.ssh/id_ed25519, as in the examples; set
# `strict_host_key: true` under ssh so CI never connects to an unknown host.
name: Deploy

on:
  push:
    branches: [main]
    tags: ["v*"]

# one deploy per environment at a time; a newer push waits for the running one
concurrency:
  group: deploy-${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'staging' }}
  cancel-in-progress: false

permissions:
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'staging' }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # tags and full history: versa clones this checkout

      # The toolchains your builds use (remove the ones you don't need)
      - uses: shivammathur/setup-php@v2
        with:
          php-version: "8.3"
          tools: composer
      - uses: actions/setup-node@v4
        with:
          node-version: "22"

      - name: Install versa
        run: |
          curl -fsSL https://raw.githubusercontent.com/kriollo/versaDeploy/main/install.sh | VERSA_INSTALL_DIR="$HOME/.local/bin" sh
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"

      - name: SSH key and known host
        env:
          DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
          DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
        run: |
          install -d -m 700 ~/.ssh
          printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts

      - name: Deploy
        env:
          VERSA_DEPLOYER: ${{ github.actor }} # who shows up in versa history
        run: versa deploy ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'staging' }} --config deploy.yml