New: Try Voli The Bear, Fast package manager (and not only) for Windows
Updated Jun 19, 2026 Edit on GitHub

Auto-Deploy on Merge

Right now, shipping a change means: SSH in, git pull, recreate the containers, by hand, every time. That's fine once; it's friction forever, and friction is where bugs and stale deploys live. The finish line is merge to main → it's live, automatically. We'll use GitHub Actions; for how pipelines work in general, Your First Pipeline (GitHub Actions) and What a CI/CD Pipeline Actually Does are the foundations - here's the deploy-over-SSH shape specifically.

The shape: push → Action → SSH → pull + recreate

On every push to main, a workflow runs on GitHub's machines, opens an SSH connection to your box using a key you've stored as a secret, and runs the same commands you'd type by hand.

The deploy key and the secret

Give the Action its own way in (never your personal key):

  1. Generate a deploy SSH key pair for CI. Put the public half in the box's ~/.ssh/authorized_keys (so the Action can log in), and store the private half in GitHub → repo → Settings → Secrets and variables → Actions as, say, DEPLOY_SSH_KEY. Add DEPLOY_HOST and DEPLOY_USER too.
  2. Secrets are encrypted and injected at run time - never printed, never committed.

The workflow

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]      # only merges/pushes to main deploy

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: SSH in and update the stack
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            cd ~/your-project
            git pull
            docker compose up -d --force-recreate

What just happened: on a push to main, GitHub spins up a runner, connects to your box with the secret key, and runs the deploy script - git pull to fetch the merged code, then docker compose up -d --force-recreate to bring the stack up on it. From now on, merging is deploying. No SSH, no remembering the commands.

⚠️ The two traps, one last time

⚠️ --force-recreate, not restart. The script uses docker compose up -d --force-recreate on purpose. If it ran docker compose restart, a deploy that only changed an env value or rebuilt an image would silently keep running the old containers - the Action goes green, and nothing actually changed. This is the Phase 3 trap, now automated: recreate, don't restart.

⚠️ Don't build on the tiny box if you can help it. git pull + docker compose up -d --build builds on the server - straight back into the Phase 1 OOM risk, now on every deploy. Two better shapes:

  • Build in the Action, pull the image on the box. The runner has plenty of RAM: build the image there, push it to a registry, and have the box's compose file just pull it (docker compose pull && up -d). No build on the box, ever.
  • Or keep building on the box but guarantee swap (Phase 1) so the build can't get OOM-killed mid-deploy.

💡 Key point. A deploy should be boring. The recipe - pull, recreate, prefer a pre-built image - makes every future change a merge and nothing more.

You shipped it

Step back at what you built across these six phases: a box you chose wisely and won't get surprise-billed for, a locked-down SSH key login, your private app running in Docker, a real domain, free HTTPS behind Cloudflare with the cookie/CSRF/firewall traps handled - and now a one-merge deploy. The gap between "works on my laptop" and "a stranger uses it at a URL" is closed, and you know where every trap was hiding.

The next time someone says "just deploy it," that word won't make your stomach drop. You've done the whole journey once, with eyes open - and that's the only time it's ever hard.

Recap

  1. GitHub Actions on push to main SSHes into the box (with a deploy key in Secrets) and runs your deploy script - merging becomes deploying.
  2. ⚠️ Use docker compose up -d --force-recreate, not restart, or deploys silently keep the old containers.
  3. ⚠️ Avoid building on the tiny box - build in the Action and pull the image, or guarantee swap so the deploy build can't OOM.

← Phase 5: Behind Cloudflare · Guide overview

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. What is the shape of auto-deploy on merge with GitHub Actions?

2. Why must the deploy script use `--force-recreate` rather than `restart`?

3. What is the better way to avoid OOM on every deploy to a tiny box?