Repository navigation
239 lines (211 loc) · 9.2 KB
/
Copy pathdeploy.yml
File metadata and controls
239 lines (211 loc) · 9.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
name: Deploy to Linode
# Manual dispatch only. The Linode this workflow deployed to has been deleted,
# and its old address now belongs to a server we do not own, so a push to master
# must not try to sign in to it. A deploy waits for a new host: set LINODE_HOST
# and SSH_PRIVATE_KEY for that host and LINODE_HOST_FINGERPRINT to its SHA256
# host key fingerprint. Until LINODE_HOST_FINGERPRINT is set, the first step
# fails and no SSH connection is made. Restore the push trigger only after a
# dispatched deploy to the new host has succeeded.
on:
workflow_dispatch:
env:
DEPLOY_DIR: /app/wunderland
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Require a pinned host key
env:
HOST_FINGERPRINT: ${{ secrets.LINODE_HOST_FINGERPRINT }}
run: |
if [ -z "$HOST_FINGERPRINT" ]; then
echo "::error::LINODE_HOST_FINGERPRINT is not set. The deploy host is gone; set up a new host and pin its key first."
exit 1
fi
- uses: actions/checkout@v5
- name: Create deployment archive
run: |
# tar exits 1 on "file changed as we read it" — non-fatal, only fail on exit >= 2
set +e
tar czf deploy.tar.gz \
--exclude='node_modules' \
--exclude='.next' \
--exclude='dist' \
--exclude='.env' \
--exclude='.env.local' \
--exclude='*.sqlite' \
--exclude='*.sqlite-journal' \
--exclude='db_data' \
--exclude='anchor/target' \
--exclude='docs-site/node_modules' \
--exclude='docs-site/.docusaurus' \
--exclude='docs-site/build' \
--exclude='.git' \
--exclude='packages/wunderland/apps' \
--exclude='packages/wunderland/packages' \
--exclude='packages/wunderland/frontend' \
--exclude='packages/wunderland/backend' \
.
TAR_EXIT=$?
set -e
if [ $TAR_EXIT -gt 1 ]; then
echo "tar failed with exit code $TAR_EXIT"
exit 1
fi
ls -lh deploy.tar.gz
- name: Upload source to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.LINODE_HOST }}
username: root
key: ${{ secrets.SSH_PRIVATE_KEY }}
fingerprint: ${{ secrets.LINODE_HOST_FINGERPRINT }}
source: deploy.tar.gz
target: /tmp/
- name: Deploy with Docker Compose
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.LINODE_HOST }}
username: root
key: ${{ secrets.SSH_PRIVATE_KEY }}
fingerprint: ${{ secrets.LINODE_HOST_FINGERPRINT }}
command_timeout: 30m
script: |
set -euo pipefail
DEPLOY_DIR=/app/wunderland
# ── Ensure storage and SSL directories ──
mkdir -p /mnt/storage-wunder/{ipfs,db,media,logs/nginx}
mkdir -p /app/ssl
# Generate self-signed SSL cert if none exists (Cloudflare Full mode)
if [ ! -f /app/ssl/origin.crt ]; then
echo "[deploy] Generating self-signed SSL cert..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /app/ssl/origin.key \
-out /app/ssl/origin.crt \
-subj "/CN=wunderland.sh/O=Wunderland/C=US"
fi
# ── Stop old systemd services (migration from previous deploy) ──
systemctl stop wunderland-sol 2>/dev/null || true
systemctl disable wunderland-sol 2>/dev/null || true
systemctl stop ipfs 2>/dev/null || true
systemctl disable ipfs 2>/dev/null || true
systemctl stop nginx 2>/dev/null || true
# ── Extract fresh source ──
# Use find+rm as fallback — deeply nested dirs from previous deploys
# can cause plain rm -rf to fail with "Directory not empty"
rm -rf ${DEPLOY_DIR}/src-new 2>/dev/null || true
if [ -d "${DEPLOY_DIR}/src-new" ]; then
find "${DEPLOY_DIR}/src-new" -depth -delete 2>/dev/null || true
rm -rf ${DEPLOY_DIR}/src-new 2>/dev/null || true
fi
mkdir -p ${DEPLOY_DIR}/src-new
cd ${DEPLOY_DIR}/src-new
tar xzf /tmp/deploy.tar.gz
rm -f /tmp/deploy.tar.gz
# Stop existing compose stack before swapping source
if [ -d ${DEPLOY_DIR}/src/deployment ]; then
cd ${DEPLOY_DIR}/src/deployment
docker compose down 2>/dev/null || true
fi
# Kill any lingering containers from previous deploys (different project names)
for name in wunderland-sol deployment; do
for svc in ipfs backend frontend nginx; do
docker rm -f "${name}-${svc}-1" 2>/dev/null || true
docker rm -f "${name}_${svc}_1" 2>/dev/null || true
done
done
# Force-release ports held by stale docker-proxy processes
for port in 4001 80 443; do
fuser -k ${port}/tcp 2>/dev/null || true
done
# Prune orphaned networks that may hold port bindings
docker network prune -f 2>/dev/null || true
sleep 2
# Move to a stable directory before the swap — docker compose
# caches its CWD and fails with "getwd: no such file or directory"
# if the previous CWD is deleted during the atomic swap.
cd /tmp
# Atomic swap
rm -rf ${DEPLOY_DIR}/src-old
if [ -d ${DEPLOY_DIR}/src ]; then
mv ${DEPLOY_DIR}/src ${DEPLOY_DIR}/src-old
fi
mv ${DEPLOY_DIR}/src-new ${DEPLOY_DIR}/src
rm -rf ${DEPLOY_DIR}/src-old
# ── Ensure .env exists ──
# Pre-place your .env at /app/wunderland/.env on the server.
# It gets symlinked into the deployment dir for docker-compose.
if [ -f ${DEPLOY_DIR}/.env ]; then
ln -sf ${DEPLOY_DIR}/.env ${DEPLOY_DIR}/src/deployment/.env
elif [ ! -f ${DEPLOY_DIR}/src/deployment/.env ]; then
echo "ERROR: No .env file found!"
echo "Create one at ${DEPLOY_DIR}/.env with your secrets."
echo "See .env.example for required variables."
exit 1
fi
# ── Install Docker if not present ──
if ! command -v docker &> /dev/null; then
echo "[deploy] Installing Docker..."
curl -fsSL https://get.docker.com | sh
systemctl enable docker
systemctl start docker
fi
# ── Ensure swap for Docker builds (prevents OOM kills) ──
if [ ! -f /swapfile ]; then
echo "[deploy] Creating 2GB swap..."
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
elif ! swapon --show | grep -q /swapfile; then
swapon /swapfile 2>/dev/null || true
fi
# ── Deploy full stack with Docker Compose (build sequentially) ──
cd ${DEPLOY_DIR}/src/deployment
docker compose build --no-cache backend
docker compose build --no-cache frontend
# Start stack — retry once if port conflict from slow docker-proxy cleanup
if ! docker compose up -d 2>&1; then
echo "[deploy] First attempt failed — killing stale port holders and retrying..."
docker compose down 2>/dev/null || true
for port in 4001 80 443; do
fuser -k ${port}/tcp 2>/dev/null || true
done
docker network prune -f 2>/dev/null || true
sleep 5
docker compose up -d
fi
# ── Prune old images ──
docker image prune -f 2>/dev/null || true
# ── Health checks ──
echo "[deploy] Waiting for services to start..."
sleep 15
echo "=== Service Status ==="
docker compose ps
echo ""
echo "=== Backend Health ==="
if curl -sf http://localhost:3001/api/health; then
echo ""
echo "Backend: OK"
else
echo "WARNING: Backend health check failed. Checking logs..."
docker compose logs --tail=30 backend
fi
echo ""
echo "=== Frontend ==="
if curl -sf -o /dev/null http://localhost:3011; then
echo "Frontend: OK"
else
echo "WARNING: Frontend not responding. Checking logs..."
docker compose logs --tail=30 frontend
fi
echo ""
echo "=== IPFS ==="
if docker compose exec -T ipfs ipfs id >/dev/null 2>&1; then
echo "IPFS: OK"
else
echo "WARNING: IPFS not responding"
fi
echo ""
echo "[deploy] Done. Full stack deployed (IPFS + Backend + Frontend + Nginx)."