Set up a domain
plym lets you connect your domain, issue an SSL certificate with an auto-renewal job in one command. Learn how to connect your plym blog with a domain name.
Updated Aug 04, 2026
On this page
Point a blog at a domain you own with one command. plym set url writes the reverse proxy config, obtains the TLS certificate, and rewrites the URLs baked into every rendered page.
plym never terminates TLS and never binds port 443. The Docker stack publishes one host port — 9173 by default — and a reverse proxy in front of it owns the domain and the certificate. plym set url configures that proxy for you when it is nginx, Caddy, or Traefik. For anything else, including a CDN, you write the proxy config and set three keys by hand.
DNS first
plym set url will not configure a proxy for a hostname that does not already point at the machine. It resolves the name, compares the answer to this machine's public IPv4 address, and stops when they disagree.
✗ plym.io resolves to 198.51.100.7 but this machine is 203.0.113.10. Point the A record at 203.0.113.10 and re-run.
The proxy is untouched when that happens, but config.yaml and .env already hold the new values — plym writes them before it checks DNS. Fix the record and run the same command again.
| Hostname | Record | Value |
|---|---|---|
plym.io (apex) |
A |
The server's public IPv4 address |
blog.plym.io (subdomain) |
A or CNAME |
The IPv4 address, or a name that resolves to it |
Add the record, wait for it to propagate, then run the command. Two setups fail the check even when the DNS is right:
- An IPv6-only server. The preflight reads the
Arecord and compares IPv4 only. - A CDN that proxies traffic (Cloudflare's orange cloud). The name resolves to the CDN's address, not to yours. Write the proxy config yourself, or turn proxying off, run
plym set url, and turn it back on.
--traefik skips the DNS check, because it writes a compose file instead of reconfiguring a live proxy.
Pick a proxy
Use --caddy unless nginx already serves this machine. Caddy fetches and renews certificates with no extra package and no timer to verify.
| Flag | What plym does | Needs sudo | TLS |
|---|---|---|---|
--caddy |
Installs Caddy if missing, writes /etc/caddy/plym/<host>.caddy, reloads |
Yes | Automatic, renewed by Caddy |
--nginx |
Installs nginx if missing, writes a vhost, reloads, runs certbot | Yes | Let's Encrypt through certbot |
--traefik |
Writes docker-compose.traefik.yml with router labels for you to finish |
No | Your certresolver |
The flag is required. There is no option for "some other proxy" — with HAProxy, a cloud load balancer, or a CDN, skip plym set url and do the two halves yourself:
website: plym.io
blog_home: plym.io/blog
blog_prefix: /blog
plym rebuild
Then forward your domain to the stack's port. Reverse proxy examples has a complete file for each front end.
Run it
Run it from the blog directory. plym asks for sudo itself when it reaches the proxy step.
plym set url blog.plym.io --caddy
plym will: install caddy if missing, write a caddy site for blog.plym.io, reload caddy (TLS is automatic).
blog.plym.io → plym-flapico proceed? [Y/n] y
blog.plym.io now serves plym-flapico
Admin: https://blog.plym.io/blog/plym-admin
Undo anytime with 'plym unset url'.
What the command changes
| File | Key | Value after plym set url blog.plym.io --caddy |
|---|---|---|
config.yaml |
website |
blog.plym.io |
config.yaml |
blog_home |
blog.plym.io/blog |
config.yaml |
blog_prefix |
Unchanged — written only when the URL carries a path |
.env |
PLYM_BLOG_PREFIX |
Unchanged — same rule |
.env |
PLYM_PUBLIC_URL |
blog.plym.io |
.env |
PLYM_PROXY |
caddy |
It then restarts the api and re-renders every post, drafts included. blog_home is the base of every canonical link, every sitemap.xml entry, the JSON-LD url, and the Sitemap: line in robots.txt, so the pages on disk are wrong until they are rebuilt.
The two PLYM_* keys are bookkeeping for plym unset url. Nothing reads them at runtime.
Where the blog answers
| URL | Serves |
|---|---|
https://blog.plym.io/ |
308 redirect to /blog/ |
https://blog.plym.io/blog/ |
The index |
https://blog.plym.io/blog/<category>/<slug> |
A post |
https://blog.plym.io/blog/plym-admin |
The admin portal |
https://blog.plym.io/blog/sitemap.xml |
The sitemap |
https://blog.plym.io/robots.txt |
robots.txt, with the sitemap URL in it |
The default prefix is /blog. To serve the blog at the domain root instead, or under a different path, see Subdirectory.
Apex and subdomain
plym treats the two identically. The DNS record is the only difference, and the command is the same.
What it does not do is configure a second hostname. plym set url plym.io --caddy writes a site for plym.io and nothing for www.plym.io, so the www name gets whatever your proxy does with an unknown host. Add that redirect yourself — Reverse proxy examples has both snippets.
Certificates
| Proxy | Issued by | Requirements | Renewal |
|---|---|---|---|
| Caddy | Caddy, on the first request for the hostname | Ports 80 and 443 reachable from the internet | Caddy, unattended |
| nginx | certbot --nginx --non-interactive --agree-tos --redirect |
Port 80 reachable, certbot's nginx plugin installed | certbot's own systemd timer |
| Traefik | Your certresolver | Whatever that resolver needs | Traefik |
certbot registers with the address in PLYM_SUPERUSER_EMAIL. An address ending in .local is not a real mailbox, so plym passes --register-unsafely-without-email instead — you get no expiry warnings.
A certificate failure is not fatal and does not roll anything back. plym prints the reason and the site stays up on port 80 with no TLS:
certbot failed — plym.io works over HTTP. Fix DNS/firewall and run: sudo certbot --nginx -d plym.io
--redirect is what sends HTTP to HTTPS on nginx. Caddy does it without being asked. Neither is configured by plym itself.
Close the direct port
docker-compose.yml publishes the stack on every interface:
ports:
- "${PLYM_PORT:-9173}:80"
So http://203.0.113.10:9173/blog/ serves the same blog with no TLS, skipping your proxy and its certificate. Bind it to localhost:
services:
caddy:
ports: !override
- "127.0.0.1:9173:80"
docker compose up -d
The !override tag matters. Compose appends list entries by default, which would leave both the old public binding and the new one, and the second bind then fails.
Put customizations in docker-compose.override.yml, never in docker-compose.yml. plym update copies docker-compose.yml, docker/Caddyfile, and the bundled templates out of the new image over the files in your project. docker-compose.override.yml, .env, and config.yaml are left alone.
Undo
plym unset url
→ blog.plym.io removed from caddy. The blog still runs on :9173.
It deletes the proxy config plym wrote, reloads the proxy, and clears the two PLYM_* keys. It leaves website, blog_home, and blog_prefix in config.yaml, so canonical links keep pointing at the domain you removed. Set them back before the next render.
FAQ
Can two blogs share one domain? Yes, on different paths. Each install has its own port, and each plym set url plym.io/<path> --nginx adds its own location block. See Subdirectory.
How do I move a blog to a new domain? Run plym set url again with the new URL. The old proxy config stays on disk under the old hostname — remove it with plym unset url before you switch, or delete the file yourself.
Does sudo plym set url work? From inside the blog directory, yes. From anywhere else, pass -C /path/to/blog: the active-blog marker lives in $HOME/.config/plym/active, and under sudo that is root's home, not yours.
What if port 80 is already taken by something plym does not manage? plym set url stops with an error rather than installing a proxy over it. Configure that proxy yourself and forward the blog's port.
Does plym read X-Forwarded-Proto? No. Every absolute URL comes from blog_home in config.yaml, so a misconfigured proxy cannot leak http:// into a canonical tag. Redirects are host-relative for the same reason — /blog answers Location: /blog/, and your proxy's scheme and host survive it.
Where do I change the port? PLYM_PORT in .env, then docker compose up -d. Update the proxy config to match; plym set url reads the port at the time it runs and writes it into the config.