feat: add deploy-static-site action, site-lib, images; remove deploy-oci-site

Content now served from S3 at runtime via shared static-server image.
- deploy-static-site: reads creds from Nomad vars, builds site, pushes
  tarball to S3, generates per-domain Nomad job JSON, deploys
- generate-job.py: emits Nomad job JSON for a static site deployment
- site-lib/flake.nix: mkSite helper, packages.default + devShells only
- images/flake.nix: shared static-server OCI image (sws + awscli2 + tools)
- images CI: builds and pushes static-server on images/flake.nix changes
- deploy-oci-site: removed (superseded by deploy-static-site)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Christopher Mühl 2026-02-18 11:27:27 +01:00
parent 04c2b06c14
commit 55652569b2
No known key found for this signature in database
GPG key ID: 925AC7D69955293F
7 changed files with 294 additions and 147 deletions

View file

@ -1,97 +0,0 @@
name: Deploy OCI Site
description: Build a Nix flake OCI image, push to registry, and deploy via Nomad
inputs:
domain:
description: 'Domain the site is served at (e.g. toph.so)'
required: true
registry:
description: 'Container registry host'
required: false
default: 'registry.toph.so'
s3-endpoint:
description: 'S3 endpoint for the Nix binary cache'
required: false
default: 'https://s3.toph.so'
nomad-addr:
description: 'Nomad API address'
required: false
default: 'http://172.17.0.1:4646'
smoke-test:
description: 'Run a smoke test against the domain after deploy'
required: false
default: 'true'
runs:
using: composite
steps:
- name: Install tools
shell: bash
run: nix profile install nixpkgs#skopeo nixpkgs#nomad
- name: Build OCI image
shell: bash
run: nix build .#ociImage --out-link result-image
- name: Push image to registry
shell: bash
run: |
IMAGE_TAG=$(nix eval --raw .#packages.x86_64-linux.ociImage.imageTag)
skopeo copy \
docker-archive:./result-image \
docker://${{ inputs.registry }}/${{ inputs.domain }}:${IMAGE_TAG}
skopeo copy \
docker-archive:./result-image \
docker://${{ inputs.registry }}/${{ inputs.domain }}:latest
echo "IMAGE_TAG=${IMAGE_TAG}" >> $GITHUB_ENV
- name: Sign and push Nix closure to cache
shell: bash
run: |
echo "${NIX_SIGNING_KEY}" > /tmp/nix-key
nix store sign -k /tmp/nix-key --recursive ./result-image
nix copy \
--to "s3://nix-cache?endpoint=${{ inputs.s3-endpoint }}&access-key-id=${S3_ACCESS_KEY}&secret-access-key=${S3_SECRET_KEY}" \
./result-image
rm /tmp/nix-key
env:
NIX_SIGNING_KEY: ${{ env.NIX_SIGNING_KEY }}
S3_ACCESS_KEY: ${{ env.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ env.S3_SECRET_KEY }}
- name: Register Nomad job
shell: bash
run: |
nomad job run "${{ github.action_path }}/static-site.hcl"
env:
NOMAD_ADDR: ${{ inputs.nomad-addr }}
- name: Dispatch deployment
shell: bash
run: |
nomad job dispatch \
-meta "image_tag=${IMAGE_TAG}" \
-meta "domain=${{ inputs.domain }}" \
static-site
env:
NOMAD_ADDR: ${{ inputs.nomad-addr }}
- name: Smoke test
if: inputs.smoke-test == 'true'
shell: bash
run: |
echo "Waiting for deployment..."
for i in $(seq 1 12); do
if curl -sf --max-time 5 "https://${{ inputs.domain }}/" > /dev/null; then
echo "OK — https://${{ inputs.domain }}/ is up"
exit 0
fi
echo "Attempt ${i}/12 — retrying in 5s..."
sleep 5
done
echo "Smoke test failed: https://${{ inputs.domain }}/ did not respond"
exit 1

View file

@ -1,50 +0,0 @@
job "static-site" {
namespace = "static-sites"
type = "service"
parameterized {
meta_required = ["image_tag", "domain"]
}
group "site" {
count = 1
network {
port "http" { to = 8080 }
}
service {
name = "static-site-${NOMAD_META_domain}"
port = "http"
provider = "nomad"
tags = [
"traefik.enable=true",
"traefik.http.routers.${NOMAD_META_domain}.rule=Host(`${NOMAD_META_domain}`)",
"traefik.http.routers.${NOMAD_META_domain}.entrypoints=websecure",
"traefik.http.routers.${NOMAD_META_domain}.tls.certresolver=letsencrypt",
]
check {
type = "http"
path = "/"
interval = "30s"
timeout = "5s"
}
}
task "server" {
driver = "docker"
config {
image = "registry.toph.so/${NOMAD_META_domain}:${NOMAD_META_image_tag}"
ports = ["http"]
}
resources {
cpu = 50
memory = 64
}
}
}
}

View file

@ -0,0 +1,98 @@
name: Deploy Static Site
description: Build site with Nix, push tarball to S3, deploy via Nomad with shared static-server image
inputs:
domain:
description: 'Domain the site is served at (e.g. toph.so)'
required: true
nomad-addr:
description: 'Nomad API address'
required: false
default: 'http://172.17.0.1:4646'
server-image:
description: 'OCI image for the static server'
required: false
default: 'registry.toph.so/static-server:latest'
datacenter:
description: 'Nomad datacenter'
required: false
default: 'contabo'
smoke-test:
description: 'Run a smoke test against the domain after deploy'
required: false
default: 'true'
runs:
using: composite
steps:
- name: Install tools
shell: bash
run: nix profile install nixpkgs#nomad nixpkgs#awscli2 nixpkgs#jq
- name: Read Nomad vars
shell: bash
run: |
S3_VARS=$(nomad var get -out json static-sites/s3)
echo "AWS_ACCESS_KEY_ID=$(echo "$S3_VARS" | jq -r '.Items.access_key')" >> $GITHUB_ENV
echo "AWS_SECRET_ACCESS_KEY=$(echo "$S3_VARS" | jq -r '.Items.secret_key')" >> $GITHUB_ENV
echo "AWS_ENDPOINT_URL=$(echo "$S3_VARS" | jq -r '.Items.endpoint')" >> $GITHUB_ENV
echo "S3_BUCKET=$(echo "$S3_VARS" | jq -r '.Items.bucket')" >> $GITHUB_ENV
NIX_VARS=$(nomad var get -out json static-sites/nix)
echo "NIX_SIGNING_KEY=$(echo "$NIX_VARS" | jq -r '.Items.signing_key')" >> $GITHUB_ENV
env:
NOMAD_ADDR: ${{ inputs.nomad-addr }}
NOMAD_TOKEN: ${{ env.NOMAD_TOKEN }}
- name: Build site
shell: bash
run: nix build .#default --out-link result-site
- name: Sign and push Nix closure to S3 cache
shell: bash
run: |
echo "${NIX_SIGNING_KEY}" > /tmp/nix-key
nix store sign -k /tmp/nix-key --recursive ./result-site
nix copy \
--to "s3://${S3_BUCKET}?endpoint=${AWS_ENDPOINT_URL}&access-key-id=${AWS_ACCESS_KEY_ID}&secret-access-key=${AWS_SECRET_ACCESS_KEY}" \
./result-site
rm /tmp/nix-key
- name: Upload site tarball to S3
shell: bash
run: |
SITE_HASH=$(git rev-parse --short=12 HEAD)
echo "SITE_HASH=${SITE_HASH}" >> $GITHUB_ENV
tar czf /tmp/site.tar.gz -C result-site .
aws s3 cp /tmp/site.tar.gz "s3://${S3_BUCKET}/sites/${{ inputs.domain }}/${SITE_HASH}.tar.gz"
- name: Deploy Nomad job
shell: bash
run: |
python3 "${{ github.action_path }}/generate-job.py" | nomad job run -json -
env:
NOMAD_ADDR: ${{ inputs.nomad-addr }}
NOMAD_TOKEN: ${{ env.NOMAD_TOKEN }}
DOMAIN: ${{ inputs.domain }}
SERVER_IMAGE: ${{ inputs.server-image }}
DATACENTER: ${{ inputs.datacenter }}
- name: Smoke test
if: inputs.smoke-test == 'true'
shell: bash
run: |
echo "Waiting for deployment..."
for i in $(seq 1 12); do
if curl -sf --max-time 5 "https://${{ inputs.domain }}/" > /dev/null; then
echo "OK — https://${{ inputs.domain }}/ is up"
exit 0
fi
echo "Attempt ${i}/12 — retrying in 5s..."
sleep 5
done
echo "Smoke test failed: https://${{ inputs.domain }}/ did not respond"
exit 1

View file

@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""
Generate a Nomad job JSON for a static site deployment.
Reads from environment variables, prints JSON to stdout.
Pipe to: nomad job run -json -
"""
import json
import os
import sys
domain = os.environ["DOMAIN"]
site_hash = os.environ["SITE_HASH"]
server_image = os.environ.get("SERVER_IMAGE", "registry.toph.so/static-server:latest")
datacenter = os.environ.get("DATACENTER", "contabo")
s3_bucket = os.environ["S3_BUCKET"]
job_id = "site-" + domain.replace(".", "-")
startup_cmd = (
f"mkdir -p /var/www && "
f"aws s3 cp s3://{s3_bucket}/sites/{domain}/{site_hash}.tar.gz - "
f"| tar xz -C /var/www/ && "
f"exec static-web-server --port 8080 --root /var/www"
)
nomad_template_data = (
'{{ with nomadVar "static-sites/s3" }}'
"AWS_ACCESS_KEY_ID={{ .access_key }}\n"
"AWS_SECRET_ACCESS_KEY={{ .secret_key }}\n"
"AWS_ENDPOINT_URL={{ .endpoint }}\n"
"{{ end }}"
)
job = {
"Job": {
"ID": job_id,
"Name": job_id,
"Namespace": "static-sites",
"Type": "service",
"Datacenters": [datacenter],
"Update": {
"MinHealthyTime": 5000000000, # 5s in nanoseconds
"HealthyDeadline": 60000000000, # 60s in nanoseconds
"MaxParallel": 1,
},
"TaskGroups": [
{
"Name": "site",
"Count": 1,
"Networks": [
{
"DynamicPorts": [
{"Label": "http", "To": 8080}
]
}
],
"Services": [
{
"Name": job_id,
"Provider": "nomad",
"PortLabel": "http",
"Tags": [
"traefik.enable=true",
f"traefik.http.routers.{job_id}.rule=Host(`{domain}`)",
f"traefik.http.routers.{job_id}.entrypoints=websecure",
f"traefik.http.routers.{job_id}.tls.certresolver=letsencrypt",
],
"Checks": [
{
"Type": "http",
"Path": "/",
"Interval": 30000000000, # 30s
"Timeout": 5000000000, # 5s
}
],
}
],
"Tasks": [
{
"Name": "server",
"Driver": "docker",
"Config": {
"image": server_image,
"command": "/bin/bash",
"args": ["-c", startup_cmd],
"ports": ["http"],
},
"Templates": [
{
"EmbeddedTmpl": nomad_template_data,
"DestPath": "secrets/s3.env",
"Envvars": True,
}
],
"Resources": {
"CPU": 100,
"MemoryMB": 128,
},
}
],
}
],
}
}
json.dump(job, sys.stdout, indent=2)
print()

View file

@ -0,0 +1,23 @@
name: Build and Push Images
on:
push:
branches: [main]
paths:
- 'images/flake.nix'
workflow_dispatch:
jobs:
build-static-server:
runs-on: nix
steps:
- uses: actions/checkout@v4
- name: Build static-server image
run: nix build ./images#staticServer --out-link result-static-server
- name: Push to registry
run: |
nix shell nixpkgs#skopeo -c skopeo copy \
docker-archive:./result-static-server \
docker://registry.toph.so/static-server:latest

30
images/flake.nix Normal file
View file

@ -0,0 +1,30 @@
{
description = "Shared infrastructure OCI images";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
# Single image used by every static-site Nomad job.
# At container startup it downloads the site tarball from S3, then serves it.
# The Nomad job spec overrides Cmd with the domain- and hash-specific fetch+serve command.
packages.${system}.staticServer = pkgs.dockerTools.buildLayeredImage {
name = "static-server";
tag = "latest";
contents = with pkgs; [
static-web-server
awscli2
bash
coreutils
gnutar
gzip
cacert
];
config.ExposedPorts."8080/tcp" = { };
};
};
}

35
site-lib/flake.nix Normal file
View file

@ -0,0 +1,35 @@
{
description = "Shared Nix library for static site flakes";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs }: {
lib.mkSite =
{ self
, nixpkgs
, src
, system ? "x86_64-linux"
, buildPhase ? "true"
, installPhase ? ''
mkdir -p $out
cp -r dist/. $out/
''
, devPackages ? []
}:
let
pkgs = nixpkgs.legacyPackages.${system};
site = pkgs.stdenv.mkDerivation {
name = "site";
inherit src buildPhase installPhase;
};
in
{
packages.${system}.default = site;
devShells.${system}.default = pkgs.mkShell {
packages = devPackages;
};
};
};
}