plym Docs
    plym.io

    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 A record 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.