How to Publish a Server with Docker and GitHub Actions
A step-by-step guide to automating your server deployment pipeline using Docker containers and GitHub Actions CI/CD.
How to Publish a Server with Docker and GitHub Actions
Deploying a server used to mean manually SSHing into a machine, pulling code, and restarting processes. Today, with Docker and GitHub Actions, you can fully automate this workflow — from a git push to a running container.
Prerequisites
Before we start, make sure you have:
- A server (VPS, Raspberry Pi, or any Linux machine)
- Docker installed on the server
- A GitHub repository with your application code
- SSH access to your server
Step 1: Dockerize Your Application
First, create a Dockerfile at the root of your project:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]This uses a multi-stage build to keep the final image small — only the production artifacts are included in the runner stage.
Step 2: Set Up Docker Compose on Your Server
On your server, create a docker-compose.yml file:
version: "3.8"
services:
app:
image: ghcr.io/your-username/your-repo:latest
restart: unless-stopped
ports:
- "3000:3000"
environment:
- DATABASE_URL=${DATABASE_URL}The restart: unless-stopped policy ensures your container automatically recovers after a server reboot.
Step 3: Configure GitHub Secrets
In your GitHub repository, go to Settings → Secrets and variables → Actions and add:
SERVER_HOST— your server's IP or domainSERVER_USER— SSH username (e.g.,ubuntu)SERVER_SSH_KEY— your private SSH key (the one whose public key is on the server)
Step 4: Create the GitHub Actions Workflow
Create .github/workflows/deploy.yml:
name: Deploy to Server
on:
push:
branches:
- main
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:latest
deploy:
runs-on: ubuntu-latest
needs: build-and-push
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /opt/your-app
docker compose pull
docker compose up -dThe workflow is split into two jobs:
- build-and-push — builds the Docker image and pushes it to GitHub Container Registry (GHCR)
- deploy — SSHes into your server, pulls the new image, and restarts the container
Step 5: First-Time Setup on the Server
Before the first deployment, log into your server and pull the image manually:
docker login ghcr.io -u your-username --password-stdin
docker compose pull
docker compose up -dAfter that, every push to main will automatically build, push, and deploy your application.
Key Takeaways
- Multi-stage Docker builds reduce image size significantly
- GitHub Container Registry is free for public repositories and integrated with GitHub Actions
- The
appleboy/ssh-actionhandles SSH deployment cleanly without needing a self-hosted runner - Secrets management keeps credentials out of your codebase
- With
docker compose pull && docker compose up -d, zero-downtime-ish updates are straightforward for simple apps
This pattern scales well from a hobby project to a production service — just add health checks, rollback logic, and notifications as your needs grow.