# make-my-server: a self-hosted stack you read like a folder tree

> make-my-server is a docker-compose configuration where every service lives in its own directory with its own compose file, so the whole stack, from Traefik to Nextcloud to a Tor relay, is one repository you can read top to bottom and extend with a pull request.

**tomMoulard/make-my-server** — Docker Compose with Traefik and lots of services

- Repository: https://github.com/tomMoulard/make-my-server
- Website: https://tom.moulard.org
- Stars: 591 · Forks: 72
- Language: Ruby
- License: not declared
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/tommoulard-make-my-server

## One directory per service, and a compose file per directory

The project is a server configuration managed by docker-compose with a full set of default configuration, and the organising rule is stated as a rule. There should be only one service by folder, so the traefik folder contains all the necessary configuration to run the traefik service, and each folder represents an available service. Every one of those folders follows the same shape, with a conf directory for configuration, a data directory for service data, a docker-compose file named after the service, a logs directory with an access log and an error log, and a README. That last detail is the one that makes the repository usable rather than merely complete. Every service documents itself in place, and the top level carries a list of what is there, from arachni and bazarr through bitwarden, gitlab, grafana, homeassistant, jellyfin, jupyter, kavita, mastodon, nextcloud, peertube, pihole, portainer, rocketchat, searxng, sonarr, tor-relay, traefik, transmission, vpn, watchtower and wordpress.

## The bash function that stitches the compose files together

The technical heart of the setup is a shell function, and it exists because of a specific limitation in Docker Compose. If you are using a docker compose version below 2.20, the project says you need a particular bash command to use it, and the command defines a function rather than calling a binary.

```bash
docker-compose ()
{
    docker-compose $(find -name 'docker-compose.*.yml' -type f -printf '%p\t%d\n'  2>/dev/null | sort -n -k2 | cut -f 1 | awk '{print "-f "$0}') $@
}
```

The function walks the tree for compose files, sorts them by their depth so the order is deterministic, and passes each one as a separate -f argument. The effect is that the function gathers all the docker-compose files in order to have the whole configuration in one place, which you can then inspect with docker-compose config. The root compose file itself is mostly a list of includes, one path entry per service directory, which is the visible face of the same arrangement.

## Running it, and the .env file that holds the values

Once the function is in place, starting the stack is a single command with a site variable.

```bash
SITE=tom.moulard.org docker-compose up -d
```

The documentation says to be a little more consistent with the management, you can use a .env file instead, copied from the default one.

```bash
cp .env.default .env
```

And then you edit the .env file to use the correct configuration. Tearing it all down is the mirror image of starting it.

```bash
docker-compose down
```

Before you do either, the project has a short list of things not to forget. Change the database passwords, which might not be needed since they are beyond the reverse proxy, and change the VPN secrets, and the note is that if none are provided they are generated directly. The configuration files themselves are docker-compose.yml and nginx.conf.

## Where the .env.default comes from

The default environment file is generated rather than hand-maintained, and the generation command is in the project documentation. It greps the compose files for variable references, pulls out the names, cuts them at the colon, sorts and de-duplicates them, and writes one NAME= entry per line into .env.default.

```bash
grep '${' **/docker-compose.*.yml | sed "s/.*\${\(.*\)}.*/\1/g" | cut -d":" -f 1 | sort -u | sort | xargs -I % echo "%=" >> .env.default
```

That is a nice piece of engineering, because it means the default file cannot drift out of sync with the compose files that use it. Credentials work the same way. To set the password, the documented command is an echo into the .env file.

```bash
echo "USERS=$(htpasswd -nB $USER)" >> .env
```

And further credentials are added by editing the .env file directly, in the form of a comma separated list of user and password pairs, with USERS=toto:pass,tata:pass as the example given.

## Local development means editing your hosts file

For running the stack on your own machine, the project has a blunt answer: edit the file /etc/hosts to provide the reverse proxy with good URLs. The example given maps a loopback address to a hostname.

```bash
127.0.0.1   traefik.moulard.org
```

That line lets you run and debug the Traefik service locally, and the technique generalises to the other services, which is the point of using a real reverse proxy with a hostname-based router even in development. Scaling is handled by compose itself, and the example scales the nginx service to two instances.

```bash
docker-compose scale nginx=2
```

The repository also carries a top-level docker-compose.networks.yml alongside the main include list, and a docker-compose.yml that pulls in one compose file per service directory.

## Two enforced checks: yamllint and the config test

The project treats its yaml as code, and the tests section is marked as enforced for all pull requests. Linting uses yamllint, with a .yamllint configuration file in the repository, and the documented command lints everything.

```bash
yamllint .
```

The second check is a shell script, test.sh, which the project says the GitHub Action runs for you and which provides a patch.patch file that should solve most of your issues. It tests that all docker-compose files are valid, that all of them are parsable, that all of them are consistent with the test_config.yml file, and that all environment variables are set inside the .env.default file. When the script runs and the tests fail, you see a bunch of modified files, for example test_config.yml, which indicates what is wrong. The generated patch is a pragmatic answer to the problem of a test that knows the expected state, since it hands you the diff rather than just the failure.

## Conclusion

make-my-server fits a person who wants a working self-hosted stack with reverse proxying already wired up, and fits a team that wants to add one more service without untangling a single monolithic compose file. It does not fit someone who wants a minimal deployment, since the point of the repository is breadth rather than leanness, and it does not fit anyone who cannot read compose files and shell, since the setup depends on a bash function and a generated environment file. Before you deploy, change the database passwords and the VPN secrets, run the repository's own test script and yaml linter, and remember that the default branch is master and the last push was on 2026-06-29, so the stack is maintained at a slower rhythm than the services it runs.

## FAQ

### How do I start the make-my-server stack?

Define the docker-compose function from the project documentation if you are on a Compose version below 2.20, then run SITE with your hostname and docker-compose up -d. You can also copy .env.default to .env and edit the values instead of passing the site on the command line.

### What is the directory structure for adding a new service?

Each folder represents one service and contains a conf directory for configuration, a data directory for service data, a compose file named after the service, a logs directory with access and error logs, and a README. There should be only one service by folder, and the root compose file includes each service's file.

### What do the enforced pull request checks test?

The project lints its yaml with yamllint, and a shell script test.sh checks that all compose files are valid and parsable, that they are consistent with the test_config.yml file, and that all environment variables are set in .env.default. The GitHub Action runs the script and provides a patch.patch file to fix most issues.

### Which services are included in the default configuration?

The repository holds one directory per service, including arachni, bazarr, bitwarden, ciao, codimd, elk, factorio, framadate, gitlab, grafana, homeassistant, hugo, jackett, jellyfin, jupyter, kavita, mastodon, minecraft, mumble, musicbot, n8n, nextcloud, nginx, pastebin, peertube, pihole, portainer, rocketchat, searxng, sharelatex, sonarr, streama, theia, tor-relay, traefik, transmission, vpn, watchtower and wordpress.

## Sources

- [Issues](https://github.com/tomMoulard/make-my-server/issues)
- [Project website](https://tom.moulard.org)
- [README](https://github.com/tomMoulard/make-my-server/blob/master/README.md)
- [Releases](https://github.com/tomMoulard/make-my-server/releases)
- [tomMoulard/make-my-server on GitHub](https://github.com/tomMoulard/make-my-server)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tommoulard-make-my-server
