Ever since DHH migrated from the cloud to their own machines last year, the indie community have been trying to replicate that. Add in the Serverless drama over the last few weeks and more people have set up Linode and others to run their workloads instead.

Most of these cloud platforms have an easy way to hook into your Github repo to auto deploy your app on changes/push, the same cannot be said for a traditional machine.

Earlier on, I used to run a K8s cluster for a bunch of other things so I just naturally just ended up hosting small projects I work on like Fotion there. But over the last few days, I had to kill the K8s cluster as it was no longer needed, thus making running k8s for Fotion and others extremely overkill, so I migrated them all to Linode.

This article is a simple walkthrough of how deployment is automated amongst others.

Check out Fotion at https://usefotion.app

What I use

  • Linode (bare machine)
  • Infisical Cloud ( to store secrets ). No AWS secrets and Hashicorp Vault too complicated to selfhost.
  • Github actions
  • Terraform and Ansible

I will use fotion in this example but you can always change to your preferred app names.

This guide assumes you already have the machine created and running

Create a SystemD file on the server

I use Ansible to do this but for simplicity case, we will do it the crude way

The first thing to do is to create a SystemD service. It allows you configure the entire lifecycle of an application using the systemctl command.

You need to ssh into the server :)

You will need to create a file in the location /usr/lib/systemd/system/fotion.service

[Unit]
Description=Fotion Backend API

[Service]
ExecStart=/usr/local/bin/fotion --env.file /root/fotion/.env
User=root
Group=root
UMask=007

[Install]
WantedBy=multi-user.targe

Take a look at the ExecStart section, we start a binary called fotion. You can verify this works by running systemctl start fotion. This should succeed but you can always verify the status of a systemd service by running journalctl -xeu fotion. This should show us that the service failed - expected because we do not have the binary

Automate deployment using Github Actions

The next piece is to automate the deployment. This deployment step will add the fotion binary into the server. And replace the binary in place, then restart the server.

Another important piece of information is certain configuration values are loaded via environment variables. I do not anually edit the .env file, instead I use Infisical to manage them. Our Github actions deployment script will fetch the latest updates from Infisical and write them to a .env file which will then be copied to the server using scp.

name: Deploy to Linode
on:
  push:
    branches:
      - main
jobs:
  deploy-fotion:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # - name: Migrate postgres
      #   run: goose -dir migrations postgres "host=${{ secrets.POSTGRES_HOST }} port=5432 user=${{ secrets.POSTGRES_USER }} password=${{ secrets.POSTGRES_PASSWORD }} dbname=crawler sslmode=require" up

      - name: Build app
        run: go build -o fotionapp cmd/server/main.go

      - name: SCP to Linode instance ( Binary )
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.IP_ADDRESS }}
          username: "root"
          key: ${{ secrets.SSH_KEY }}
          port: 22
          source: "fotionapp"
          target: "/root"

      - uses: zerodays/action-infisical@v1
        name: Load .env from Infisical
        with:
          infisical_token: ${{ secrets.INFISICAL_TOKEN }}
          workspace_id: ${{ secrets.INFISICAL_WORKSPACE_ID }}
          environment: "production"

      - name: SCP to Linode instance ( .env )
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.IP_ADDRESS }}
          username: "root"
          key: ${{ secrets.SSH_KEY }}
          port: 22
          source: ".env"
          target: "/root"

      - name: Restart Fotion systemd service
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.IP_ADDRESS }}
          username: "root"
          key: ${{ secrets.SSH_KEY }}
          port: 22
          script:
            sudo systemctl stop fotion && sudo mv /root/fotionapp /usr/local/bin/fotion &&
            sudo mkdir -p /root/fotion && sudo mv /root/.env /root/fotion/.env &&
            sudo systemctl restart fotion && sudo systemctl status fotion