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:
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:
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.