Deploy to a Server¶
This is the moment the pipeline stops just publishing an image and starts deploying it. When you cut a release, the pipeline will connect to a real server and put the new version live, with no manual steps. It automates the deploy you did by hand in the Docker course. 🚀
ℹ️ You can read this without a server. The deployment steps need a VM, which costs a little. Follow along if you have one, or read through to understand how automated deployment works and return when you are ready.
What we will do (in very simple steps)¶
- Add the server credentials as secrets
- Add a
deployjob that connects over SSH - Cut a release and watch it deploy itself
Prerequisites¶
- A server with Docker installed (the same kind you set up in the Docker course's deploy lesson)
- The image package set to public, so the server can pull it (on GitHub, open the package under Packages, then Package settings, and change visibility to public)
Step 1: Create a deploy key and add the secrets¶
Your pipeline needs to log in to the server without a password, using an SSH key.
On your machine, create a key pair just for deployment:
That makes two files: snapshot-deploy-key (private) and snapshot-deploy-key.pub (public).
- Add the public key to your server, by appending the contents of
snapshot-deploy-key.pubto~/.ssh/authorized_keyson the server. - Add three secrets to your repository (Settings, then Secrets and variables, then Actions), just like the last lesson:
SSH_HOST: your server's IP addressSSH_USER: the login name on the serverSSH_PRIVATE_KEY: the full contents of the privatesnapshot-deploy-keyfile
Step 2: Add the deploy job¶
Open .github/workflows/ci.yml and add this third job, after the build job:
deploy:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Deploy over SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
docker pull ghcr.io/${{ github.repository }}:${{ github.ref_name }}
docker rm -f snapshot || true
docker run -d --name snapshot -p 80:5000 ghcr.io/${{ github.repository }}:${{ github.ref_name }}
Reading it:
needs: buildruns deploy only after the image is built and published.if: startsWith(github.ref, 'refs/tags/v')means deploy runs only on version tags, not on every push. Ordinary work still tests and builds, but only a release actually goes live.- The
appleboy/ssh-actionconnects using your secrets and runs the script on the server: pull the new image, remove the old container, start the new one.
Your pipeline is now three stages:
Step 3: Release and watch it deploy¶
Commit the workflow, then cut a release:
In the Actions tab, the tag triggers all three jobs in sequence. When deploy goes green, visit your server's address in a browser. The new version is live, deployed entirely by the pipeline. 🎉
From now on, shipping a release is just pushing a tag.
✅ Checkpoint¶
You are ready for the next lesson if:
- The three secrets are set, and the deploy key works
- The
deployjob runs only on version tags, afterbuild - Pushing a version tag deployed the app to your server
🩹 Common hiccups¶
- SSH connection fails: the public key must be in the server's
authorized_keys, andSSH_PRIVATE_KEYmust contain the whole private key, including its first and last lines. - "pull access denied" on the server: the image package is still private. Set it to public, or log in to the registry on the server.
- "invalid reference format":
ghcr.ioneeds lowercase names. If your username has capitals, that is the cause. deploywas skipped: it only runs on version tags. A plain push will not trigger it, by design.
Next up: Environments and Gates, where you add staging and production, with a manual approval before anything reaches production.