Back to Article List

Set up GitHub Actions CI/CD for Node.js on a VPS

Set up GitHub Actions CI/CD for Node.js on a VPS

Create a dedicated deploy user on your VPS

GitHub Actions CI/CD pairs perfectly with self-hosted VPS deployments. You get the automation of a managed platform without the costs and constraints. This guide covers setting up a complete GitHub Actions workflow to build, test and deploy your Node.js app to a Node.js VPS automatically.

Never use root for deployments. This violates the principle of least privilege. If a deployment key gets compromised, an attacker has full root access to your server. Create a non-root deploy user with only the permissions needed to update your application.

ssh root@your-vps-ip
sudo useradd -m -s /bin/bash deploy
sudo usermod -aG sudo deploy

The deploy user can now use sudo for commands that need elevated privileges, like restarting Nginx or PM2. But the user can't directly run arbitrary commands as root.

Switch to the deploy user and generate an SSH key pair.

sudo su - deploy
ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/github-actions -N ""
cat ~/.ssh/github-actions.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
chmod 700 ~/.ssh
cat ~/.ssh/github-actions

Ed25519 keys are more secure and faster than RSA. The empty passphrase (-N "") is necessary for CI/CD automation. The key output shows your private key with the header "BEGIN OPENSSH PRIVATE KEY". Copy the entire output including the header and footer lines.

Store credentials in GitHub Secrets

Go to your GitHub repository and navigate to Settings > Security > Secrets and variables > Actions. Create two repository secrets.

First secret: DEPLOY_KEY. Paste the entire private key from the previous step, including the BEGIN and END lines.

Second secret: DEPLOY_HOST. Enter your VPS IP address or domain name.

You can also add application-specific secrets here like DATABASE_URL, API_KEYS, JWT_SECRETS and other sensitive configuration. Keep these values off your code repository.

GitHub Actions runs with access to these secrets, but they're not logged to build output. The platform masks them in logs, so you can safely reference them in your workflow.

Create the GitHub Actions workflow file

Create .github/workflows/deploy.yml in your repository root. This YAML file defines your CI/CD pipeline. The appleboy/ssh-action is widely used for VPS deployments via SSH.

name: Deploy to VPS

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '24'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test --if-present

      - name: Build application
        run: npm run build

      - name: Prepare deploy artifacts
        run: bash ./prepare-deploy.sh

      - name: Create SSH directory
        run: mkdir -p ~/.ssh && chmod 700 ~/.ssh

      - name: Add SSH key
        run: |
          echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true

      - name: Deploy via rsync
        run: |
          rsync -avz --delete \
            -e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts" \
            deploy-dist/ deploy@${{ secrets.DEPLOY_HOST }}:/var/www/myapp/

      - name: Reload application on VPS
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: deploy
          key: ${{ secrets.DEPLOY_KEY }}
          port: 22
          script: |
            cd /var/www/myapp
            npm ci --production
            pm2 reload ecosystem.config.js --wait-ready --listen-timeout 5000

      - name: Wait for app to stabilize
        run: sleep 5

      - name: Health check
        run: |
          curl -f https://${{ secrets.DEPLOY_HOST }}/health || exit 1
          echo "Health check passed"

      - name: Notify Slack on success
        if: success()
        uses: slackapi/[email protected]
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK }}
          payload: |
            {
              "text": "Deployment successful to production",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "Deployment to production succeeded.\nCommit: ${{ github.sha }}"
                  }
                }
              ]
            }

      - name: Notify Slack on failure
        if: failure()
        uses: slackapi/[email protected]
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK }}
          payload: |
            {
              "text": "Deployment failed",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "Deployment to production failed.\nCheck logs at ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
                  }
                }
              ]
            }

This workflow triggers automatically on every push to main, and also supports manual triggering via workflow_dispatch. Each step is crucial. The checkout step pulls your latest code. Node setup caches npm modules, speeding up subsequent builds by 60-80 percent.

