Thanks for helping improve these Tailscale sidecar examples.
The rules on this page apply to new stacks and to the parts of an existing stack that you change. Existing stacks are brought in line over time, so you do not have to fix the rest of a stack that you touch.
-
Copy the service template from the repository root. Replace
my-servicewith your service name:cp -R templates/service-template services/my-service
This command includes the hidden
.envfile. Use a lowercase directory name. -
Update
.env.Set
SERVICE, usually to the directory name, and setIMAGE_URL,SERVICEPORT, and the application variables. Define every variable thatcompose.yamluses, unless it has a default such as${VAR:-value}. Put comments on their own line:VAR= # notesetsVARto# note..envis published with the repository. Never commit a working auth key, password, token, or other credential. Leave each secret empty. When the stack cannot start without it, require it incompose.yaml:- DB_PASSWORD=${DB_PASSWORD:?Set DB_PASSWORD in .env}Compose then stops with an error until the user sets a value. Above the empty variable in
.env, add a comment that starts with# Required:and says what to set, such as# Required: signs the sessions. Generate it with: openssl rand -hex 32. KeepTS_AUTHKEYand optional secrets without:?. -
Adapt
compose.yaml.- Keep the Compose service keys
tailscaleandapplication. - Name the containers
tailscale-${SERVICE}andapp-${SERVICE}, unless the stack documents a deviation under "Deviations from the standard setup". - Keep
network_mode: service:tailscaleon the application. - Keep the application's health-based dependency on
tailscale. - Add all required persistent volumes.
- Add required devices and capabilities explicitly.
- When the stack needs another Compose service key or container name, more
than one application container, or none, list the difference and the
reason under "Deviations from the standard setup". Prefer
app-${SERVICE}-<role>for extra containers, such asapp-${SERVICE}-db. - Pass
PUIDandPGIDonly when the image reads them, such as LinuxServer.io images. KeepTZ, as the template does. - Write the image tag explicitly and use the tag upstream recommends. Pin databases, caches, and search engines to a major or major.minor tag. When you pin an exact version, say why in a comment.
- Keep application data under
./${SERVICE}-data/, so that.gitignorecovers it. A file that the repository must track inside such a folder needs its own exception in.gitignore, as for Donetick and Dozzle.
- Keep the Compose service keys
-
Set the Serve proxy to the application's internal port.
Hard-code the port in the Serve JSON. Do not use
${SERVICEPORT}: it is meant for the optional LANportsblock and can differ from the port that Serve must proxy to. Keep runtime variables escaped, such as$${TS_CERT_DOMAIN}.Keep the
portsblock commented for Tailnet-only access. Document any LAN port you expose. When nothing in the stack uses Tailscale Serve, remove the Serve configuration: theTS_SERVE_CONFIGline, theconfigsentry of thetailscaleservice, and the top-levelconfigsblock. -
Configure the application's health check.
Use the first option that the image supports.
- When the image defines its own
HEALTHCHECK, omit the block and add a# Healthcheck: defined by the image (...)comment that names the command or what it checks. Override the image's check only when it does not work with the stack, and say why in a comment. - Call an application endpoint with a client that ships with the image.
- Run a health command that ships with the image.
- Check a fixed process name with
pidoforpgrep -x, when the container has no endpoint or the image has no HTTP client.pgrep -xmatches at most 15 characters of the process name. When the process name is generic, such aspython, usepgrep -fwith a fixed pattern. - When the image has no shell, HTTP client, or health command, no check
is possible. Omit the block and add a
# Healthcheck: none possible ...comment that names the reason.
Use the comment text from the template's
compose.yaml, which covers the same options, the database case, and the no-check cases. Run the command inside the running container before you commit it. Keep the Tailscale health check. - When the image defines its own
-
Complete the service README.
Every service README uses the headings of the template, in the same order. Do not rename them, and add no headings other than the optional sections below, so that every service reads the same way. Replace the placeholders in capitals.
- Introduction. The paragraph under the title. Say what the service does in one or two sentences and link to the upstream project. Leave feature lists to upstream.
- At a glance. Give the Tailnet address, the port that Tailscale Serve
forwards to, the image, and the data paths on the host. Add a row for
each further port that users connect to, such as DNS or SMTP. Write
Nonefor the web interface when there is none, and add anAPIrow when Serve publishes an API. - Before you start. List only what the Quick Start does not cover:
values that must change in
.env, secrets to generate, folders to create, and required host groups or devices. List every required secret and say that Compose stops with an error until it is set. - Deviations from the standard setup. List every difference from the standard setup and give the reason. Examples are extra containers, published host ports, a changed or removed Serve configuration, DNS settings, and added capabilities.
- First run. Describe what the user does after the first start, such
as creating the first account or finding a generated password. When there
is nothing to do, write
Nothing to set up. Open the web interface. - Links. Link to the upstream documentation and source code.
When a section has nothing to report, keep the heading and the sentence from the template. A reader can then tell that the service follows the standard setup, and that the section was not forgotten.
Add these optional sections between "First run" and "Links" when the service needs them, in this order:
- Configuration. Optional settings that users commonly change.
- Troubleshooting. Known errors and their solutions.
- Upgrading. Required when a change makes existing users act, such as a
new required secret, a moved data folder, or a changed
SERVICE. Say what to do, as in FossFLOW.
State only what you confirmed in the Compose file, the upstream documentation, or a running stack. Put guidance that applies to more than one service in
documentation/and link to it. -
Add the service to the correct category in the root
README.md.Insert the row in alphabetical order of the bold name, ignoring case.
- Read the service README and Compose file before you make changes.
- Preserve the shared network namespace and Tailscale dependency.
- Preserve persistent volumes unless you document a safe migration.
- Use
${VARIABLE}for Compose interpolation, not$(VARIABLE). - Update the service README when ports, paths, setup, or behavior change.
- Update the root service list when you add, remove, or rename a service.
- Changing
SERVICErenames the Tailnet device and its address, the containers, and the./${SERVICE}-datafolder. Treat it like a volume change and add an "Upgrading" section. - Do not rename existing containers or service keys. Other containers and
.envvalues may refer to them.
Preserve valid service-specific exceptions.
Run Compose validation from each changed service directory. Stacks that
require your own secrets stop with "required variable ... is missing a value".
Give those variables a dummy value on the command line, as CI does, instead of
editing .env:
env $(grep -ohE '\$\{[A-Za-z0-9_]+:\?' compose.yaml .env | sed -E 's/^\$\{//; s/:\?$/=dummy/' | sort -u) docker compose config --quietThe command must exit with status 0 and add no "variable is not set" warnings. It does not prove that the application works.
The template leaves SERVICE and IMAGE_URL empty. To validate it, run
SERVICE=dummy IMAGE_URL=dummy docker compose config --quiet from
templates/service-template/.
From the repository root, lint all Markdown with the rumdl version that
.github/workflows/linting.yml pins in its version: input:
version=$(sed -nE 's/.*version: "([0-9.]+)".*/\1/p' .github/workflows/linting.yml)
uvx "rumdl@$version" check --config .markdownlint.yml .Without uv, download that version from the
rumdl releases and run
rumdl check --config .markdownlint.yml ..
After you stage new files, check whitespace with git diff --check origin/main.
In a fork, compare with the main branch of this repository instead, for
example upstream/main.
When possible, start the stack from a copy outside the repository, such as
cp -R services/my-service /tmp/my-service, and put real values only in the
copy. A running stack writes ./config, ./ts/state (with the Tailscale
device key), and its data folders next to compose.yaml. Before you commit,
run git status and stage only the files you changed on purpose.
Then confirm:
- Tailscale becomes healthy and joins the Tailnet.
- The application starts and is reachable through the Tailnet.
- The application's main function works.
- Persistent storage and documented LAN access work, when applicable.
Title the pull request Service: what changes, such as Umami: new service.
For several services, list them or use All services:. For other areas, use
CI:, Docs:, or Template:. Fill in the pull request template, and report
the checks you ran and any checks you could not run. Maintainers apply the
labels.