· 3 months ago · Jul 04, 2025, 10:10 PM
1name: Backend CI/CD
2
3on:
4 push:
5 paths:
6 - 'backend/**'
7 - 'backend/infra/traefik/**'
8 - '.github/workflows/ci-cd-pipeline.yml'
9 branches: [main]
10 pull_request:
11 paths:
12 - 'backend/**'
13 - 'backend/infra/traefik/**'
14 - '.github/workflows/ci-cd-pipeline.yml'
15
16jobs:
17 build-and-push:
18 name: Build and Push Docker Image
19 runs-on: ubuntu-latest
20 outputs:
21 image_tag: ${{ steps.get_version.outputs.image_tag }}
22 steps:
23 - name: Checkout code
24 uses: actions/checkout@v3
25
26 - name: Set up Docker Buildx
27 uses: docker/setup-buildx-action@v3
28
29 - name: Log in to DigitalOcean Container Registry
30 uses: docker/login-action@v3
31 with:
32 registry: registry.digitalocean.com
33 username: do
34 password: ${{ secrets.DOCR_TOKEN }}
35
36 - name: Install Poetry
37 run: pip install poetry
38
39 - name: Get backend version
40 id: get_version
41 working-directory: ./backend
42 run: |
43 VERSION=$(poetry version -s)
44 echo "VERSION=$VERSION" >> $GITHUB_ENV
45 echo "image_tag=$VERSION" >> $GITHUB_OUTPUT
46
47 - name: Print VERSION
48 run: echo "VERSION is ${{ env.VERSION }}"
49 env:
50 VERSION: ${{ env.VERSION }}
51
52 - name: Build and push Docker image
53 id: build_push
54 uses: docker/build-push-action@v5
55 env:
56 VERSION: ${{ env.VERSION }}
57 with:
58 context: ./backend
59 file: ./backend/Dockerfile
60 push: true
61 tags: |
62 registry.digitalocean.com/hackalyst-backend/hackalyst-backend:latest
63 registry.digitalocean.com/hackalyst-backend/hackalyst-backend:${{ env.VERSION }}
64 registry.digitalocean.com/hackalyst-backend/hackalyst-backend:${{ github.sha }}
65
66 deploy-traefik:
67 name: Deploy Traefik Configuration
68 needs: build-and-push
69 runs-on: ubuntu-latest
70 if: github.ref == 'refs/heads/main'
71 steps:
72 - name: Checkout code
73 uses: actions/checkout@v3
74
75 - name: Copy Traefik files to remote server
76 uses: appleboy/scp-action@master
77 with:
78 host: ${{ secrets.DROPLET_IP }}
79 username: ${{ secrets.DROPLET_USER }}
80 key: ${{ secrets.DROPLET_SSH_KEY }}
81 source: "backend/infra/traefik/*,backend/infra/traefik/dynamic/*"
82 target: "/opt/hackalyst/"
83 strip_components: 2
84
85 - name: Deploy Traefik Configuration
86 uses: appleboy/ssh-action@v1.0.0
87 with:
88 host: ${{ secrets.DROPLET_IP }}
89 username: ${{ secrets.DROPLET_USER }}
90 key: ${{ secrets.DROPLET_SSH_KEY }}
91 script_stop: true
92 script: |
93 # Create traefik directory if it doesn't exist
94 mkdir -p /opt/hackalyst/traefik
95 mkdir -p /opt/hackalyst/traefik/dynamic
96
97 # Navigate to Traefik directory
98 cd /opt/hackalyst/traefik
99
100 # Fix acme.json permissions
101 touch acme/acme.json
102 chmod 600 acme/acme.json
103
104 # Remove the version line from docker-compose.yml
105 grep -v "^version:" docker-compose.yml > docker-compose.tmp && mv docker-compose.tmp docker-compose.yml
106
107 # Update network configuration to use external network
108 sed -i 's/external: false/external: true/' docker-compose.yml
109
110 # Fix network issue by handling active containers before recreating the network
111 # First stop traefik container if it exists
112 docker stop traefik || true
113 docker rm traefik || true
114
115 # Handle active endpoints on the proxy network
116 if docker network inspect proxy 2>/dev/null; then
117 # Get list of containers connected to the proxy network
118 CONNECTED_CONTAINERS=$(docker network inspect proxy -f '{{range .Containers}}{{.Name}} {{end}}')
119
120 if [ -n "$CONNECTED_CONTAINERS" ]; then
121 echo "Disconnecting containers from proxy network: $CONNECTED_CONTAINERS"
122 for CONTAINER in $CONNECTED_CONTAINERS; do
123 echo "Disconnecting $CONTAINER from proxy network"
124 docker network disconnect -f proxy "$CONTAINER" || true
125 done
126 fi
127
128 # Now it's safe to remove the network
129 docker network rm proxy || true
130 fi
131
132 # Create the network with proper subnet and labels
133 docker network create --subnet=137.184.161.0/24 --gateway=137.184.161.1 --label="com.docker.compose.network=proxy" proxy
134
135 # Pull latest changes and restart Traefik
136 # Ensure Traefik dashboard port is available
137 if netstat -tuln | grep -q ':8081'; then
138 echo "Warning: Port 8081 is already in use. Checking what's using it..."
139 fuser -n tcp 8081 || true
140 lsof -i:8081 || true
141 echo "Attempting to stop any process using port 8081..."
142 fuser -k 8081/tcp || true
143 fi
144
145 # Pass the Traefik dashboard credentials from GitHub secrets
146 export TRAEFIK_DASHBOARD_CRED="${{ secrets.TRAEFIK_DASHBOARD_CRED }}"
147
148 docker compose -f docker-compose.yml pull
149 docker compose -f docker-compose.yml up -d
150
151 deploy-production:
152 name: Deploy to Production Droplet
153 needs: deploy-traefik
154 runs-on: ubuntu-latest
155 if: github.ref == 'refs/heads/main'
156 steps:
157 - name: Checkout code
158 uses: actions/checkout@v3
159
160 - name: Install jq
161 run: sudo apt-get update && sudo apt-get install -y jq
162
163 - name: Install Infisical CLI
164 run: |
165 curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | sudo -E bash
166 sudo apt-get update && sudo apt-get install -y infisical
167
168 - name: Generate Infisical Access Token
169 run: |
170 chmod +x backend/scripts/generate_infisical_token.sh
171 source backend/scripts/generate_infisical_token.sh
172 echo "INFISICAL_ACCESS_TOKEN=$INFISICAL_ACCESS_TOKEN" >> $GITHUB_ENV
173 env:
174 INFISICAL_MACHINE_IDENTITY_ID: ${{ secrets.INFISICAL_MACHINE_IDENTITY_ID }}
175 INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET: ${{ secrets.INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET }}
176 INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_PROJECT_ID }}
177
178 - name: Deploy to Production Droplet
179 uses: appleboy/ssh-action@v1.0.0
180 with:
181 envs: INFISICAL_MACHINE_IDENTITY_ID,INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET,INFISICAL_PROJECT_ID
182 host: ${{ secrets.DROPLET_IP }}
183 username: ${{ secrets.DROPLET_USER }}
184 key: ${{ secrets.DROPLET_SSH_KEY }}
185 script: |
186 # Install Docker if needed
187 if ! command -v docker &> /dev/null; then
188 curl -fsSL https://get.docker.com -o get-docker.sh
189 sudo sh get-docker.sh
190 sudo usermod -aG docker $USER
191 newgrp docker
192 fi
193
194 # Login to DigitalOcean Container Registry
195 echo "${{ secrets.DOCR_TOKEN }}" | docker login \
196 --username do \
197 --password-stdin \
198 registry.digitalocean.com
199
200 # Pull the latest image
201 docker pull registry.digitalocean.com/hackalyst-backend/hackalyst-backend:latest
202
203 # Stop and remove existing container if it exists
204 docker stop hackalyst-backend || true
205 docker rm hackalyst-backend || true
206
207 # Ensure proxy network exists with the correct labels and subnet
208 # Remove existing network if it exists
209 docker network inspect proxy >/dev/null 2>&1 && docker network rm proxy
210
211 # Create network with specific subnet
212 docker network create --subnet=137.184.161.0/24 --gateway=137.184.161.1 proxy --label="com.docker.compose.network=proxy"
213
214 # Run the new container with Infisical environment variables and Traefik labels
215 docker run -d --name hackalyst-backend --network proxy \
216 --ip 137.184.161.2 \
217 --network-alias hackalyst-backend \
218 -p 127.0.0.1:8000:8000 \
219 -e INFISICAL_MACHINE_IDENTITY_ID="${{ secrets.INFISICAL_MACHINE_IDENTITY_ID }}" \
220 -e INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET="${{ secrets.INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET }}" \
221 -e INFISICAL_PROJECT_ID="${{ secrets.INFISICAL_PROJECT_ID }}" \
222 -l "traefik.enable=true" \
223 -l "traefik.http.routers.backend.rule=Host(\`backend.hackalyst.com\`)" \
224 -l "traefik.http.routers.backend.entrypoints=websecure" \
225 -l "traefik.http.routers.backend.tls.certresolver=letsencrypt" \
226 -l "traefik.http.services.backend.loadbalancer.server.port=3000" \
227 --restart unless-stopped \
228 registry.digitalocean.com/hackalyst-backend/hackalyst-backend:latest
229
230 # Verify DNS resolution is working correctly
231 echo "Verifying DNS resolution from traefik to hackalyst-backend..."
232 docker exec traefik ping -c 4 hackalyst-backend
233
234 # Verify IP address assignment
235 echo "Verifying IP address assignment..."
236 docker inspect hackalyst-backend | grep "IPv4Address"
237
238 env:
239 INFISICAL_MACHINE_IDENTITY_ID: ${{ secrets.INFISICAL_MACHINE_IDENTITY_ID }}
240 INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET: ${{ secrets.INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET }}
241 INFISICAL_ACCESS_TOKEN: ${{ env.INFISICAL_ACCESS_TOKEN }}
242