Tip
Sign up for Hetzner Cloud via this referral link — new accounts may receive cloud credits, and Roots receives referral credit in return.
Warning
The deploy workflow prints the generated WordPress admin password in the run summary and logs. If your fork is public, anyone can read that password from your Actions tab until you change it. Either keep the fork private, or change the password via wp-admin immediately after first login.
One-click Hetzner Cloud deploy for Troy Server — a permanent, self-hosted WordPress install with the Troy Server plugin pre-activated, fronted by Caddy with automatic Let's Encrypt on a custom domain.
Fork, add one secret, click Run workflow, paste the returned IP into your DNS. Done in ~3–5 min.
- Ubuntu 24.04 on a Hetzner Cloud VM (default
cx23— Intel shared, 2 vCPU / 4 GB / 40 GB NVMe, ~€3.99/mo, EU locations only; usecpx22if you want US or Singapore) - Caddy v2 (auto HTTPS via Let's Encrypt, auto-renewing)
- PHP 8.5-FPM (from
ondrej/phpPPA) + MariaDB — all on-box, persistent - Latest WordPress, auto-installed via WP-CLI
- Latest Troy Server and Troy Client plugins, downloaded from GitHub Releases and activated (Troy Client is required to update Troy Server)
- UFW firewall limited to SSH + 80 + 443
- A Hetzner Cloud project and API token (
HETZNER_TOKEN) with read/write scope - A domain you can create an A record on (any DNS provider — no API integration needed)
- (Optional) An SSH key uploaded to your Hetzner project, so you can SSH in to debug
Settings → Secrets and variables → Actions → New repository secret:
| Secret | Where to find it |
|---|---|
HETZNER_TOKEN |
Hetzner Cloud Console → your project → Security → API tokens |
- Actions → Deploy Troy → Run workflow
- Fill in:
- fqdn — e.g.
troy.example.org - admin_email — used for WP admin + Let's Encrypt renewal notices
- Optional:
site_title,server_type,location,plugin_repo,ssh_key_names
- fqdn — e.g.
- Run workflow
- The job summary will show the server IP. Create an A record
<fqdn> → <ip>at your DNS provider. - Wait ~30 seconds after DNS propagates; Caddy will fetch the cert on the first HTTPS hit. Admin URL + password are in the summary — save the password, it is not stored anywhere else.
cloud-init.yaml.tmpl— cloud-init template with{{PLACEHOLDERS}}for domain, credentials, plugin repo, and a Caddyfile.github/workflows/deploy.yml— onworkflow_dispatch, generates passwords, renders the template, and callsPOST /v1/serverson the Hetzner API with the rendered YAML asuser_data- On first boot, cloud-init installs packages (Caddy + PHP 8.5 from ondrej's PPA), runs
/root/install-troy.shwhich: sets up MariaDB, downloads WP + WP-CLI, installs WP, pulls the latesttroy-server-*.zipandtroy-client-*.zipfrom the configured GitHub release, activates Troy Server first then Troy Client (order matters — the client is what updates the server), and starts Caddy. Caddy handles TLS on its own once the A record resolves.
Logs land in /var/log/troy-install.log on the server.
- Different plugin repo — override
plugin_repoat dispatch time (defaultsybrew/troy) - Automate the DNS step — add a step calling your provider's API between Create Hetzner server and Summary in
deploy.ymlusing${{ steps.hetzner.outputs.ip }} - Bigger box —
cx33,cx43(EU Intel), orcpx32,cpx42(AMD, any region). - ARM —
cax11is ~€4.49/mo in EU locations; everything in the stack (Caddy, PHP 8.5 via ondrej PPA, MariaDB) runs fine on arm64. - Non-EU location —
cx*andcax*are EU-only. Forash/hil/sin, usecpx22or larger. - Different admin username — edit
ADMIN_USER: adminin.github/workflows/deploy.yml
- Site loads on HTTP but not HTTPS — DNS hasn't propagated yet, or is pointing elsewhere. Check with
dig +short <fqdn>. Caddy will keep retrying; SSH in and watchjournalctl -u caddy -f. - Plugin not activated — check
/var/log/troy-install.log; the release must contain an asset matchingtroy-server-*.zip. user_data size exceeded— you've added too much to the template. Hetzner capsuser_dataat 32 KiB.