Deploying a Next.js Static Site to an FTP Server

How I moved my campus website away from a bloated WordPress setup and fully automated the deployment process using GitHub Actions and FTP.

A while back, I looked into how my campus website was being hosted. To my horror, they were running a fragile combination of WordPress alongside ASP.NET on their own legacy servers. It was slow, hard to maintain, and quite frankly, a mess.

Recently, they reached out and asked me to build a completely new website from scratch. I enthusiastically agreed and reached for my go-to framework: Next.js.

The development went smoothly. But when it came to deployment, my initial plan—throwing it up on Vercel—hit a roadblock. The administrative requirement was strict: it must be hosted on their existing FTP server.

The Manual Approach

Since the site was purely informational, I didn't need a Node.js server. I configured my Next.js project to output a static export. This drops a production-ready static build directly into an out/ folder.

Here's the snippet for my next.config.ts file that makes this magic happen:

next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "export", // Enables static HTML export
  trailingSlash: true, // Recommended for static exports
  images: {
    unoptimized: true, // Required for static export
  },
  trailingSlash: true, // Ensures all routes end with a slash
};

export default nextConfig;

Initially, I resorted to opening FileZilla, connecting to the FTP server, and manually dragging and dropping the out folder. It was quick enough, but I knew immediately this wasn't a sustainable, modern workflow.

Automating with GitHub Actions

No developer wants to deploy manually. I decided to explore CI/CD options and naturally gravitated towards GitHub Actions. I found an awesome action that handles FTP syncing flawlessly.

1. Setting up Repository Secrets

Before creating the workflow, we need to securely store our FTP credentials so they aren't exposed in the codebase. In your GitHub repository, head over to Settings > Secrets and variables > Actions and create the following repository secrets:

  • FTP_SERVER (e.g., ftp.example.com)
  • FTP_USERNAME
  • FTP_PASSWORD

2. Creating the Workflow File

With the secrets in place, I created the following workflow file under .github/workflows/deploy.yml:

deploy.yml
name: Deploy to FTP

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  deploy:
    name: Build & Deploy
    runs-on: ubuntu-latest

    steps:
      - name: 🚚 Get latest code
        uses: actions/checkout@v4

      - name: 🟢 Use Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '24.x' // Use the latest Stable Node.js version
          cache: 'npm'

      - name: 📦 Install dependencies
        run: npm install

      - name: 🛠️ Build project
        run: npm run build

      - name: 🚀 Sync files via FTP
        uses: SamKirkland/FTP-Deploy-Action@4.3.3
        with:
          server: ${{ secrets.FTP_SERVER }}
          username: ${{ secrets.FTP_USERNAME }}
          password: ${{ secrets.FTP_PASSWORD }}
          local-dir: ./out/
          exclude: |
            **/.git*
            **/.git*/**
            **/node_modules/**

By securely storing the credentials in my GitHub Repository Secrets, pushes to the main branch automatically trigger a fresh build. The action safely syncs the newly compiled static files straight onto the legacy FTP server. We enjoy the developer experience of modern tooling without abandoning the required hosting environment.