The build and test steps verify everything works before deployment. If tests fail, the entire workflow stops. No broken code gets deployed. The rsync step transfers your build output. The reload step restarts your app on the VPS with zero downtime. Health check verifies the app is actually working after deployment. Slack notifications keep your team informed.

Understanding each workflow step

Checkout uses actions/checkout@v4, the official GitHub action for cloning your repository. This gives the workflow access to your code, package.json and prepare-deploy.sh script. For detailed GitHub Actions documentation, see the official GitHub Actions docs.

Setup Node uses actions/setup-node@v4 with Node 24, the current LTS in April 2026. The cache: npm line activates npm dependency caching. GitHub stores your node_modules in a cache bucket. On subsequent runs, the cached modules are restored instantly. Builds that previously took 3 minutes now take 30 seconds.

npm ci (clean install) is preferred over npm install in CI environments. ci reads your package-lock.json exactly and fails if the lockfile is out of sync with package.json. This guarantees reproducible builds. npm install is more forgiving and might upgrade patch versions accidentally.

The test step runs whatever test command your package.json defines. The --if-present flag means the step succeeds even if no test script exists. This prevents the workflow from failing on projects without tests.

Build creates your production bundle. Prepare-deploy.sh organizes everything for deployment. The rsync step is the actual deployment. It uses SSH authentication with your private key stored in GitHub Secrets.

The SSH action (appleboy/ssh-action) logs into your VPS and runs commands. The reload command uses PM2's graceful restart, which keeps your app online during deployment.

Handling environment variables and secrets in CI/CD

Your application needs environment variables at runtime. GitHub Actions makes these available during the build, but you must pass them to your VPS when the app starts.

Update your workflow's reload step to export environment variables before starting PM2.

      - name: Reload application on VPS
        uses: appleboy/[email protected]
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          API_KEY: ${{ secrets.API_KEY }}
          REDIS_URL: ${{ secrets.REDIS_URL }}
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: deploy
          key: ${{ secrets.DEPLOY_KEY }}
          port: 22
          script: |
            export DATABASE_URL="${{ env.DATABASE_URL }}"
            export API_KEY="${{ env.API_KEY }}"
            export REDIS_URL="${{ env.REDIS_URL }}"
            cd /var/www/myapp
            npm ci --production
            pm2 reload ecosystem.config.js --wait-ready --listen-timeout 5000

The env section makes secrets available within the step. The script section exports them so the running Node process sees them. This approach works for secrets that change per deployment.

For secrets that rarely change, store them directly on the VPS in a .env file. Have PM2 load this file. Both approaches work. Choose based on your deployment frequency and how often secrets rotate. Managing secrets this way requires discipline, buit it's straightforward when you establish a process.

Designing an effective health check endpoint

A simple curl of your home page confirms the app is responding, but a real health check endpoint is better. It verifies critical systems are working, not just that the process started.

// Express example
import { Router } from 'express';

const router = Router();

router.get('/health', async (req, res) => {
  const health = {
    status: 'ok',
    checks: {},
  };

  // Check database connection
  try {
    await db.query('SELECT 1');
    health.checks.database = 'ok';
  } catch (error) {
    health.status = 'degraded';
    health.checks.database = 'failed: ' + error.message;
  }

  // Check Redis if you use it
  try {
    await redis.ping();
    health.checks.redis = 'ok';
  } catch (error) {
    health.status = 'degraded';
    health.checks.redis = 'failed: ' + error.message;
  }

  // Return appropriate status code
  const statusCode = health.status === 'ok' ? 200 : 503;
  res.status(statusCode).json(health);
});

export default router;

This endpoint checks actual dependencies. If your database is unreachable, the health check fails and the workflow fails. You know immediately that something is wrong, before your users do. The status code matters: 200 means healthy, 503 means degraded. Your monitoring system can react accordingly.

