Docker & Your Private Repo
The box is yours and you're logged in. Now get your app running on it. We'll assume your project already
has a docker-compose.yml (if Docker or Compose themselves are fuzzy,
Docker Without the Magic and
Docker Compose for Real Projects are the foundations this
phase stands on). Three moves: install Docker, get your code onto the box, bring it up.
Install Docker
The official convenience script is the fastest path on a fresh Linux box:
$ curl -fsSL https://get.docker.com | sh
$ sudo docker compose version
Docker Compose version v2.29.7
What just happened: the script installed Docker Engine and the Compose plugin. If docker compose version answers, you're set. (Add your user to the docker group to drop the sudo:
sudo usermod -aG docker $USER, then log out and back in.)
Get your PRIVATE repo onto the box: a deploy key
If your repo is private, git clone will be refused - the box has no permission to read it. The clean
fix is a deploy key: an SSH key pair whose public half you register on that one repository, granting
read-only access. Generate it on the server (so the private key never leaves the box), add the public
half to the repo, then clone over SSH.
$ ssh-keygen -t ed25519 -C "deploy@myserver" -f ~/.ssh/deploy_key # make a key ON the box
$ cat ~/.ssh/deploy_key.pub # paste this into GitHub
On GitHub: repo → Settings → Deploy keys → Add deploy key, paste the .pub, leave "Allow write
access" unchecked (read-only is all a deploy needs). Then tell the box to use that key and clone:
$ git clone [email protected]:you/your-project.git
Cloning into 'your-project'...
remote: Enumerating objects: 312, done.
Receiving objects: 100% (312/312), done.
What just happened: the deploy key proved the box is allowed to read this repo (and no other), so the clone succeeded over SSH. 📝 Deploy key = an SSH key scoped to a single repository - safer than putting your personal account's key on a server, because if the box is compromised, the blast radius is read access to one repo.
Bring it up
$ cd your-project
$ cp .env.example .env && nano .env # fill in your real secrets
$ docker compose up -d
[+] Running 3/3
✔ Container your-project-db-1 Started
✔ Container your-project-api-1 Started
✔ Container your-project-web-1 Started
What just happened: Compose built/pulled the images and started every service in the background (-d).
Your app is now running on the box - reachable at the server's IP on whatever port you exposed. (Not at
a domain or over HTTPS yet - that's the next two phases.)
⚠️ Build OOM, again. If docker compose up builds on a tiny box, it can get Killed (exit 137) for
the out-of-memory reason from Phase 1. Same fixes: add swap, or build the image
elsewhere and pull it.
⚠️ The two .env traps that eat your afternoon
These are the deploy bugs that make people question reality, because nothing is "wrong" - the tool is behaving exactly as designed, just not as you assumed.
⚠️ A $ in a value gets mangled. Docker Compose performs variable substitution - it treats $NAME
and ${NAME} in values as "insert another variable here." So a password like pa$$w0rd in your .env
becomes paw0rd (it tried to expand $$ and $w0rd), and your database auth fails with a password you'd
swear is correct. The fix: escape each $ as $$ in values Compose reads, or generate secrets
without $:
# .env - a literal $ must be doubled so Compose doesn't try to expand it
DB_PASSWORD=pa$$$$w0rd # the value the container receives is pa$$w0rd
What just happened: each real $ is written as $$, so Compose's substitution leaves a single $
behind in the value the container actually gets. This one is invisible until a login or DB connection
fails for no reason you can see.
⚠️ restart does NOT re-read .env. You change a value in .env, run docker compose restart, and
your change doesn't take effect. That's because restart just stops and starts the existing
containers - it does not re-read .env or the compose file. To actually apply new env or image changes,
recreate the containers:
$ docker compose up -d --force-recreate
What just happened: up -d reconciles to the current .env/compose definition, and --force-recreate
forces new containers even if Compose thinks nothing changed - so your edited values are actually picked
up. Reach for this, not restart, whenever you touch .env. (Burn it in now; it comes back in
Phase 6.)
⏭️ Why config lives in
.envat all, and how${...}interpolation works, is Environment Variables & Config.
Recap
- Install Docker with the official script;
docker compose versionto confirm. - Private repo → deploy key: generate the key on the box, register its public half as a read-only deploy key on the repo, clone over SSH.
docker compose up -dbrings the stack up in the background. ⚠️ Building on a tiny box can OOM - swap, or build elsewhere.- ⚠️ A
$in.envis mangled by Compose substitution - double it ($$). - ⚠️
restartignores.envchanges - usedocker compose up -d --force-recreateto apply them.
It's running - but only reachable by raw IP. Let's give it a real name.
← Phase 2: SSH In With a Key · Guide overview · Phase 4: Domains & DNS →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. How do you give a server read access to your private repo safely?
2. A `$` in a value in your `.env` gets mangled by Docker Compose. Why and what is the fix?
3. After editing `.env`, why does `docker compose restart` not apply your change?