# Deployment Guide > How a project gets onto **rethink-net** — our Ubuntu 26.x servers. Get your > container to follow a few consistent rules and deploying is mostly handing us a > `compose.yaml`. !!! tip "What can run here" APIs, websites, applets, bots, monitors. ## Docker — the services account Every service runs containerized as the shared **`services`** account: **uid/gid 1337**, fixed fleet-wide. Build your image to be **uid-agnostic** so it runs cleanly as that account. ```dockerfile FROM python:3.12-slim ENV HOME=/tmp # (1)! WORKDIR /app RUN apt-get update \ && apt-get install -y --no-install-recommends git \ && rm -rf /var/lib/apt/lists/* # (2)! COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # (3)! COPY . . RUN chmod -R a+rwX /app # (4)! CMD ["python", "-m", "yourapp"] ``` 1. `HOME=/tmp` — the `services` account has no home dir; anything writing to `$HOME` (caches, configs) needs a writable target. 2. Include `git` **only if the container itself needs it** — e.g. you `pip install` from git, or the app shells out to git at runtime. The build and host always have git; this line is about what's *inside* the image. 3. Install deps **before** copying the code (see [layer caching](#layer-caching)). 4. `chmod -R a+rwX /app` makes the app tree writable by **any** uid — that's what "uid-agnostic" means. ```yaml services: yourapp: build: . user: "1337:1337" # (1)! environment: HOME: /tmp volumes: - /srv/config//:/app/config:ro # (2)! - /srv/logs//:/app/logs # (3)! - yourapp-data:/app/data # (4)! volumes: yourapp-data: ``` 1. Run as the shared account. **No** in-container `user`/`useradd` — don't bake a user into the image; set it here. 2. Configs: host-managed bind mount, mounted **read-only**. 3. Logs: bind mount — live and rolled, scraped for monitoring. 4. Everything else: a **named volume**. Docker owns it, so there are no host permissions to fiddle with. ## Paths and mounts Everything host-side follows one shape: `/srv////`. !!! warning "What `` is" `` is the owning bucket for a project. - **An individual dev?** It's your **lowercase username** — `ricky`, `xattam`, … - **A shared / official project?** It's the **workspace** it belongs to — `bots`, `web`, `apis`, … | What | Where | How | | --- | --- | --- | | Repo + compose | `/srv/docker///` | created by the git clone, not pre-provisioned | | Config | `/srv/config///` | bind mount, host-managed, read-only | | Logs | `/srv/logs///` | bind mount; live + rolled, scraped | | Mounts (other host-visible data) | `/srv/mounts///` | bind mount, host-managed | | Caches, profiles, scratch | named volume | Docker manages ownership | All `/srv/...` paths are owned by the `services` user (uid/gid **1337**). !!! note "If your service won't start or its logs aren't persisting" That's usually a host-side bind-mount **ownership** thing — the kind of detail **we sort out at deploy time**, not something you need to chown or provision. If a bot won't come up or logs/caches keep vanishing, flag it and we'll fix the mount perms. Stick to a clean `compose.yaml` and let us handle the host. ## Layer caching Copy `requirements.txt` and `pip install` **before** `COPY . .`. Docker caches layers in order, so deps only reinstall when `requirements.txt` changes — not on every code edit. Get this backwards and every one-line change triggers a full dependency reinstall. ```dockerfile COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # cached until deps change COPY . . # changes every build ``` ## Subprocess and browser workloads Bots that spawn Chrome, Xvfb, ffmpeg, or other child processes need three extra knobs in compose: ```yaml services: yourbot: build: . user: "1337:1337" init: true # (1)! shm_size: "2gb" # (2)! mem_limit: "4g" # (3)! ``` 1. Runs **tini** as PID 1 to reap zombie subprocesses and forward signals. Without it, spawned Chrome/Xvfb processes leak as zombies. 2. Chrome and most headless browsers crash on Docker's default **64 MB** `/dev/shm`. Bump it for any browser workload. 3. Bound memory — especially when each worker spawns a browser. Raise it as worker count grows. !!! warning "The PID-1 gotcha with shell-wrapper CMDs" If your `CMD` is a shell-script wrapper (e.g. `xvfb-run ...`), it must **not** be PID 1, or the real process dies on startup. `init: true` is exactly what fixes this — tini takes PID 1, and your wrapper runs as a normal child. ## What your compose / Dockerfile needs - `user: "1337:1337"` - bind mounts for **configs + logs** - named volumes for **the rest** - secrets bind-mounted **`:ro`** - `HOME=/tmp` - `chmod -R a+rwX /app` - deps installed **before** the code copy (layer caching) - `git` in the image **if the container needs it** - for browser/subprocess workloads: `init: true`, `shm_size`, `mem_limit` ## Secrets !!! warning "Secrets never go in the image" We do **not** commit secrets (usually, lol). They stay **gitignored**, live on the host at `/srv/config///`, and are bind-mounted **read-only** at runtime. Add them to `.dockerignore` so a `COPY . .` can't sweep them into a layer. Rotating a secret = edit the host file and restart. No rebuild. ```bash vim /srv/config///secrets.env # edit on the host docker compose restart yourapp # pick up the change — no rebuild ```