In your workflow, curl this endpoint.

      - name: Health check
        run: |
          sleep 5
          curl -f https://${{ secrets.DEPLOY_HOST }}/health || exit 1
          echo "Application is healthy"

The sleep waits for the app to fully start. The -f flag tells curl to fail if the response is not 2xx. If health check returns 503, curl exits with an error and the workflow fails.

Branch-based deployments to staging and production

You might want to deploy to staging on one push and production on another. Extend your workflow to handle multiple environments.

on:
  push:
    branches:
      - main
      - staging

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '24'
          cache: 'npm'
      - run: npm ci
      - run: npm test --if-present
      - run: npm run build

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '24'
          cache: 'npm'
      - run: npm run build
      - run: bash ./prepare-deploy.sh

      - name: Determine deployment target
        id: target
        run: |
          if [ "${{ github.ref }}" = "refs/heads/main" ]; then
            echo "HOST=${{ secrets.PROD_HOST }}" >> $GITHUB_OUTPUT
            echo "PATH=/var/www/myapp-prod" >> $GITHUB_OUTPUT
            echo "ENVIRONMENT=production" >> $GITHUB_OUTPUT
          else
            echo "HOST=${{ secrets.STAGING_HOST }}" >> $GITHUB_OUTPUT
            echo "PATH=/var/www/myapp-staging" >> $GITHUB_OUTPUT
            echo "ENVIRONMENT=staging" >> $GITHUB_OUTPUT
          fi

      - name: Deploy
        uses: appleboy/[email protected]
        with:
          host: ${{ steps.target.outputs.HOST }}
          username: deploy
          key: ${{ secrets.DEPLOY_KEY }}
          port: 22
          script: |
            mkdir -p ~/.ssh
            echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/deploy_key
            chmod 600 ~/.ssh/deploy_key
            rsync -avz --delete \
              -e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no" \
              deploy-dist/ deploy@${{ steps.target.outputs.HOST }}:${{ steps.target.outputs.PATH }}/
            cd ${{ steps.target.outputs.PATH }}
            npm ci --production
            pm2 reload ecosystem.config.js --wait-ready

This workflow has two jobs. The test job runs regardless of which branch you push to. The deploy job runs only after test succeeds. Within deploy, the target step checks which branch you pushed to and sets different hosts and paths accordingly. Push to staging for testing, push to main for production.

Rollback strategies and failure recovery

Deployments can fail. A bad build, a database migration issue, or a configuration mistake. You need a quick rollback procedure.

The simplest rollback: Keep your previous deploy-dist folder on the VPS with a timestamped backup. If something goes wrong, you can manually switch PM2 back to the previous version while you investigate.

#!/bin/bash
# On VPS, in /var/www/myapp

# Backup current deployment
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
cp -r . ../myapp-backup-$TIMESTAMP/

