Each entry below starts with what you see, then how to find the cause, then how to fix it. Most of the commands read a log. Run them in the site folder on the server, the one with compose.yaml in it. If you are working through Run it on a server and something does not match, start here. example.com stands for your domain, and my-site for the name of the site's folder.
A good first step for almost anything is to see what is running and what the site says:
docker compose ps
docker compose logs --tail 50 appIn https mode Caddy asks Let's Encrypt for a certificate as soon as it starts. If that fails, it says why in its log:
docker compose logs --tail 50 caddy-httpsLook for lines that contain error. They are a good guide to which of these it is:
dig +short example.com and compare the answer with your server's address. DNS changes can take a while to spread.sudo ufw status) and your hosting company's own firewall. From another machine, curl -I http://example.com should answer with a redirect to https.AAAA record for an IPv6 address that the server does not answer on. Remove the record or fix the address.CMS_SITE_URL in .env is not https:// and your domain, or it still holds the address from your computer. Caddy serves exactly the address it is given. This shows it:docker compose config | grep CADDY_ADDRESSIt should print CADDY_ADDRESS: https://example.com, with your domain and nothing after it.
When you have fixed the cause, docker compose restart caddy-https makes Caddy try again at once. Let's Encrypt limits repeated failures for the same name, so if it keeps failing, find the cause before you retry again and again.
docker compose up -d stops with a message such as failed to bind host port 127.0.0.1:4321/tcp: address already in use, or with port is already allocated. Something else on the server holds a port the site needs: 80 and 443 in https mode, or the site's port in proxy mode. It may be another web server such as nginx or Apache, or an old Caddy of this site (see the next entry). This shows who holds the ports:
sudo ss -ltnp | grep -E ':(80|443|4321)\s'Stop the program that holds the port, and disable it if you do not want it back after a restart. For nginx that is sudo systemctl disable --now nginx. Then start Caddy again with --force-recreate:
docker compose up -d --force-recreate caddy-httpsThe flag matters. After the first failed start Compose has already created the Caddy container, and a plain docker compose up -d starts that one again without the ports. The command reports success and the site still does not answer. In proxy mode, use caddy-proxy instead of caddy-https.
You switched between https mode, proxy mode or a tunnel, ran docker compose up -d, and the old Caddy is still there. Compose only looks after the profiles that are switched on, so it starts the new ones and leaves the old ones alone. List everything that runs:
docker ps --filter name=my-siteRemove the old Caddy and connectors by name, then start the new set. The command does not mind if one of the names is not running:
docker compose rm -sf caddy-https caddy-proxy cloudflared newt
docker compose up -dBehind a proxy explains why both commands are needed and what to expect.
Caddy is running but cannot reach the site behind it. A short 502 while the site restarts is normal. It lasts a second or two when you restart the site and up to half a minute after a rebuild. If it does not go away, look at the site and at what Caddy says:
docker compose ps
docker compose logs --tail 20 caddy-httpsCaddy's log has a line with connection refused or no such host for each failed request. That means the site is not listening. If app shows unhealthy or keeps restarting, the next entry is your problem. In proxy mode, read caddy-proxy instead of caddy-https.
The site cannot start. docker compose ps shows app as unhealthy, and docker compose up -d ends after a couple of minutes with dependency failed to start: container my-site-app-1 is unhealthy. On a first start, Caddy is then not started at all. The site's log says why:
docker compose logs --tail 50 appLook for a line that starts with CmsError:. The word after it is the cause:
ADAPTER_ENV_MISSING means a setting is missing. The first one to check is CMS_AUTH_SECRET in .env, which must be at least 32 characters. The same code appears when a site uses Postgres, SMTP, SES or Stripe and the matching settings are not in .env.CONFIG_INVALID means site.config.ts has something the cms refuses. It happens when a shop still uses fakePayments() on an https address, and when an old setting is left in after an update.Fix the setting in .env or the code. Then run docker compose up -d again, or add --build if you changed code. Compose notices a changed .env and restarts the site.
The address in CMS_SITE_URL has to be exactly the address in the browser's address bar. The site compares them to decide whether a sign-in request really came from its own page. When they differ, the request is refused. Open your browser's developer tools, try to sign in, and look at the network tab: a response with status 403 and the code CSRF_REJECTED means the two do not match. The other common sign is an https address in CMS_SITE_URL while you open the site over plain http. Browsers then refuse the Secure cookie, and you are signed out at once. Compare the value the site uses with the address you type:
docker compose exec app printenv CMS_SITE_URLCommon causes are the address from your computer, http://127.0.0.1:4321, left in .env; a slash or a path at the end; www. in one and not in the other; and http:// where the site is served over https. Correct it in .env and run docker compose up -d. The site restarts in about twenty seconds. Behind a proxy says more about why the value matters.
A new site uses consoleEmail(), which does not send anything. It writes each message to a file inside the container and logs one line about it:
docker compose logs app | grep '\[email\]'Each line shows who the message was for, its subject and the file it went to. Set up a real provider as described in Going live.
Until then, if you have lost your admin password you can read the reset link from the file, after you ask for a reset on the sign-in page. The files live inside the container, so a rebuild deletes them. This prints the text of every message that is there:
docker compose exec app sh -c 'cat .cms/mail/*.txt'docker compose up -d --build stops during the build with a message such as process "/bin/sh -c pnpm build" did not complete successfully: exit code: 137. Exit code 137 means the system killed the process, and on a small server that almost always means it ran out of memory. The site's build used about 1.5 GB at its peak on a test machine. See how much memory and swap the server has:
free -hWith 1 GB and no swap, the build is likely to be killed. Add a swap file, which lets the server use some disk as slow extra memory, and build again:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -hThe last line of the block makes the swap file come back after a restart. A server with 2 GB of memory or more usually does not need it.
You ran a docker command as a user who is not allowed to control Docker. Either put sudo in front of the command, or add your user to the docker group, log out and log in again. Run it on a server has the command.
pnpm does not take a release until it is a day old. Name the version instead of using pnpm up. Backups and updates shows how.
The site is served over plain http on an address other than localhost, and the captcha only runs on https or localhost. Put the site behind https. For a deployment that has to stay on plain http, set security: { captcha: false } in site.config.ts. Forms covers the captcha.
There are two copies of React in the site. Make sure the site depends on react and react-dom at the same version as the cms, and that a second copy has not been installed beside them. Look for more than one version:
pnpm why reactThis applies to a site that links the cms from a checkout of its source, which is something contributors do. The development server must be allowed to serve files from that checkout. Check that node_modules/@jamiemcconnell/cms in the site folder points at the checkout you built. A site that installs the cms from the registry does not have this problem.