Skip to content

Commit 5b66d98

Browse files
feat: nginx + deployment
1 parent 9c33fed commit 5b66d98

2 files changed

Lines changed: 200 additions & 0 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Deploy Realworld React
2+
3+
on:
4+
push:
5+
branches: [release]
6+
7+
jobs:
8+
build-and-deploy:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Checkout code
13+
uses: actions/checkout@v4
14+
15+
- name: Set up Docker Buildx
16+
uses: docker/setup-buildx-action@v3
17+
18+
- name: Login to Docker Hub
19+
uses: docker/login-action@v3
20+
with:
21+
username: ${{ secrets.DOCKERHUB_USERNAME }}
22+
password: ${{ secrets.DOCKERHUB_TOKEN }}
23+
24+
- name: Build and push
25+
uses: docker/build-push-action@v5
26+
with:
27+
context: .
28+
push: true
29+
tags: |
30+
${{ secrets.DOCKERHUB_USERNAME }}/realworld-react:latest
31+
${{ secrets.DOCKERHUB_USERNAME }}/realworld-react:${{ github.sha }}
32+
target: builder
33+
34+
- name: Deploy to VPS
35+
env:
36+
VITE_APP_API_URL: ${{ secrets.VITE_APP_API_URL }}
37+
VITE_APP_APP_URL: ${{ secrets.VITE_APP_APP_URL }}
38+
run: |
39+
mkdir -p ~/.ssh
40+
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
41+
chmod 600 ~/.ssh/deploy_key
42+
43+
ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no deploy@$VPS_HOST << EOF
44+
# Pull latest image
45+
docker pull $DOCKERHUB_USERNAME/realworld-react:latest
46+
47+
# Extract build artifacts
48+
docker run --rm -v /tmp/react-build:/output \
49+
$DOCKERHUB_USERNAME/realworld-react:latest \
50+
sh -c "cp -r /app/dist/* /output/"
51+
52+
# Copy to web directory
53+
sudo rm -rf /var/www/realworld-react/*
54+
sudo cp -r /tmp/react-build/* /var/www/realworld-react/
55+
sudo chown -R www-data:www-data /var/www/realworld-react
56+
57+
# Cleanup
58+
rm -rf /tmp/react-build
59+
EOF

nginx/nginx.conf

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# SSL Certificate Setup Instructions:
2+
#
3+
# 1. Install certbot:
4+
# sudo apt update && sudo apt install certbot python3-certbot-nginx
5+
#
6+
# 2. Create a temporary nginx config to get certificates (HTTP-01 challenge):
7+
# sudo nano /etc/nginx/sites-available/temp
8+
#
9+
# server {
10+
# listen 80;
11+
# server_name realworld.minhhoccode111.com;
12+
# location / {
13+
# return 200 "ACME challenge ready";
14+
# }
15+
# }
16+
#
17+
# 3. Enable and test the temporary config:
18+
# sudo ln -s /etc/nginx/sites-available/temp /etc/nginx/sites-enabled/temp
19+
# sudo nginx -t && sudo systemctl reload nginx
20+
#
21+
# 4. Obtain SSL certificate:
22+
# sudo certbot certonly --nginx -d realworld.minhhoccode111.com
23+
#
24+
# 5. Setup auto-renewal:
25+
# sudo certbot renew --dry-run
26+
# # Certbot automatically sets up a cron job or systemd timer
27+
# sudo systemctl list-timers | grep certbot
28+
#
29+
# 6. After obtaining certs, replace temporary config with this full config:
30+
# sudo rm /etc/nginx/sites-enabled/temp
31+
# sudo cp nginx.conf /etc/nginx/nginx.conf (or add to sites-enabled)
32+
# sudo nginx -t && sudo systemctl reload nginx
33+
#
34+
# Certificate files will be located at:
35+
# - /etc/letsencrypt/live/realworld.minhhoccode111.com/fullchain.pem
36+
# - /etc/letsencrypt/live/realworld.minhhoccode111.com/privkey.pem
37+
#
38+
# To test SSL configuration:
39+
# https://www.ssllabs.com/ssltest/analyze.html?d=realworld.minhhoccode111.com
40+
#
41+
# To renew certificates manually:
42+
# sudo certbot renew --force-renewal
43+
# sudo systemctl reload nginx
44+
45+
events {
46+
worker_connections 1024;
47+
}
48+
49+
http {
50+
include /etc/nginx/mime.types;
51+
default_type application/octet-stream;
52+
53+
sendfile on;
54+
sendfile_max_chunk 1m;
55+
tcp_nopush on;
56+
tcp_nodelay on;
57+
keepalive_timeout 65;
58+
59+
open_file_cache max=1000 inactive=20s;
60+
open_file_cache_valid 30;
61+
open_file_cache_min_uses 2;
62+
63+
# Logging
64+
# access_log /var/log/nginx/access.log main;
65+
error_log /var/log/nginx/error.log;
66+
67+
# Rate Limit
68+
limit_req_zone $binary_remote_addr zone=static_limit:10m rate=30r/s;
69+
limit_req_status 429;
70+
71+
# Compression
72+
gzip on;
73+
gzip_vary on;
74+
gzip_proxied any;
75+
gzip_comp_level 6;
76+
gzip_types
77+
text/plain
78+
text/css
79+
application/json
80+
application/javascript
81+
text/xml
82+
application/xml
83+
image/svg+xml;
84+
85+
# Redirect HTTP to HTTPS
86+
server {
87+
listen 80;
88+
server_name realworld.minhhoccode111.com;
89+
return 301 https://$server_name$request_uri;
90+
}
91+
92+
# Frontend static serving
93+
server {
94+
listen 443 ssl http2;
95+
server_name realworld.minhhoccode111.com;
96+
97+
# TLS certificates
98+
ssl_certificate /etc/letsencrypt/live/realworld.minhhoccode111.com/fullchain.pem;
99+
ssl_certificate_key /etc/letsencrypt/live/realworld.minhhoccode111.com/privkey.pem;
100+
101+
# Strong TLS settings
102+
ssl_protocols TLSv1.2 TLSv1.3;
103+
ssl_prefer_server_ciphers off;
104+
ssl_session_cache shared:SSL:10m;
105+
ssl_session_timeout 1d;
106+
107+
# Path to your React 'dist' or 'build' folder
108+
root /var/www/realworld-react/dist;
109+
index index.html;
110+
111+
# React Router Fix: Fallback to index.html for SPA routing
112+
location / {
113+
try_files $uri $uri/ /index.html;
114+
}
115+
116+
# Static Assets Caching
117+
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
118+
limit_req zone=static_limit burst=20 nodelay;
119+
120+
etag on;
121+
expires 1y;
122+
add_header Cache-Control "public, immutable";
123+
access_log off;
124+
}
125+
126+
# Security Headers
127+
add_header X-Frame-Options "SAMEORIGIN";
128+
add_header X-Content-Type-Options "nosniff";
129+
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
130+
add_header X-XSS-Protection "1; mode=block";
131+
add_header Referrer-Policy "strict-origin-when-cross-origin";
132+
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
133+
134+
# Health check
135+
location /health {
136+
access_log off;
137+
return 200 "healthy\n";
138+
add_header Content-Type text/plain;
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)