Composite action for deploying Nix flake OCI images to Nomad. Owns the static-site parameterized Nomad job template, all infra defaults (registry, S3, Nomad addr), and an optional smoke test. Site repos only need to provide a flake with an ociImage output and pass domain + 3 secrets (S3_ACCESS_KEY, S3_SECRET_KEY, NIX_SIGNING_KEY). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|---|---|---|
| deploy-nix-site | ||
| deploy-oci-site | ||
| deploy-site | ||
| .gitignore | ||
| README.md | ||
CI Actions
Reusable Forgejo/Gitea actions for toph's infrastructure.
Available Actions
deploy-nix-site (Recommended)
Deploy a static site built with Nix flake to S3 and Nomad. Provides proper isolation and reproducibility.
Requirements:
- Repository must have a
flake.nixwith a default package output - Runner label:
nix(usesdocker://nixos/nix:latest)
Usage:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: nix
steps:
- uses: actions/checkout@v4
- uses: https://git.toph.so/toph/ci-actions/deploy-nix-site@main
with:
site-name: mysite
traefik-rule: Host(`mysite.example.com`)
env:
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
NIX_SIGNING_KEY: ${{ secrets.NIX_SIGNING_KEY }}
NOMAD_TOKEN: ${{ secrets.NOMAD_TOKEN }}
Inputs:
site-name(required): Site identifiertraefik-rule(required): Traefik routing ruleflake-output(optional): Nix flake output, defaults to.#s3-endpoint(optional): S3 endpoint, defaults tohttps://s3.toph.so
Example flake.nix:
{
description = "My static site";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
packages.${system}.default = pkgs.stdenv.mkDerivation {
name = "my-site";
src = ./.;
buildInputs = [ pkgs.nodejs_20 ];
buildPhase = ''
npm ci
npm run build
'';
installPhase = ''
cp -r dist $out
'';
};
devShells.${system}.default = pkgs.mkShell {
packages = [ pkgs.nodejs_20 pkgs.vite ];
shellHook = "echo 'Run: vite'";
};
};
}
deploy-site
Deploy a static site to production via S3 and Nomad (non-Nix).
Usage:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Build site (if needed)
run: npm run build
- name: Deploy
uses: https://git.toph.so/toph/ci-actions/deploy-site@main
with:
site-name: mysite
traefik-rule: Host(`mysite.example.com`)
source-dir: dist # optional, defaults to current dir
env:
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
NOMAD_TOKEN: ${{ secrets.NOMAD_TOKEN }}
Inputs:
site-name(required): Site identifier used as Nomad service nametraefik-rule(required): Traefik routing rule (see examples below)source-dir(optional): Directory with built files, defaults to.s3-endpoint(optional): S3 endpoint, defaults tohttps://s3.toph.so
Environment variables:
S3_ACCESS_KEY: S3 access key (set via Forgejo secrets)S3_SECRET_KEY: S3 secret key (set via Forgejo secrets)NOMAD_TOKEN: Nomad ACL token for thestatic-sitesnamespace (set via Forgejo secrets, auto-synced bynomad-acl-forgejo-sync)
What it does:
- Packages the site directory as a tarball
- Uploads to S3 at
s3://artifacts/<commit-sha>.tar.gz - Sets public-read ACL on the artifact
- Dispatches Nomad job to deploy the site
- Site becomes available via the specified Traefik rule with Let's Encrypt SSL
Infrastructure handles:
- Docker containers (static-web-server)
- Resource limits (100 CPU, 64MB RAM)
- Traefik routing & Let's Encrypt SSL
- Automatic restarts
Traefik Rule Examples
Single domain:
traefik-rule: Host(`example.com`)
Multiple domains (with www):
traefik-rule: Host(`example.com`) || Host(`www.example.com`)
Subdomain:
traefik-rule: Host(`blog.example.com`)
toph.so domain:
traefik-rule: Host(`mysite.toph.so`)
Path-based routing:
traefik-rule: Host(`example.com`) && PathPrefix(`/docs`)
Setup
1. Deploy S3 service (SeaweedFS)
cd /srv/infra
nix run .#bosun -- run s3
2. Generate binary cache signing keys
nix-store --generate-binary-cache-key cache.toph.so cache-priv-key.pem cache-pub-key.pem
# Public key (add to nix.conf trusted-public-keys on hosts that will fetch):
cat cache-pub-key.pem
# Example: cache.toph.so:9zFo64TPnxaQeyFM6NS9ou2Fd8OQv4Ia+MuLMjLBYjY=
# Private key (store in Forgejo secrets):
cat cache-priv-key.pem
# Keep this secret!
3. Create S3 buckets
# Configure AWS CLI
export AWS_ACCESS_KEY_ID=<your-access-key>
export AWS_SECRET_ACCESS_KEY=<your-secret-key>
export AWS_ENDPOINT_URL=https://s3.toph.so
export AWS_EC2_METADATA_DISABLED=true
# Create binary cache bucket
aws s3 mb s3://nix-cache
# (Optional) Create artifacts bucket for non-Nix deployments
aws s3 mb s3://artifacts
aws s3api put-bucket-acl --bucket artifacts --acl public-read
4. Configure alvin to trust the binary cache
Add to /srv/infra/hosts/alvin/default.nix:
nix.settings = {
substituters = [
"https://cache.nixos.org"
"s3://nix-cache?endpoint=s3.toph.so&scheme=https"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"cache.toph.so:9zFo64TPnxaQeyFM6NS9ou2Fd8OQv4Ia+MuLMjLBYjY=" # Your public key
];
};
Then deploy: sudo nixos-rebuild switch --flake .#alvin
5. Add Forgejo secrets
In your repository settings (or organization settings for global secrets):
S3_ACCESS_KEY: S3 access keyS3_SECRET_KEY: S3 secret keyNIX_SIGNING_KEY: Contents ofcache-priv-key.pemNOMAD_TOKEN: Auto-synced bynomad-acl-forgejo-syncon alvin (or set manually fromcat /var/lib/nomad-acl/ci.token)
6. Configure SSH access from runner to alvin
The runner needs to pull store paths to alvin's /nix/store. Add the runner's SSH key to alvin or use an agent socket mount.
Examples
Simple HTML site
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- uses: https://git.toph.so/toph/ci-actions/deploy-site@main
with:
site-name: mysite
traefik-rule: Host(`mysite.toph.so`)
source-dir: . # HTML files in repo root
env:
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
NOMAD_TOKEN: ${{ secrets.NOMAD_TOKEN }}
Node.js/Vite site with custom domain
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install & build
run: |
npm ci
npm run build
- uses: https://git.toph.so/toph/ci-actions/deploy-site@main
with:
site-name: myapp
traefik-rule: Host(`app.example.com`) || Host(`www.app.example.com`)
source-dir: dist
env:
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
NOMAD_TOKEN: ${{ secrets.NOMAD_TOKEN }}
Hugo site
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
with:
submodules: true # For Hugo themes
- name: Setup Hugo
run: |
wget https://github.com/gohugoio/hugo/releases/download/v0.121.0/hugo_extended_0.121.0_linux-amd64.tar.gz
tar xzf hugo_extended_0.121.0_linux-amd64.tar.gz
sudo mv hugo /usr/local/bin/
- name: Build
run: hugo --minify
- uses: https://git.toph.so/toph/ci-actions/deploy-site@main
with:
site-name: myblog
traefik-rule: Host(`blog.example.com`)
source-dir: public
env:
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
NOMAD_TOKEN: ${{ secrets.NOMAD_TOKEN }}
S3 Access
- API endpoint: https://s3.toph.so
- Console: https://s3-console.toph.so (restricted to Tailscale)
- Credentials: Stored in Nomad variables at
nomad/jobs/s3 - Backend: SeaweedFS (S3-compatible)
Troubleshooting
AWS CLI not found
The action automatically installs AWS CLI if not present on the runner.
Access denied during upload
Check that:
S3_ACCESS_KEYandS3_SECRET_KEYare set in Forgejo secrets- The credentials match those in the S3 Nomad job config
- S3 service is running:
nomad job status s3
Site not accessible
Check:
- Nomad allocation is running:
nomad job status static-site - Traefik has picked up the service: check Traefik dashboard
- DNS resolves:
dig <site-name>.toph.so - Certificate is valid:
curl -v https://<site-name>.toph.so
Development
To add a new action:
mkdir -p new-action
cd new-action
vim action.yaml
Then commit and push to main.
License
MIT