# Restore previous deployment (if you have it)
cp -r ../myapp-backup-PREVIOUS/* .
npm ci --production
pm2 reload ecosystem.config.js

For more automated rollback, create a separate GitHub Actions workflow that reverts the last commit and redeploys.

name: Rollback

on:
  workflow_dispatch:
    inputs:
      commits_back:
        description: 'How many commits to revert'
        required: false
        default: '1'

jobs:
  rollback:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - run: git reset --hard HEAD~${{ github.event.inputs.commits_back }}
      - run: git push origin main --force-with-lease

This workflow reverts and redeploys when triggered manually. The force-with-lease flag prevents accidental overwrites of work others pushed. But be careful. Reverting rewrites history and might confuse your team. Use sparingly.

Caching dependencies for faster CI runs

Your workflow already caches npm modules via the setup-node action. This saves massive amounts of time. But you can optimize further. For monitoring production Node.js apps, integrate with Prometheus and Grafana to track build times and deployment metrics.

GitHub caches are per-branch. Push to a feature branch, and the cache is specific to that branch. This prevents cache pollution but means first builds on new branches are slower. Adjust your workflow to use main's cache as a fallback.

      - uses: actions/setup-node@v4
        with:
          node-version: '24'
          cache: 'npm'
          cache-dependency-path: 'package-lock.json'

The cache-dependency-path ensures the cache key only changes when package-lock.json changes. If you update a dependency, the cache is invalidated. Otherwise, it's reused. On subsequent runs, npm ci finds node_modules already downloaded and skips the network call entirely.

For monorepos with multiple package-lock.json files, list them all. For typical projects, just npm is fine.

Matrix testing for multiple Node versions

Your app might need to work on Node 22, 24 and 26 LTS versions. Test all simultaneously with a matrix.

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [22, 24]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test
      - run: npm run build

GitHub Actions runs this job twice in parallel. Once with Node 22, once with Node 24. If either fails, the entire job fails. This catches version-specific bugs early. The parallel execution means no additional time cost compared to testing a single version.

Slack and email notifications

Deployments should be visible to your team. Add Slack notifications to your workflow.

      - name: Notify Slack
        if: always()
        uses: slackapi/[email protected]
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK }}
          payload: |
            {
              "text": "Deployment ${{ job.status }} for ${{ github.repository }}",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*${{ job.status }}*\n${{ github.repository }}\n${{ github.ref }} - ${{ github.sha }}"
                  }
                }
              ]
            }

The if: always() ensures this step runs whether the job succeeded or failed. You need a Slack webhook URL stored in GitHub Secrets. Create one in your Slack workspace's incoming webhooks settings.

Troubleshooting failed deployments

SSH connection failures usually mean incorrect host, username or key. Verify your DEPLOY_HOST and DEPLOY_KEY secrets are set correctly. Test the SSH connection locally.

ssh -i your-private-key deploy@your-vps-ip

If you get a known_hosts error in the workflow, the VPS's host key isn't recognized. The workflow includes ssh-keyscan to automatically add it, but this only works if the VPS is reachable via DNS.

Permission denied errors mean the deploy user doesn't have access to the target directory. SSH in and fix permissions.

ssh deploy@your-vps-ip
ls -la /var/www/myapp/
# Ensure deploy user owns the directory
sudo chown deploy:deploy /var/www/myapp

Disk full errors happen when your VPS runs out of space. Log in and check.

df -h
du -sh /var/www/

Delete old backups or unnecessary files. If deployments are truly too large, use a bigger VPS or split your app across multiple machines.

Application timeouts during health check usually mean the app is still starting. Increase the sleep duration before health check. Or improve your health check endpoint to handle slow startup gracefully. For production Node.js security best practices, see the guide to securing a Node.js production server.

Your idea deserves better hosting

24/7 support 30-day money-back guarantee Cancel anytime
Ciclo de Pagamento

1 GB RAM VPS

$3.99 Save  50 %
$1.99 por mês
  • 1 vCPU AMD EPYC
  • 30 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Gerenciamento de firewall
  • Monitor grátis

2 GB RAM VPS

$5.99 Save  17 %
$4.99 por mês
  • 2 vCPU AMD EPYC
  • 30 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Gerenciamento de firewall
  • Monitor grátis

6 GB RAM VPS

$14.99 Save  33 %
$9.99 por mês
  • 6 vCPU AMD EPYC
  • 70 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Gerenciamento de firewall
  • Monitor grátis

AMD EPYC VPS.P1

$7.99 Save  25 %
$5.99 por mês
  • 2 vCPU AMD EPYC
  • 4 GB memória RAM
  • 40 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Backup automático incluído
  • Gerenciamento de firewall
  • Monitor grátis

AMD EPYC VPS.P2

$14.99 Save  27 %
$10.99 por mês
  • 2 vCPU AMD EPYC
  • 8 GB memória RAM
  • 80 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Backup automático incluído
  • Gerenciamento de firewall
  • Monitor grátis

AMD EPYC VPS.P4

$29.99 Save  20 %
$23.99 por mês
  • 4 vCPU AMD EPYC
  • 16 GB memória RAM
  • 160 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Backup automático incluído
  • Gerenciamento de firewall
  • Monitor grátis

AMD EPYC VPS.P5

$36.49 Save  21 %
$28.99 por mês
  • 8 vCPU AMD EPYC
  • 16 GB memória RAM
  • 180 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Backup automático incluído
  • Gerenciamento de firewall
  • Monitor grátis

AMD EPYC VPS.P6

$56.99 Save  21 %
$44.99 por mês
  • 8 vCPU AMD EPYC
  • 32 GB memória RAM
  • 200 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Backup automático incluído
  • Gerenciamento de firewall
  • Monitor grátis

AMD EPYC VPS.P7

$69.99 Save  20 %
$55.99 por mês
  • 16 vCPU AMD EPYC
  • 32 GB memória RAM
  • 240 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Backup automático incluído
  • Gerenciamento de firewall
  • Monitor grátis

EPYC Genoa VPS.G1

$4.99 Save  20 %
$3.99 por mês
  • 1 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4ª geração 9xx4 com 3,25 GHz ou similar, baseado na arquitetura Zen 4.
  • 1 GB DDR5 memória RAM
  • 25 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Backup automático incluído
  • Gerenciamento de firewall
  • Monitor grátis

EPYC Genoa VPS.G2

$12.99 Save  23 %
$9.99 por mês
  • 2 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4ª geração 9xx4 com 3,25 GHz ou similar, baseado na arquitetura Zen 4.
  • 4 GB DDR5 memória RAM
  • 50 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Backup automático incluído
  • Gerenciamento de firewall
  • Monitor grátis

EPYC Genoa VPS.G4

$25.99 Save  27 %
$18.99 por mês
  • 4 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4ª geração 9xx4 com 3,25 GHz ou similar, baseado na arquitetura Zen 4.
  • 8 GB DDR5 memória RAM
  • 100 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Backup automático incluído
  • Gerenciamento de firewall
  • Monitor grátis

EPYC Genoa VPS.G5

$44.99 Save  33 %
$29.99 por mês
  • 4 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4ª geração 9xx4 com 3,25 GHz ou similar, baseado na arquitetura Zen 4.
  • 16 GB DDR5 memória RAM
  • 150 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Backup automático incluído
  • Gerenciamento de firewall
  • Monitor grátis

EPYC Genoa VPS.G6

$48.99 Save  31 %
$33.99 por mês
  • 8 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4ª geração 9xx4 com 3,25 GHz ou similar, baseado na arquitetura Zen 4.
  • 16 GB DDR5 memória RAM
  • 200 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Backup automático incluído
  • Gerenciamento de firewall
  • Monitor grátis

EPYC Genoa VPS.G7

$74.99 Save  27 %
$54.99 por mês
  • 8 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4ª geração 9xx4 com 3,25 GHz ou similar, baseado na arquitetura Zen 4.
  • 32 GB DDR5 memória RAM
  • 250 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível na França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Backup automático incluído
  • Gerenciamento de firewall
  • Monitor grátis

FAQ

How do I deploy only when certain files change?

Add path filters to your workflow trigger.

on:
  push:
    branches:
      - main
    paths:
      - 'src/**'
      - 'package.json'
      - 'package-lock.json'
      - '.github/workflows/deploy.yml'

Now deployment only triggers if files in src/, package.json, or the workflow itself change. This prevents unnecessary deployments when you change README or documentation.

Your ideas deserve better hosting

Bring your winning ideas online faster, with modern hardware and unmetered bandwidth. Join a European cloud trusted by thousands of developers and businesses worldwide.

GPU products are in high demand at the moment. Fill the form to get notified as soon as your preferred GPU server is back in stock.