Build An Astro Blog And Deploy It Via FTP With GitHub Actions


GitHub
CI/CD
Actions

As you can see, my website is a static blog generated with Astro. This setup allows for blazingly fast performance and is easy to develop and deploy. I host my blog on a classic shared hosting service. The problem is, every time I write a new article, I need to run the build on my laptop and then manually upload the files to the hosting service.

Sounds like something that can be automated, right?

GitHub Actions

Fortunately, GitHub offers a solution by allowing us to run custom actions in response to specific events. But what exactly are GitHub Actions? They are scripts executed as part of a workflow triggered by GitHub events (such as a push, issue creation, new release, etc.).

You can develop any action you want using Docker containers or JavaScript (you can find more on the official documentation).

So, I decided to build a simple action that deploys files via FTP.

Create the action: lftp-upload

This action uses lftp to upload files to an FTP server. lftp is a command-line client for several file transfer protocols. It is designed for Unix and Unix-like operating systems, developed by Alexander Lukyanov, and is distributed under the GNU General Public License.

Since it’s lightweight and performant, it’s an excellent choice for automating FTP uploads with scripts. The simplest way to achieve this was by creating a Docker container to run lftp.

The code is essentially composed of three parts: an action.yml file that defines the action, a Dockerfile to create the container, and an entrypoint.sh shell script that runs lftp.

action.yml

The action.yml file defines the action. It’s straightforward and specifies all the input variables needed to run the action (these variables can be referenced in the entrypoint.sh shell script using the INPUT_ prefix).

name: "lftp-upload"
author: "Matteo De Filippis <hello@matteodefilippis.com>"
description: "A GitHub Action for uploading files to a standard FTP server through Docker and lftp"
inputs:
host:
description: "FTP host"
required: true
username:
description: "FTP username"
required: true
password:
description: "FTP password"
required: true
forceSsl:
description: "Force SSL encryption"
required: false
default: "false"
localDir:
description: "Local directory"
required: false
default: "."
remoteDir:
description: "Remote directory"
required: false
default: "."
timeout:
description: "Connection timeout"
required: false
default: "60"
retries:
description: "Max number of retries"
required: false
default: "20"
multiplier:
description: "Retry multiplier"
required: false
default: "1.5"
baseInterval:
description: "Base retry interval"
required: false
default: "5"
pConn:
description: "Number of parallel connections"
required: false
default: "5"
runs:
using: "docker"
image: "Dockerfile"
branding:
color: "white"
icon: "upload-cloud"

Dockerfile

The Dockerfile is very simple. It uses an Alpine Linux image (one of the lightest Linux distributions) and installs lftp. It then copies the entrypoint.sh script and makes it executable.

FROM alpine:latest
WORKDIR /
RUN apk --no-cache add lftp
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

Shell script

The entrypoint.sh shell script is also straightforward. It runs lftp with the appropriate arguments defined in the action.yml file.

entrypoint.sh
#!/bin/sh -l
lftp ${INPUT_HOST} -u ${INPUT_USERNAME},${INPUT_PASSWORD} -e "
set net:timeout $INPUT_TIMEOUT;
set net:max-retries $INPUT_RETRIES;
set net:reconnect-interval-multiplier $INPUT_MULTIPLIER;
set net:reconnect-interval-base $INPUT_BASEINTERVAL;
set ftp:ssl-force $INPUT_FORCESSL;
set sftp:auto-confirm yes;
set ssl:verify-certificate $INPUT_FORCESSL;
mirror -v -P $INPUT_PCONN -R -n -L -x ^\.git/$ $INPUT_LOCALDIR $INPUT_REMOTEDIR;
quit
"

The command sets all the parameters needed to run lftp and then executes it. It’s pretty straightforward, and you can read more about it here. The mirror flags enable verbose and recursive uploading while excluding the .git folder. The -P flag allows to specify the number of parallel connections that lftp will use. The -n flag ensures that only new files are uploaded, optimizing the process.

Build and deploy the website

After creating the action (or you can find it ready-to-use directly on the Marketplace), the next step is to use it in a GitHub workflow. If you want to learn more about Workflows, you can check the documentation.

All you need to do is create a YAML file like this in the .github/workflows/ folder of your repository:

.github/workflows/build-and-deploy.yml
name: Build and Deploy Website
on:
push:
branches:
- master
jobs:
build-and-deploy:
name: Build & Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Use Node.js v20
uses: actions/setup-node@v4
with:
node-version: "20.x"
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Build
run: npm run build
- name: Deploy
uses: matteodf/lftp-upload@v1
with:
host: ${{ secrets.FTP_HOST }}
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
remoteDir: ${{ secrets.FTP_REMOTE_DIR }}
localDir: ${{ secrets.FTP_LOCAL_DIR }}

Conclusion

These steps are easier than you might think, and you can adapt them to your own needs. I hope you found this article helpful! If you have any suggestions (for either the action or the article), please let me know.

Other articles